Skip to content

Instantly share code, notes, and snippets.

@thomasmery
Last active July 8, 2020 06:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save thomasmery/5fc438f43ef69704abe1b0ab32b7c22d to your computer and use it in GitHub Desktop.
Save thomasmery/5fc438f43ef69704abe1b0ab32b7c22d to your computer and use it in GitHub Desktop.
Generated by XState Viz: https://xstate.js.org/viz
// 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