Last active
July 8, 2020 06:31
-
-
Save thomasmery/5fc438f43ef69704abe1b0ab32b7c22d to your computer and use it in GitHub Desktop.
Generated by XState Viz: https://xstate.js.org/viz
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Available variables: | |
// - Machine | |
// - interpret | |
// - assign | |
// - send | |
// - sendParent | |
// - spawn | |
// - raise | |
// - actions | |
// - XState (all XState exports) | |
/** | |
* Fan Actions State Machine | |
* a.k.a Fasm ... | |
* | |
* orchestrates the different states and holds data for the different steps | |
* of the interaction of a fan with the backlink page | |
*/ | |
var ConnectStatus; | |
(function (ConnectStatus) { | |
ConnectStatus["Idle"] = "idle"; | |
ConnectStatus["Connecting"] = "connecting"; | |
ConnectStatus["Connected"] = "connected"; | |
})(ConnectStatus || (ConnectStatus = {})); | |
/** | |
* The machine states | |
*/ | |
/** an enum to conveniently access the machine states */ | |
var FasmStates; | |
(function (FasmStates) { | |
FasmStates["landing"] = "landing"; | |
FasmStates["connecting"] = "connecting"; | |
FasmStates["connected"] = "connected"; | |
FasmStates["userForm"] = "userForm"; | |
FasmStates["redirect"] = "redirect"; | |
FasmStates["error"] = "error"; | |
})(FasmStates || (FasmStates = {})); | |
/** Events | |
* | |
* we're declaring different types of events separately | |
* and join them in the FasmEvent type | |
* note: this is done because it seems like using the generic union type FasmEvent is | |
* not enough in some cases and we need to type events more preciselys in some cases | |
* it also serves as a way to better identify the event types | |
* maybe revisit this as the xstate lib refines its typings | |
*/ | |
var FasmEventType; | |
(function (FasmEventType) { | |
FasmEventType["START"] = "START"; | |
FasmEventType["CONNECT"] = "CONNECT"; | |
FasmEventType["CONNECTED"] = "CONNECTED"; | |
FasmEventType["DISCONNECT"] = "DISCONNECT"; | |
FasmEventType["SHOW_USER_FORM"] = "SHOW_USER_FORM"; | |
FasmEventType["USER_FORM_CHANGE"] = "USER_FORM_CHANGE"; | |
FasmEventType["USER_FORM_SUBMIT"] = "USER_FORM_SUBMIT"; | |
FasmEventType["ERROR"] = "ERROR"; | |
})(FasmEventType || (FasmEventType = {})); | |
const { log } = actions; | |
const initialContext = { | |
backlinkId: '', | |
hasUserForm: true, | |
storeKey: '', | |
userFormData: { | |
fanId: '', | |
fanInfos: { | |
email: '', | |
optin_competition: false, | |
optin_subscription: false, | |
}, | |
formOTPToken: '', | |
}, | |
stores: {}, | |
// this redirect url will be store specific | |
redirectUrl: 'https://someRedirectUrl', | |
}; | |
/** | |
* wrapper for assigning a ConnectStatus to a store in the state machine context | |
* note: the actual connect status per store is not so relevant in the final flow | |
* but we keep this here as a reference to how to update the stores object in the context | |
* as we might need to do something similar with other store specific infos, | |
* it also serves a an example for a function that wraps the xstate assign action | |
* */ | |
const assignConnectStatus = (connectStatus) => assign({ | |
stores: (context, event) => { | |
const _event = event; | |
let storeKey = ''; | |
if (typeof _event.payload === 'string') { | |
storeKey = _event.payload; | |
} | |
else if (typeof _event.payload === 'object' && | |
_event.payload.storeKey) { | |
storeKey = _event.payload.storeKey; | |
} | |
if (storeKey) { | |
return Object.assign(Object.assign({}, context.stores), { [storeKey]: Object.assign(Object.assign({}, context.stores[storeKey]), { connectStatus }) }); | |
} | |
return context.stores; | |
}, | |
}); | |
/** | |
* updates a user form property | |
* prop key and values are part of the FasmEventUserFormChange | |
*/ | |
const assignUserFormProp = () => assign({ | |
userFormData: (context, event) => { | |
var _a; | |
const { key, value } = event.payload; | |
return Object.assign(Object.assign({}, context.userFormData), { fanInfos: Object.assign(Object.assign({}, (_a = context.userFormData) === null || _a === void 0 ? void 0 : _a.fanInfos), { [key]: value }) }); | |
}, | |
}); | |
/** | |
* The Machine | |
*/ | |
const fanActionsStateMachine = Machine({ | |
id: 'fan-actions', | |
initial: FasmStates.landing, | |
context: initialContext, | |
states: { | |
[FasmStates.landing]: { | |
entry: [assignConnectStatus(ConnectStatus.Idle)], | |
on: { | |
[FasmEventType.CONNECT]: { | |
target: FasmStates.connecting, | |
}, | |
[FasmEventType.SHOW_USER_FORM]: [ | |
{ | |
cond: 'canAccessUserForm', | |
target: FasmStates.userForm, | |
}, | |
{ | |
cond: 'hasStoreKey', | |
target: FasmStates.redirect, | |
actions: ['assignStoreKey'], | |
}, | |
{ | |
target: FasmStates.landing, | |
}, | |
], | |
}, | |
}, | |
[FasmStates.connecting]: { | |
entry: [assignConnectStatus(ConnectStatus.Connecting)], | |
on: { | |
[FasmEventType.CONNECTED]: { | |
target: FasmStates.connected, | |
}, | |
[FasmEventType.ERROR]: FasmStates.landing, | |
}, | |
}, | |
[FasmStates.connected]: { | |
entry: [ | |
assignConnectStatus(ConnectStatus.Connected), | |
assign({ | |
storeKey: (context, event) => { | |
var _a; | |
const storeKey = ((_a = event.payload) === null || _a === void 0 ? void 0 : _a.storeKey) || context.storeKey; | |
return storeKey; | |
}, | |
}), | |
], | |
invoke: { | |
id: 'subscribeFanWithConnectData', | |
src: 'subscribeFanWithConnectData', | |
onDone: [ | |
{ | |
/** transition to userForm only if Backlink CRM option is active */ | |
target: FasmStates.userForm, | |
cond: 'hasUserForm', | |
actions: ['assignUserFormData'], | |
}, | |
{ | |
target: `#fan-actions.${FasmStates.redirect}`, | |
actions: ['assignUserFormData'], | |
}, | |
], | |
onError: { | |
target: FasmStates.error, | |
}, | |
}, | |
}, | |
/** User Form */ | |
[FasmStates.userForm]: { | |
entry: [ | |
'assignStoreKey', | |
assignConnectStatus(ConnectStatus.Idle), | |
'navigate', | |
], | |
initial: 'editing', | |
// sub states for the user form | |
// TODO: make this a separate machine or simply extract this state node for readability | |
states: { | |
editing: { | |
on: { | |
/** | |
* controlled user form inputs data src | |
* should allow controlling every form input value | |
* if the input's name = key in the payload of the event | |
*/ | |
[FasmEventType.USER_FORM_CHANGE]: { | |
actions: assignUserFormProp(), | |
}, | |
[FasmEventType.USER_FORM_SUBMIT]: [ | |
{ | |
target: 'validating', | |
}, | |
], | |
}, | |
}, | |
/** a specific state to do validation through a service running async validation (w/ yup ATM) */ | |
validating: { | |
invoke: { | |
src: async (context) => { | |
const validationErrors = await validateUserForm(context.userFormData.fanInfos); | |
return validationErrors; | |
}, | |
onDone: { | |
target: 'submitting', | |
}, | |
onError: { | |
target: 'editing', | |
actions: assign({ | |
userFormErrors: (context, event) => { | |
return event.data; | |
}, | |
}), | |
}, | |
}, | |
}, | |
/** actually submitting the form infos via an xstate service that calls the api */ | |
submitting: { | |
invoke: { | |
id: 'subscribeFanWithFormData', | |
src: 'subscribeFanWithFormData', | |
onDone: { | |
target: `#fan-actions.${FasmStates.redirect}`, | |
actions: assign({ | |
/** | |
* the api returns the fan id | |
* it is part of the context userFormData object here | |
* we update it in case we did not have it from a Connect step | |
*/ | |
userFormData: (context, event) => { | |
return Object.assign(Object.assign({}, context.userFormData), { fanId: event.data.fanId }); | |
}, | |
}), | |
}, | |
onError: { | |
target: `editing`, | |
// deal with server errors | |
actions: assign({ | |
userFormErrors: (_context, event) => { | |
return { errors: [event.data.message] }; | |
}, | |
}), | |
}, | |
}, | |
}, | |
}, | |
}, | |
redirect: { | |
entry: ['redirect'], | |
}, | |
error: { | |
entry: [log()], | |
}, | |
}, | |
on: { | |
[FasmEventType.START]: FasmStates.landing, | |
}, | |
}, { | |
actions: { | |
assignStoreKey: assign({ | |
storeKey: (context, event) => { | |
var _a; | |
const storeKey = ((_a = event.payload) === null || _a === void 0 ? void 0 : _a.storeKey) || | |
context.storeKey; | |
return storeKey; | |
}, | |
}), | |
assignUserFormData: assign({ | |
userFormData: (context, event) => (Object.assign(Object.assign({}, event.data), { | |
// we need to keep the fields we already have in our initial form setup | |
fanInfos: Object.assign(Object.assign({}, context.userFormData.fanInfos), event.data.fanInfos) })), | |
}), | |
redirect: (context) => { | |
console.warn(`Redirect to ${context.redirectUrl}`); | |
// window.location.href = context.redirectUrl; | |
}, | |
}, | |
guards: { | |
hasUserForm: (context, _event) => context.hasUserForm, | |
hasStoreKey: (context, event) => { | |
var _a; | |
return !!context.storeKey || | |
!!((_a = event.payload) === null || _a === void 0 ? void 0 : _a.storeKey); | |
}, | |
canAccessUserForm: (context, event) => { | |
var _a; | |
return !!context.hasUserForm && | |
(!!context.storeKey || | |
!!((_a = event.payload) === null || _a === void 0 ? void 0 : _a.storeKey)); | |
}, | |
}, | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment