@clicktap/state 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/auth/auth.ts DELETED
@@ -1,408 +0,0 @@
1
- import { createMachine } from 'xstate';
2
- import { assign } from '@xstate/immer';
3
- import type { DoneInvokeEvent } from 'xstate';
4
-
5
- export interface AuthMachineContext {
6
- user: {
7
- email: string;
8
- username: string;
9
- firstName: string;
10
- lastName: string;
11
- birthdate?: Date;
12
- gender?: string /** @todo use enum? */;
13
- createdAt: Date;
14
- updatedAt: Date;
15
- role?: string /** @todo use enum? */;
16
- locked: boolean;
17
- affiliateId: string;
18
- stripeId: string;
19
- } | null;
20
- accessToken: string;
21
- refreshToken: string;
22
- ignoreRefreshToken: boolean;
23
- endpoints: {
24
- login: string;
25
- logout: string;
26
- refresh: string;
27
- ssrRefresh: string;
28
- };
29
- }
30
-
31
- export interface RefreshTokenEvent {
32
- type: 'REFRESH_TOKEN';
33
- accessToken: AuthMachineContext['accessToken'];
34
- }
35
-
36
- export interface LoginEvent {
37
- type: 'LOGIN';
38
- username: string;
39
- password: string;
40
- // grant_type: 'password';
41
- // client_id: string;
42
- // client_secret: string;
43
- // scope: 'default';
44
- }
45
-
46
- export interface LogoutEvent {
47
- type: 'LOGOUT';
48
- }
49
-
50
- export interface AuthenticateSuccessEvent {
51
- type: 'AUTHENTICATE_SUCCESS';
52
- user: AuthMachineContext['user'];
53
- accessToken: AuthMachineContext['accessToken'];
54
- }
55
-
56
- export interface RefreshTokenSuccessEvent {
57
- type: 'REFRESH_TOKEN_SUCCESS';
58
- accessToken: AuthMachineContext['accessToken'];
59
- }
60
-
61
- // type AuthEvents = RefreshTokenEvent | AnotherEvent;
62
- export type AuthMachineEvents =
63
- | RefreshTokenEvent
64
- | LoginEvent
65
- | LogoutEvent
66
- | DoneInvokeEvent<AuthenticateSuccessEvent>
67
- | DoneInvokeEvent<RefreshTokenSuccessEvent>;
68
-
69
- type RefreshTokenResponse = {
70
- accessToken?: string;
71
- };
72
- type AuthenticateResponse = {
73
- accessToken?: string;
74
- message?: string;
75
- success: boolean;
76
- user?: AuthenticateSuccessEvent['user'];
77
- };
78
- async function request<T>(url: string, options: object): Promise<T> {
79
- const response = await fetch(url, options);
80
- if (response.status === 200) {
81
- return response.json() as Promise<T>;
82
- }
83
-
84
- return Promise.resolve({
85
- message: response.statusText,
86
- success: false,
87
- }) as Promise<T>;
88
- }
89
-
90
- export const authMachine =
91
- /** @xstate-layout N4IgpgJg5mDOIC5QEMCuAXAFgOjVsAdugJYDGyJBUAxBAPYFjbEEBudA1k3jj4SeUpQELdoOIMA2gAYAujNmJQABzqxiJBkpAAPRAEZ9Adn3YAbABYATAGYr1gKwWLDoxYA0IAJ6JbATmwADkCXPyszK0CzPyijAF84zx5cDEx+MgoWGjAAJxy6HOxlABsKADMCgFsUrBq0ogyhETY6cSk5BW1VdU0CbT0EfWkncysHYelba0DXTx8EBzNTCxDXPzMbaTCjQISk1OxUAj4G8SpaBiZRTm4Do5OBTKpmsUz2+TkutQ0JPqRdRAAWkMAUiNkCxjMO02+gsNjmvkCVmw+hsRnRjiM0hMexAyXuqXSZ2yeQKRVK6AqOWq+OOhNOT2E1zaBAUnX+3R+Wn+A2R0RsmwhRjMUKsRgcSIRCCR2Fs6MCYSmDn0wVxyRyYDKGtgmCyF0YzBaXDq2A1WrguuezLerI6nw5316-UQRhi2BiQ2kk2VkylUSCfnB9iWwSxYrVBzN2stJPyhRK5SqJqjFqyL1aNrZ9pUjt+zoQFn0Zmw0gs6Ks0n0gZMMSlwOx2ELTj80jMTisYvCEdqxToUBgEAA8hhqAAZQcAcQAkgA5dk5np5nm+KElsyBcWWQzhMZSizSQLYVzbhyB8F+Pz6bs4Xv9yBTghjyeDgCqABV5yBOU7l9KjDYghmQJhgVPwHArKw6yrYswKWf8HE2cYJWvbBbwHB9qB0WB0AobgynQXIAAozC9L0AEpqGSND71tRQHUXblQAGKJkVdZwqwlMIokCKCjGwYUYThf8SLRCwEkSEACDoCA4G0HgvgYv4mKBIs+LBIUoUCQT4W8IFIgsRtFmMMskMvKwUIeRosgUrklIBQZKwA1soWccJXU2PwpVEktjBmdcIhiZwUIJfAGSEGyf2UhBAQM0N1gPJYoVbHYoJCHyTCiLYYnXcMJPVTVo2s+jbPzawAkMSZwmxBUJX0KUxUPOV3LYy8tjEvKDmoocMAipcopsAJTzRNY0VhWEpVcaQSy0sVxjGFs0RQrqH16xj7MMOFGxWRYBXBRYeN0hA3XsINnBCfcLyvcSgA */
92
- createMachine<AuthMachineContext, AuthMachineEvents>(
93
- {
94
- context: {
95
- // id: '',
96
- user: null,
97
- accessToken: '',
98
- refreshToken: '', // will only be on the request from the server side for initial check, does not get passed to the client
99
- /**
100
- * If unauthenticating fails, we need a way to ignore the refresh token on subsequent access token refresh.
101
- * This would be set to true done/error of unauthenticate service, and false on done of authenticate service
102
- */
103
- ignoreRefreshToken: false,
104
- endpoints: {
105
- login: '',
106
- logout: '',
107
- refresh: '',
108
- ssrRefresh: '',
109
- },
110
- },
111
- predictableActionArguments: true,
112
- id: 'auth',
113
- initial: 'refreshing',
114
- states: {
115
- authenticating: {
116
- invoke: {
117
- src: 'authenticate',
118
- onDone: [
119
- {
120
- actions: 'setUserContext',
121
- target: 'loggedIn',
122
- },
123
- ],
124
- onError: [
125
- {
126
- target: 'loggedOut',
127
- },
128
- ],
129
- },
130
- },
131
- unauthenticating: {
132
- invoke: {
133
- src: 'unauthenticate',
134
- onDone: [
135
- {
136
- actions: 'unsetUserContext',
137
- target: 'loggedOut',
138
- },
139
- ],
140
- onError: [
141
- {
142
- actions: ['unsetUserContext', 'setIgnoreRefreshToken'],
143
- target: 'loggedOut',
144
- },
145
- ],
146
- },
147
- },
148
- refreshing: {
149
- invoke: {
150
- src: 'refreshAccessToken',
151
- onDone: [
152
- {
153
- actions: ['unsetRefreshToken', 'setAccessToken'],
154
- target: 'loggedIn',
155
- },
156
- ],
157
- onError: [
158
- {
159
- actions: 'unsetRefreshToken',
160
- target: 'unauthenticating',
161
- },
162
- ],
163
- },
164
- },
165
- loggedOut: {
166
- on: {
167
- LOGIN: {
168
- target: 'authenticating',
169
- },
170
- },
171
- },
172
- loggedIn: {
173
- after: {
174
- '60000': {
175
- cond: () => typeof window !== 'undefined',
176
- target: 'refreshing',
177
- },
178
- },
179
- // entry: ['unsetUserContext'],
180
- on: {
181
- LOGOUT: {
182
- target: 'unauthenticating',
183
- },
184
- },
185
- },
186
- },
187
- },
188
- {
189
- actions: {
190
- // setAccessToken: assign<UserContext, RefreshTokenEvent>((context, event) => {
191
- // context.accessToken = event.accessToken;
192
- // }),
193
- setUserContext: assign(
194
- (
195
- context: AuthMachineContext,
196
- event: DoneInvokeEvent<AuthenticateSuccessEvent>
197
- ) => {
198
- // event.type - done.invoke.auth.authenticating:invocation[0]
199
- if (event.data.type !== 'AUTHENTICATE_SUCCESS') return;
200
-
201
- // context.user = event.data.user;
202
- context.accessToken = event.data.accessToken;
203
- context.ignoreRefreshToken = false;
204
- }
205
- ),
206
- unsetUserContext: assign((context) => {
207
- Object.assign(context, authMachine.initialState.context);
208
- // context = authMachine.initialState.context;
209
- }),
210
- setAccessToken: assign(
211
- (
212
- context: AuthMachineContext,
213
- event: DoneInvokeEvent<RefreshTokenSuccessEvent>
214
- ) => {
215
- if (event.data.type !== 'REFRESH_TOKEN_SUCCESS') return; // is this really necessary?
216
-
217
- context.accessToken = event.data.accessToken;
218
- context.ignoreRefreshToken = false;
219
- }
220
- ),
221
- unsetRefreshToken: assign(
222
- (
223
- context: AuthMachineContext,
224
- event: DoneInvokeEvent<RefreshTokenSuccessEvent>
225
- ) => {
226
- if (event.data.type !== 'REFRESH_TOKEN_SUCCESS') return; // is this really necessary?
227
-
228
- context.refreshToken = '';
229
- }
230
- ),
231
- setIgnoreRefreshToken: assign((context) => {
232
- context.ignoreRefreshToken = true;
233
- }),
234
- },
235
- services: {
236
- refreshAccessToken: (context) => async () => {
237
- // eslint-disable-next-line no-console
238
- // console.log(
239
- // 'refresh access token (%s)',
240
- // typeof window === 'undefined' ? 'server' : 'client'
241
- // );
242
- // eslint-disable-next-line no-console
243
- // console.log('--------------------');
244
-
245
- let response: RefreshTokenResponse;
246
- if (typeof window === 'undefined') {
247
- // server
248
- // eslint-disable-next-line no-console
249
- // console.log('in server refreshAccessToken');
250
- // eslint-disable-next-line no-console
251
- // console.log(context.refreshToken);
252
- if (context.refreshToken === '') throw new Error('Unauthorized.');
253
-
254
- try {
255
- response = await request<RefreshTokenResponse>(
256
- 'http://nginx:5210/auth/refresh',
257
- {
258
- method: 'POST',
259
- mode: 'cors',
260
- credentials: 'include',
261
- headers: {
262
- Cookie: `refresh_token=${context.refreshToken}`,
263
- 'Content-Type': 'application/json',
264
- },
265
- body: JSON.stringify({
266
- grant_type: 'refresh_token',
267
- // client_id: 'default',
268
- // client_secret: 'Password123!',
269
- // scope: 'default',
270
- }).toString(),
271
- }
272
- );
273
-
274
- const data = response;
275
-
276
- if (typeof data.accessToken === 'undefined') {
277
- throw new Error('Unauthorized.');
278
- }
279
-
280
- return {
281
- type: 'REFRESH_TOKEN_SUCCESS',
282
- accessToken: data.accessToken,
283
- } as RefreshTokenSuccessEvent;
284
- } catch (err) {
285
- // eslint-disable-next-line no-console
286
- // console.log(err);
287
- throw new Error('Could not complete refresh request (server)');
288
- }
289
- } else {
290
- // client
291
- response = await request<RefreshTokenResponse>(
292
- 'https://middleware-clicktap.local-rmgmedia.com/auth/refresh',
293
- {
294
- method: 'POST',
295
- mode: 'cors',
296
- credentials: 'include',
297
- headers: {
298
- 'Content-Type': 'application/json',
299
- },
300
- body: JSON.stringify({
301
- grant_type: 'refresh_token',
302
- // client_id: 'default',
303
- // client_secret: 'Password123!',
304
- // scope: 'default',
305
- }).toString(),
306
- }
307
- );
308
-
309
- const data = response;
310
-
311
- if (typeof data.accessToken === 'undefined') {
312
- throw new Error('Unauthorized.');
313
- }
314
-
315
- return {
316
- type: 'REFRESH_TOKEN_SUCCESS',
317
- accessToken: data.accessToken,
318
- };
319
- }
320
- },
321
- authenticate: (context, event) => async () => {
322
- const e = event as LoginEvent; // workaround to typescript not allowing casting in parameter
323
- if (e.type !== 'LOGIN') {
324
- throw new Error(
325
- `Authenticate can only be called for "LOGIN" event type. Event caller: ${String(
326
- e.type
327
- )}`
328
- );
329
- }
330
-
331
- const response = await request<AuthenticateResponse>(
332
- 'https://middleware-clicktap.local-rmgmedia.com/auth/login',
333
- {
334
- method: 'POST',
335
- mode: 'cors',
336
- credentials: 'include',
337
- headers: {
338
- 'Content-Type': 'application/json',
339
- },
340
- body: JSON.stringify({
341
- username: e.username,
342
- password: e.password,
343
- // grant_type: 'password',
344
- // client_id: 'default',
345
- // client_secret: 'Password123!',
346
- // scope: 'default',
347
- }).toString(),
348
- }
349
- );
350
- /** @todo do we need to handle different status codes here...like what if the service is down? */
351
- const data = response;
352
-
353
- if (typeof data.accessToken === 'undefined') {
354
- throw new Error(
355
- data.message?.toString() ?? 'Sign in failed. Please try again.'
356
- );
357
- }
358
-
359
- return {
360
- type: 'AUTHENTICATE_SUCCESS',
361
- // user: data.user,
362
- accessToken: data.accessToken,
363
- } as AuthenticateSuccessEvent;
364
- // /** @todo how to call immer assign from inside invoke here to update context? */
365
-
366
- // console.log(data);
367
- // console.log(event);
368
- },
369
- unauthenticate: (context) => async () => {
370
- // const response = await fetch(
371
- await fetch(
372
- 'https://middleware-clicktap.local-rmgmedia.com/auth/logout',
373
- {
374
- method: 'POST',
375
- mode: 'cors',
376
- credentials: 'include',
377
- // headers: {
378
- // 'Access-Control-Allow-Origin': 'https://middleware-clicktap.local-rmgmedia.com',
379
- // 'Access-Control-Allow-Credentials': 'true',
380
- // },
381
- }
382
- );
383
- /** @todo do we need to handle different status codes here...like what if the service is down? */
384
- // const data = await response.json();
385
-
386
- return { type: 'UNAUTHENTICATE' };
387
-
388
- // if (!data.success) {
389
- // throw new Error(data.message ?? 'Sign in failed. Please try again.');
390
- // }
391
-
392
- // return {
393
- // type: 'AUTHENTICATE_SUCCESS',
394
- // user: data.user,
395
- // accessToken: data.accessToken,
396
- // };
397
- },
398
- },
399
- // doesn't make sense in the case where you have an access token that is about to expire
400
- // and the loggedIn state won't fire another refresh for 15 min
401
- //
402
- // guards: {
403
- // checkIfLoggedIn: (context, _event) => {
404
- // if (context.user) return true;
405
- // },
406
- // },
407
- }
408
- );
@@ -1,44 +0,0 @@
1
- import { createContext, useContext } from 'react';
2
- import type { InterpreterFrom } from 'xstate';
3
- import type { ReactNode } from 'react';
4
- import { toastMachine } from './toast';
5
-
6
- export const ToastContext = createContext(
7
- {} as InterpreterFrom<typeof toastMachine>
8
- );
9
-
10
- export const useToast = () => {
11
- return useContext(ToastContext);
12
- };
13
-
14
- type Props = {
15
- children: ReactNode;
16
- // options: {
17
- // devTools: boolean;
18
- // };
19
- // state: StateConfig<ToastMachineContext, ToastMachineEvents>;
20
- // setState: Dispatch<
21
- // SetStateAction<StateConfig<ToastMachineContext, ToastMachineEvents>>
22
- // >;
23
- service: InterpreterFrom<typeof toastMachine>;
24
- };
25
-
26
- export function ToastProvider({
27
- children,
28
- service,
29
- }: // options = { devTools: false },
30
- // state,
31
- // setState,
32
- Props) {
33
- // const toastService = interpret(toastMachine, {
34
- // devTools: options.devTools,
35
- // }).start(state);
36
-
37
- // toastService.subscribe((s) => {
38
- // setState(s);
39
- // });
40
-
41
- return (
42
- <ToastContext.Provider value={service}>{children}</ToastContext.Provider>
43
- );
44
- }
@@ -1,109 +0,0 @@
1
- import { createMachine } from 'xstate';
2
- import { assign } from '@xstate/immer';
3
-
4
- export interface TimerContext {
5
- // The elapsed time (in ms)
6
- elapsed: number;
7
- // The maximum time (in ms)
8
- duration: number;
9
- // The interval to send TICK events (in ms)
10
- interval: number;
11
- // The interval timer
12
- intervalId: NodeJS.Timer | null;
13
- }
14
-
15
- export type TimerEvents =
16
- | {
17
- // User intent to start the timer
18
- type: 'START_TIMER';
19
- }
20
- | {
21
- // User intent to resume the timer
22
- type: 'RESUME_TIMER';
23
- }
24
- | {
25
- // User intent to pause the timer
26
- type: 'PAUSE_TIMER';
27
- }
28
- | {
29
- // The TICK event sent by the spawned interval service
30
- type: 'TICK';
31
- }
32
- | {
33
- // User intent to update the duration
34
- type: 'DURATION.UPDATE';
35
- value: number;
36
- }
37
- | {
38
- // User intent to reset the elapsed time to 0
39
- type: 'RESET_TIMER';
40
- };
41
-
42
- export const timerMachine =
43
- /** @xstate-layout N4IgpgJg5mDOIC5QBcCWBbMAnAdABwEMBXWSAYgCUBRAZQFUBZKgfQBUBJJigbQAYBdRKDwB7WKjQiAdkJAAPRAFoAjADYATDmUBOXQA5tAZmV69AFgDsq1QBoQAT0SHehnAFYAvh7tpMuLERSUqhSUGQcAMIA0nyCSCCi4pIy8QoIJqo4qnoWbnaOCGZmmaq8Zbyqyi5uym6qXj4Y2DgBQSFhAAoAgnQ0LBxcsbKJEqjSsmlVbjimFmZ6agZmbsbq+U4u7g0gvs2twaFkQ-EjyRNObrxZOXkOiGbKFjjq5WbqFurFvNpmXt4gUhEEDgsl2WGGYlG41SSnUVS0um0BmMpks1nW6Tcmk8-zB+GIpAgEKSYxSoDSvAxn142zx+3axKhZPkiEpdwQhjmW1xTVwAGMROg8AAbMDISCMs4whBsgpuOp-DxAA */
44
- createMachine<TimerContext, TimerEvents>({
45
- id: 'timer',
46
- initial: 'running',
47
- context: {
48
- elapsed: 0,
49
- duration: 3000,
50
- interval: 100,
51
- intervalId: null,
52
- },
53
- states: {
54
- paused: {
55
- on: {
56
- // START_TIMER: {
57
- // target: 'running',
58
- // cond: (context) => context.elapsed < context.duration,
59
- // },
60
- RESUME_TIMER: {
61
- target: 'running',
62
- cond: (context) => context.elapsed < context.duration,
63
- },
64
- },
65
- },
66
- running: {
67
- invoke: {
68
- src: (context) => (send) => {
69
- // eslint-disable-next-line no-console
70
- // console.log('context.interval: ', context.interval);
71
- const interval = setInterval(() => {
72
- send('TICK');
73
- }, context.interval);
74
- return () => {
75
- clearInterval(interval);
76
- };
77
- },
78
- },
79
- always: [
80
- {
81
- target: 'completed',
82
- cond: (context) => {
83
- return context.elapsed >= context.duration;
84
- },
85
- },
86
- ],
87
- on: {
88
- TICK: {
89
- actions: assign((context, event) => {
90
- if (event.type !== 'TICK') return;
91
- context.elapsed += context.interval;
92
- // eslint-disable-next-line no-console
93
- // console.log(
94
- // 'elapsed: %d | interval: %d',
95
- // context.elapsed,
96
- // context.interval
97
- // );
98
- }),
99
- },
100
- PAUSE_TIMER: { target: 'paused' },
101
- },
102
- },
103
- completed: {
104
- type: 'final',
105
- },
106
- },
107
- });
108
-
109
- export default timerMachine;
@@ -1,7 +0,0 @@
1
- import { toastMachine } from './toast';
2
-
3
- describe('state', () => {
4
- it('should work', () => {
5
- expect(toastMachine).toEqual('toast');
6
- });
7
- });