@indietabletop/appkit 7.0.0-0 → 7.0.0-rc.0

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.
Files changed (279) hide show
  1. package/lib/AppConfig/AppConfig.tsx +80 -0
  2. package/lib/AppConfig/formatters.tsx +43 -0
  3. package/lib/AuthCard/AuthCard.stories.ts +34 -0
  4. package/lib/AuthCard/AuthCard.tsx +64 -0
  5. package/lib/AuthCard/style.css.ts +49 -0
  6. package/lib/CacheProvider.tsx +20 -0
  7. package/lib/DialogTrigger/index.tsx +36 -0
  8. package/lib/DocumentTitle/DocumentTitle.tsx +10 -0
  9. package/lib/EnumMapper.ts +50 -0
  10. package/lib/ExternalLink.tsx +10 -0
  11. package/lib/FullscreenDismissBlocker.tsx +23 -0
  12. package/{dist/HistoryState.d.ts → lib/HistoryState.ts} +5 -2
  13. package/lib/IndieTabletopClubLogo.tsx +44 -0
  14. package/lib/IndieTabletopClubSymbol.tsx +37 -0
  15. package/lib/InfoPage/index.tsx +46 -0
  16. package/lib/InfoPage/pages.tsx +36 -0
  17. package/lib/InfoPage/style.css.ts +36 -0
  18. package/lib/Letterhead/index.tsx +85 -0
  19. package/lib/Letterhead/stories.tsx +41 -0
  20. package/lib/Letterhead/style.css.ts +152 -0
  21. package/lib/LetterheadForm/LetterheadReadonlyTextField.stories.tsx +17 -0
  22. package/lib/LetterheadForm/LetterheadSubmitError.stories.tsx +19 -0
  23. package/lib/LetterheadForm/LetterheadTextField.stories.tsx +19 -0
  24. package/lib/LetterheadForm/index.tsx +137 -0
  25. package/lib/LetterheadForm/style.css.ts +89 -0
  26. package/lib/LoadingIndicator.tsx +40 -0
  27. package/lib/MiddotSeparated/MiddotSeparated.stories.ts +26 -0
  28. package/lib/MiddotSeparated/MiddotSeparated.tsx +26 -0
  29. package/lib/MiddotSeparated/style.css.ts +10 -0
  30. package/lib/ModalDialog/index.tsx +28 -0
  31. package/lib/ModalDialog/style.css.ts +88 -0
  32. package/lib/ModernIDB/Cursor.ts +91 -0
  33. package/lib/ModernIDB/ModernIDB.ts +337 -0
  34. package/lib/ModernIDB/ModernIDBError.ts +9 -0
  35. package/lib/ModernIDB/ObjectStore.ts +195 -0
  36. package/lib/ModernIDB/ObjectStoreIndex.ts +102 -0
  37. package/lib/ModernIDB/README.md +9 -0
  38. package/lib/ModernIDB/Transaction.ts +40 -0
  39. package/lib/ModernIDB/VersionChangeManager.ts +57 -0
  40. package/lib/ModernIDB/bindings/factory.tsx +165 -0
  41. package/lib/ModernIDB/bindings/index.ts +2 -0
  42. package/{dist/ModernIDB/bindings/types.d.ts → lib/ModernIDB/bindings/types.ts} +32 -13
  43. package/lib/ModernIDB/bindings/utils.tsx +32 -0
  44. package/lib/ModernIDB/index.ts +10 -0
  45. package/lib/ModernIDB/types.ts +120 -0
  46. package/lib/ModernIDB/utils.ts +51 -0
  47. package/lib/QRCode/QRCode.stories.tsx +41 -0
  48. package/lib/QRCode/QRCode.tsx +54 -0
  49. package/lib/QRCode/style.css.ts +23 -0
  50. package/lib/ReleaseInfo/index.tsx +29 -0
  51. package/lib/RulesetResolver.ts +214 -0
  52. package/lib/SafariCheck/SafariCheck.stories.tsx +99 -0
  53. package/lib/SafariCheck/SafariCheck.tsx +273 -0
  54. package/lib/SafariCheck/addToDock.svg +13 -0
  55. package/lib/SafariCheck/addToHomeScreen.svg +12 -0
  56. package/lib/SafariCheck/safari.svg +32 -0
  57. package/lib/SafariCheck/shareIcon.svg +11 -0
  58. package/lib/SafariCheck/style.css.ts +106 -0
  59. package/lib/ServiceWorkerHandler.tsx +53 -0
  60. package/lib/ShareButton/ShareButton.stories.tsx +58 -0
  61. package/lib/ShareButton/ShareButton.tsx +153 -0
  62. package/lib/ShareButton/test.css.ts +3 -0
  63. package/lib/SubscribeCard/LetterheadInfoCard.tsx +23 -0
  64. package/lib/SubscribeCard/SubscribeByEmailCard.stories.tsx +69 -0
  65. package/lib/SubscribeCard/SubscribeByEmailCard.tsx +183 -0
  66. package/lib/SubscribeCard/SubscribeByPledgeCard.stories.tsx +133 -0
  67. package/lib/SubscribeCard/SubscribeByPledgeCard.tsx +127 -0
  68. package/lib/SubscribeCard/style.css.ts +14 -0
  69. package/lib/Sync/SyncIcon.stories.tsx +67 -0
  70. package/lib/Sync/SyncIcon.tsx +102 -0
  71. package/lib/Sync/SyncLog.tsx +222 -0
  72. package/lib/Sync/SyncLogList.stories.tsx +219 -0
  73. package/lib/Sync/style.css.ts +126 -0
  74. package/lib/account/AccountIcon.tsx +15 -0
  75. package/lib/account/AccountIssueView.tsx +44 -0
  76. package/lib/account/AlreadyLoggedInView.tsx +47 -0
  77. package/lib/account/CurrentUserFetcher.stories.tsx +292 -0
  78. package/lib/account/CurrentUserFetcher.tsx +118 -0
  79. package/lib/account/FailureFallbackView.tsx +36 -0
  80. package/lib/account/JoinCard.stories.tsx +257 -0
  81. package/lib/account/JoinCard.tsx +301 -0
  82. package/lib/account/LoadingView.tsx +14 -0
  83. package/lib/account/LoginCard.stories.tsx +288 -0
  84. package/lib/account/LoginCard.tsx +100 -0
  85. package/lib/account/LoginView.tsx +151 -0
  86. package/lib/account/NoConnectionView.tsx +34 -0
  87. package/lib/account/PasswordResetCard.stories.tsx +242 -0
  88. package/lib/account/PasswordResetCard.tsx +296 -0
  89. package/lib/account/UserMismatchView.tsx +62 -0
  90. package/lib/account/VerifyPage.tsx +195 -0
  91. package/lib/account/iconMask.svg +3 -0
  92. package/lib/account/style.css.ts +81 -0
  93. package/{dist/account/types.d.ts → lib/account/types.ts} +6 -3
  94. package/lib/account/useFetchCurrentUser.tsx +63 -0
  95. package/lib/account/useRedirectPath.ts +21 -0
  96. package/lib/animations.css.ts +17 -0
  97. package/lib/append-copy-to-text.ts +35 -0
  98. package/lib/async-op.ts +286 -0
  99. package/lib/atomic.css.ts +11 -0
  100. package/{dist/caught-value.d.ts → lib/caught-value.ts} +11 -1
  101. package/{dist/class-names.d.ts → lib/class-names.ts} +17 -6
  102. package/lib/client.ts +662 -0
  103. package/lib/common.css.ts +48 -0
  104. package/lib/copyrightRange.ts +10 -0
  105. package/lib/createSafeStorage.ts +91 -0
  106. package/lib/createStrictContext.ts +15 -0
  107. package/lib/failureMessages.ts +108 -0
  108. package/lib/form/FormSubmitButton.tsx +58 -0
  109. package/lib/form/SubmitErrorAlert.tsx +21 -0
  110. package/lib/form/style.css.ts +9 -0
  111. package/lib/globals.css.ts +62 -0
  112. package/lib/groupBy.ts +25 -0
  113. package/lib/hrefs.ts +48 -0
  114. package/lib/idToDate.ts +8 -0
  115. package/lib/ids.ts +6 -0
  116. package/lib/index.ts +75 -0
  117. package/lib/internal.css.ts +10 -0
  118. package/lib/mailto.ts +40 -0
  119. package/lib/media.ts +50 -0
  120. package/lib/random.ts +19 -0
  121. package/lib/result/swr.ts +18 -0
  122. package/{dist/sleep.d.ts → lib/sleep.ts} +3 -1
  123. package/lib/store/index.tsx +294 -0
  124. package/lib/store/store.ts +482 -0
  125. package/lib/store/types.ts +45 -0
  126. package/lib/store/utils.ts +54 -0
  127. package/lib/storybook/decorators.tsx +10 -0
  128. package/lib/structs.ts +3 -0
  129. package/{dist/typeguards.d.ts → lib/typeguards.ts} +3 -1
  130. package/{dist/types.d.ts → lib/types.ts} +23 -12
  131. package/lib/unique.ts +24 -0
  132. package/lib/use-async-op.ts +16 -0
  133. package/lib/use-document-background-color.ts +16 -0
  134. package/lib/use-form.ts +78 -0
  135. package/lib/use-is-installed.ts +17 -0
  136. package/lib/use-media-query.ts +21 -0
  137. package/lib/use-reverting-state.ts +32 -0
  138. package/lib/use-scroll-restoration.ts +99 -0
  139. package/lib/useEnsureValue.ts +31 -0
  140. package/lib/useInvokeClient.ts +54 -0
  141. package/lib/useIsVisible.ts +27 -0
  142. package/lib/utm.ts +92 -0
  143. package/lib/validations.ts +25 -0
  144. package/lib/vars.css.ts +13 -0
  145. package/package.json +23 -29
  146. package/dist/AppConfig/AppConfig.d.ts +0 -29
  147. package/dist/AuthCard/AuthCard.d.ts +0 -10
  148. package/dist/AuthCard/AuthCard.stories.d.ts +0 -34
  149. package/dist/AuthCard/style.css.d.ts +0 -23
  150. package/dist/DialogTrigger/index.d.ts +0 -13
  151. package/dist/DocumentTitle/DocumentTitle.d.ts +0 -3
  152. package/dist/EnumMapper.d.ts +0 -25
  153. package/dist/ExternalLink.d.ts +0 -3
  154. package/dist/FullscreenDismissBlocker.d.ts +0 -5
  155. package/dist/IndieTabletopClubLogo.d.ts +0 -7
  156. package/dist/IndieTabletopClubSymbol.d.ts +0 -7
  157. package/dist/InfoPage/index.d.ts +0 -8
  158. package/dist/InfoPage/pages.d.ts +0 -2
  159. package/dist/InfoPage/style.css.d.ts +0 -5
  160. package/dist/Letterhead/index.d.ts +0 -19
  161. package/dist/Letterhead/stories.d.ts +0 -13
  162. package/dist/Letterhead/style.css.d.ts +0 -46
  163. package/dist/LetterheadForm/LetterheadReadonlyTextField.stories.d.ts +0 -17
  164. package/dist/LetterheadForm/LetterheadSubmitError.stories.d.ts +0 -11
  165. package/dist/LetterheadForm/LetterheadTextField.stories.d.ts +0 -336
  166. package/dist/LetterheadForm/index.d.ts +0 -44
  167. package/dist/LetterheadForm/style.css.d.ts +0 -8
  168. package/dist/LoadingIndicator.d.ts +0 -3
  169. package/dist/MiddotSeparated/MiddotSeparated.d.ts +0 -8
  170. package/dist/MiddotSeparated/MiddotSeparated.stories.d.ts +0 -586
  171. package/dist/MiddotSeparated/style.css.d.ts +0 -1
  172. package/dist/ModalDialog/index.d.ts +0 -12
  173. package/dist/ModalDialog/style.css.d.ts +0 -58
  174. package/dist/ModernIDB/Cursor.d.ts +0 -56
  175. package/dist/ModernIDB/ModernIDB.d.ts +0 -66
  176. package/dist/ModernIDB/ModernIDBError.d.ts +0 -3
  177. package/dist/ModernIDB/ObjectStore.d.ts +0 -112
  178. package/dist/ModernIDB/ObjectStoreIndex.d.ts +0 -53
  179. package/dist/ModernIDB/Transaction.d.ts +0 -16
  180. package/dist/ModernIDB/VersionChangeManager.d.ts +0 -30
  181. package/dist/ModernIDB/bindings/factory.d.ts +0 -12
  182. package/dist/ModernIDB/bindings/index.d.ts +0 -2
  183. package/dist/ModernIDB/bindings/utils.d.ts +0 -2
  184. package/dist/ModernIDB/index.d.ts +0 -10
  185. package/dist/ModernIDB/types.d.ts +0 -88
  186. package/dist/ModernIDB/utils.d.ts +0 -4
  187. package/dist/QRCode/QRCode.d.ts +0 -7
  188. package/dist/QRCode/QRCode.stories.d.ts +0 -33
  189. package/dist/QRCode/style.css.d.ts +0 -4
  190. package/dist/ReleaseInfo/index.d.ts +0 -5
  191. package/dist/RulesetResolver.d.ts +0 -87
  192. package/dist/SafariCheck/SafariCheck.d.ts +0 -23
  193. package/dist/SafariCheck/SafariCheck.stories.d.ts +0 -73
  194. package/dist/SafariCheck/style.css.d.ts +0 -17
  195. package/dist/ServiceWorkerHandler.d.ts +0 -11
  196. package/dist/ShareButton/ShareButton.d.ts +0 -57
  197. package/dist/ShareButton/ShareButton.stories.d.ts +0 -1577
  198. package/dist/ShareButton/test.css.d.ts +0 -1
  199. package/dist/SubscribeCard/LetterheadInfoCard.d.ts +0 -2
  200. package/dist/SubscribeCard/SubscribeByEmailCard.d.ts +0 -24
  201. package/dist/SubscribeCard/SubscribeByEmailCard.stories.d.ts +0 -10
  202. package/dist/SubscribeCard/SubscribeByPledgeCard.d.ts +0 -36
  203. package/dist/SubscribeCard/SubscribeByPledgeCard.stories.d.ts +0 -65
  204. package/dist/SubscribeCard/style.css.d.ts +0 -4
  205. package/dist/account/AccountIssueView.d.ts +0 -3
  206. package/dist/account/AlreadyLoggedInView.d.ts +0 -5
  207. package/dist/account/CurrentUserFetcher.d.ts +0 -20
  208. package/dist/account/CurrentUserFetcher.stories.d.ts +0 -136
  209. package/dist/account/FailureFallbackView.d.ts +0 -1
  210. package/dist/account/JoinCard.d.ts +0 -14
  211. package/dist/account/JoinCard.stories.d.ts +0 -143
  212. package/dist/account/LoadingView.d.ts +0 -1
  213. package/dist/account/LoginCard.d.ts +0 -39
  214. package/dist/account/LoginCard.stories.d.ts +0 -217
  215. package/dist/account/LoginView.d.ts +0 -10
  216. package/dist/account/NoConnectionView.d.ts +0 -4
  217. package/dist/account/PasswordResetCard.d.ts +0 -15
  218. package/dist/account/PasswordResetCard.stories.d.ts +0 -128
  219. package/dist/account/UserMismatchView.d.ts +0 -6
  220. package/dist/account/VerifyPage.d.ts +0 -13
  221. package/dist/account/style.css.d.ts +0 -10
  222. package/dist/account/useFetchCurrentUser.d.ts +0 -28
  223. package/dist/account/useRedirectPath.d.ts +0 -6
  224. package/dist/animations.css.d.ts +0 -3
  225. package/dist/append-copy-to-text.d.ts +0 -10
  226. package/dist/append-copy-to-text.test.d.ts +0 -1
  227. package/dist/appkit.css +0 -1
  228. package/dist/appkit.js +0 -10692
  229. package/dist/async-op.d.ts +0 -101
  230. package/dist/atomic.css.d.ts +0 -6
  231. package/dist/client.d.ts +0 -424
  232. package/dist/common.css.d.ts +0 -5
  233. package/dist/copyrightRange.d.ts +0 -1
  234. package/dist/copyrightRange.test.d.ts +0 -1
  235. package/dist/createSafeStorage.d.ts +0 -34
  236. package/dist/failureMessages.d.ts +0 -20
  237. package/dist/failureMessages.test.d.ts +0 -1
  238. package/dist/form/FormSubmitButton.d.ts +0 -17
  239. package/dist/form/SubmitErrorAlert.d.ts +0 -5
  240. package/dist/form/style.css.d.ts +0 -3
  241. package/dist/globals.css.d.ts +0 -0
  242. package/dist/groupBy.d.ts +0 -1
  243. package/dist/groupBy.test.d.ts +0 -1
  244. package/dist/hrefs.d.ts +0 -32
  245. package/dist/hrefs.test.d.ts +0 -1
  246. package/dist/idToDate.d.ts +0 -5
  247. package/dist/idToDate.test.d.ts +0 -1
  248. package/dist/ids.d.ts +0 -1
  249. package/dist/ids.test.d.ts +0 -1
  250. package/dist/index.d.ts +0 -64
  251. package/dist/internal.css.d.ts +0 -2
  252. package/dist/mailto.d.ts +0 -8
  253. package/dist/mailto.test.d.ts +0 -1
  254. package/dist/media.d.ts +0 -39
  255. package/dist/random.d.ts +0 -3
  256. package/dist/result/swr.d.ts +0 -4
  257. package/dist/store/index.d.ts +0 -237
  258. package/dist/store/store.d.ts +0 -144
  259. package/dist/store/types.d.ts +0 -49
  260. package/dist/store/utils.d.ts +0 -10
  261. package/dist/storybook/decorators.d.ts +0 -3
  262. package/dist/structs.d.ts +0 -1
  263. package/dist/typeguards.test.d.ts +0 -1
  264. package/dist/unique.d.ts +0 -10
  265. package/dist/unique.test.d.ts +0 -1
  266. package/dist/use-async-op.d.ts +0 -6
  267. package/dist/use-document-background-color.d.ts +0 -4
  268. package/dist/use-form.d.ts +0 -29
  269. package/dist/use-is-installed.d.ts +0 -8
  270. package/dist/use-media-query.d.ts +0 -1
  271. package/dist/use-reverting-state.d.ts +0 -5
  272. package/dist/use-scroll-restoration.d.ts +0 -25
  273. package/dist/useEnsureValue.d.ts +0 -6
  274. package/dist/useInvokeClient.d.ts +0 -25
  275. package/dist/useIsVisible.d.ts +0 -4
  276. package/dist/utm.d.ts +0 -58
  277. package/dist/utm.test.d.ts +0 -1
  278. package/dist/validations.d.ts +0 -3
  279. package/dist/vars.css.d.ts +0 -10
@@ -0,0 +1,482 @@
1
+ import { number } from "superstruct";
2
+ import {
3
+ and,
4
+ assertEvent,
5
+ assign,
6
+ fromCallback,
7
+ fromPromise,
8
+ setup,
9
+ } from "xstate";
10
+ import {
11
+ createSafeStorage,
12
+ type SafeStorageKey,
13
+ } from "../createSafeStorage.ts";
14
+ import { currentUser, sessionInfo } from "../structs.ts";
15
+ import type { CurrentUser, SessionInfo } from "../types.ts";
16
+ import type {
17
+ MachineContext,
18
+ MachineEvent,
19
+ PullResult,
20
+ PushResult,
21
+ SyncedItem,
22
+ } from "./types.ts";
23
+ import { assertNonNullish, caughtToResult } from "./utils.ts";
24
+
25
+ const safeStorage = createSafeStorage({
26
+ currentUser: currentUser(),
27
+ sessionInfo: sessionInfo(),
28
+ lastSuccessfulSyncTs: number(),
29
+ });
30
+
31
+ function createInMemoryContext(): Omit<
32
+ MachineContext,
33
+ SafeStorageKey<typeof safeStorage>
34
+ > {
35
+ return {
36
+ currentSync: null,
37
+ pushOnceIdle: false,
38
+ syncLog: [],
39
+ };
40
+ }
41
+
42
+ function createInitialContext(): MachineContext {
43
+ return {
44
+ ...createInMemoryContext(),
45
+ currentUser: safeStorage.getItem("currentUser"),
46
+ sessionInfo: safeStorage.getItem("sessionInfo"),
47
+ lastSuccessfulSyncTs: safeStorage.getItem("lastSuccessfulSyncTs"),
48
+ };
49
+ }
50
+
51
+ const sync = fromCallback<MachineEvent>(() => {
52
+ throw new Error(`Sync actor not provided.`);
53
+ });
54
+
55
+ const auth = fromCallback<MachineEvent>(() => {
56
+ throw new Error(`Auth actor not provided.`);
57
+ });
58
+
59
+ export type PullChangesInput = {
60
+ sinceTs: number | null;
61
+ currentUser: CurrentUser;
62
+ };
63
+
64
+ const pullChanges = fromPromise<PullResult, PullChangesInput>(() => {
65
+ throw new Error(`Pull changes actor not provided.`);
66
+ });
67
+
68
+ export type PushChangesInput = {
69
+ sinceTs: number | null;
70
+ currentSyncTs: number;
71
+ ignoredItems: SyncedItem[];
72
+ currentUser: CurrentUser;
73
+ };
74
+
75
+ const pushChanges = fromPromise<PushResult, PushChangesInput>(async () => {
76
+ throw new Error(`Push changes actor not provided.`);
77
+ });
78
+
79
+ const config = setup({
80
+ types: {
81
+ context: {} as MachineContext,
82
+ events: {} as MachineEvent,
83
+ children: {} as {
84
+ sync: "sync";
85
+ auth: "auth";
86
+ },
87
+ },
88
+
89
+ actors: {
90
+ auth,
91
+ sync,
92
+ pullChanges,
93
+ pushChanges,
94
+ },
95
+
96
+ actions: {
97
+ setCurrentSync: assign({
98
+ currentSync: () => ({ startedTs: Date.now(), pull: null, push: null }),
99
+ }),
100
+
101
+ flushCurrentSync: assign(({ context }) => {
102
+ const { currentSync, syncLog } = context;
103
+
104
+ assertNonNullish(
105
+ currentSync,
106
+ "Flushing current sync but context.currentSync is nullish.",
107
+ );
108
+
109
+ return {
110
+ currentSync: null,
111
+ syncLog: [currentSync, ...syncLog],
112
+ };
113
+ }),
114
+
115
+ setLastSuccessfulSync: assign(({ context }) => {
116
+ assertNonNullish(
117
+ context.currentSync,
118
+ "Setting last successful sync but context.currentSync is nullish.",
119
+ );
120
+
121
+ const lastSuccessfulSyncTs = context.currentSync.startedTs;
122
+ safeStorage.setItem("lastSuccessfulSyncTs", lastSuccessfulSyncTs);
123
+ return { lastSuccessfulSyncTs };
124
+ }),
125
+
126
+ markSyncStepResult: assign({
127
+ currentSync: (
128
+ { context },
129
+ params: { pull?: PullResult; push?: PushResult },
130
+ ) => {
131
+ assertNonNullish(
132
+ context.currentSync,
133
+ "Marking sync step result but context.currentSync is nullish.",
134
+ );
135
+
136
+ return {
137
+ ...context.currentSync,
138
+ ...params,
139
+ };
140
+ },
141
+ }),
142
+
143
+ queuePush: assign({ pushOnceIdle: true }),
144
+
145
+ clearPushQueue: assign({ pushOnceIdle: false }),
146
+
147
+ setCurrentUser: assign((_, currentUser: CurrentUser) => {
148
+ safeStorage.setItem("currentUser", currentUser);
149
+ return { currentUser };
150
+ }),
151
+
152
+ patchSessionInfo: assign(({ context }, newSessionInfo: SessionInfo) => {
153
+ const sessionInfo =
154
+ context.sessionInfo ?
155
+ { ...context.sessionInfo, expiresTs: newSessionInfo.expiresTs }
156
+ : newSessionInfo;
157
+
158
+ safeStorage.setItem("sessionInfo", sessionInfo);
159
+ return { sessionInfo };
160
+ }),
161
+
162
+ resetContext: assign((): MachineContext => {
163
+ safeStorage.clear();
164
+
165
+ return {
166
+ ...createInMemoryContext(),
167
+ currentUser: null,
168
+ sessionInfo: null,
169
+ lastSuccessfulSyncTs: null,
170
+ };
171
+ }),
172
+
173
+ resetWithServerUser: assign((_, serverUser: CurrentUser) => {
174
+ safeStorage.clear();
175
+ safeStorage.setItem("currentUser", serverUser);
176
+
177
+ return {
178
+ ...createInMemoryContext(),
179
+ currentUser: serverUser,
180
+ sessionInfo: null,
181
+ lastSuccessfulSyncTs: null,
182
+ };
183
+ }),
184
+ },
185
+
186
+ guards: {
187
+ shouldPushOnceIdle: ({ context }) => {
188
+ return context.pushOnceIdle;
189
+ },
190
+
191
+ isEligibleForSync: and(["isUserVerified", "isNotMismatched"]),
192
+
193
+ isUserVerified: ({ event }) => {
194
+ assertEvent(event, "currentUser");
195
+
196
+ return event.currentUser.isVerified;
197
+ },
198
+
199
+ isNotMismatched: ({ event, context }) => {
200
+ assertEvent(event, "currentUser");
201
+
202
+ // If there is no current user, there cannot be a mismatch
203
+ if (!context.currentUser) {
204
+ return true;
205
+ }
206
+
207
+ // Otherwise check that user IDs match.
208
+ return context.currentUser.id === event.currentUser.id;
209
+ },
210
+
211
+ hasBecomeEligibleForSync: and(["isNotMismatched", "hasBecomeVerified"]),
212
+
213
+ hasBecomeVerified: ({ event, context }) => {
214
+ assertEvent(event, "currentUser");
215
+
216
+ if (!context.currentUser) {
217
+ return false;
218
+ }
219
+
220
+ // User was unverified previously, but now they are verified.
221
+ return (
222
+ context.currentUser.isVerified === false &&
223
+ event.currentUser.isVerified === true
224
+ );
225
+ },
226
+ },
227
+ });
228
+
229
+ export type AppMachine = typeof machine;
230
+
231
+ export const machine = config.createMachine({
232
+ /** @xstate-layout N4IgpgJg5mDOIC5QEMAOqDEAnOYAuA2gAwC6ioqA9rAJZ42UB25IAHogLQBMRAbAHQBOQbwCMggOy8ALES7DRAGhABPTvIn8AzAA5B0rdN4BWCRKKDRvAL7XlaVPwCujZE7wALMI3oBjZHiQGL5OWDg+AKqwYFjEZEggVLT0TCzsCEZc-FyiXHqiOtJcvLwSymoI3Fqi-LkiIjkSIrIStvbozq7uXj40-oEQwaHheFExBKLxFNR0DMwJ6dJL-GJ1gkTSwubS5Yj6mjrG602F0ietdiAOnW6e3n4BQSFh92OxXFOJMynzoIs6uwQljaVw6LluPQeAww0SwADcYm84iwkrNUgtEHkiICOKI8SDrhD7n1HoNniMkaQUd85mlELkctktLxDkQdDoJFYuGVVPTREZaoZjMKdNzRFILu1HETev0nsNXrCJp9UT86QhRVkiHVjDIiHwSlxAXiBaJDEVqkRzNVpASOjKofKXpElR9qclaRiENjed67dLusS5YNorBaEwAJKMABmlGRCVVnr+9JKAkE3LyPF4zN0Sl9JoEZukwuMos5Ev9-AdJOhofDjAAoqxUDQcBB49MPejkwhDDUtERqjwDBIuMYszitOnsmaOdJcqW5zZLoTA7LSfxYCpGL5N9vfDRGFAMBAmGBN3hHlX7WvHRA9zuHwejx2vl3fmxMcVpPxjOLNgOeISAYgLcsY-DmJIxjVJYg6iBslbVsGT5Poex6oE4sAeK+ibdp+CDwdqQjSHogimPopiGICxaaAU6bchseS6IIiG3jWkAoVuO5ofwGEADZ8WhJ5nvwh5wpQADW56rnc64DJx+48fxglHggYmUP0cxxDhNJ4ekuQbPwhz6AYA6FCUWjGgIHLCLopj8tqJmsbJd4KdxR68U4AlCTEWCUFgvF8QEsZYAAtteAYuex95cbusVKV5KlQGpjDiZpTDaVSCa6R++n8j+YjmKYo7ATovDGgY2SbCY2YkUczmQtFbnPlAnlYUJp6MOe6lSRFVZschsWoR5GHtap6npYwmUqjl6pWBIOhGdq1r6pyYqAgOtR8MY2q6HkYiag1QYbkN8UjZhHg+WE-mBcF-nhTJjWDfuw2taNl3jalGkBFppA6e+c26tZpksiRFjzgCvpcFwP7QUU46CDoQH8sYR1yRxQ00BAfFgBg73-WiuX0usAh-sBghaOYZWFICUg-nR0HajDejAWjrmY9juPKQTapejkxRLbk7KDoawqAtmgi-tDxhLNBZljmzTUczjGA80m+EFGI2haDrZhyKKxZGr6SNaNrOvjkU2aiqIivPY+ADuyCzEeeMXWren0lIWRkwBFhIzyFQslkdE8DLXBTtmkqgpFT0nS9jvO8erCwJegRVtGgRYAAFKIO1EAAlBgj3HfJQ0J-QL5ZZ2hNzUUWTcrwMMSNBZEmYCpaaI3lN8PqPDAbaIKMJQEBwCwDjujXXrcDLQgiOIdNyAoOKrLUBjjloPDHFOlbggNpIT7zPa4sBEH6gObImBy+qQxUHBGDUueitmMhmufqMrjeUXBgf6vpAbTI6CIKYAC9RLK+jvlIFYa9zakUcu-KU-Uv4bkPGAQSUAaAACMcY-w9kCQy4ckZ4gUGRcQgIKbgRRpsM0RYmhR2LujGK+4cFEw1KOABQDyYRwppOZYMgtC6m1DIUUSNlwIKQnHR8Z0oDMPVOmIg7DgEU1ATicUi0+FwxkLBeCdDP6x1Li9KRnlvJHhkV6EQi0CEcJAbwbh+YCrbW1MtWQiMWIfxjiXDGBjFLnTGtI7KAMvT8jyPwIw1ReBskKFYaCFUjI6z4Pwng1DCq2wkXFbxrVYqQFMT2cQeJT4imECRIoAd6Sm34SYfUiMzDh3HDoFJ+jJHpP4NGJ2OMIDZPwokzQ9l5xHHgrnBaoEpy1DMCRGGMMRBmFEdHRBejPGPixtg-xk8ew5AKNoJmHIdDdw5LTRu2hdTimbsAsU9T5m7nLmhDp+kIbaE5Drfko4ii5D2VkcpRzhTAW5FoWwtggA */
233
+ id: "app",
234
+ context: () => createInitialContext(),
235
+
236
+ invoke: {
237
+ id: "auth",
238
+ src: "auth",
239
+ },
240
+
241
+ initial: "unauthenticated",
242
+
243
+ on: {
244
+ reset: [
245
+ {
246
+ actions: "resetContext",
247
+ target: ".unauthenticated",
248
+ },
249
+ ],
250
+ },
251
+
252
+ states: {
253
+ unauthenticated: {
254
+ description:
255
+ "The user is either completely anonymous, authenticated via insecure means (localStorage), or in an otherwise no-correctly-authenticated state (e.g. user mismatch).",
256
+
257
+ on: {
258
+ currentUser: [
259
+ {
260
+ guard: "isEligibleForSync",
261
+ target: "authenticated",
262
+
263
+ actions: {
264
+ type: "setCurrentUser",
265
+ params: ({ event }) => event.currentUser,
266
+ },
267
+ },
268
+ {
269
+ guard: "isNotMismatched",
270
+ target: "authenticated.ineligible",
271
+
272
+ actions: {
273
+ type: "setCurrentUser",
274
+ params: ({ event }) => event.currentUser,
275
+ },
276
+ },
277
+ { target: "unauthenticated" },
278
+ ],
279
+ serverUser: {
280
+ target: "authenticated",
281
+ actions: {
282
+ type: "resetWithServerUser",
283
+ params: ({ event }) => event.serverUser,
284
+ },
285
+ },
286
+ },
287
+ },
288
+
289
+ authenticated: {
290
+ description: "The user has a session with ITC.",
291
+
292
+ initial: "sync",
293
+
294
+ on: {
295
+ currentUser: [
296
+ {
297
+ guard: "hasBecomeEligibleForSync",
298
+ target: "authenticated",
299
+ reenter: true,
300
+
301
+ actions: {
302
+ type: "setCurrentUser",
303
+ params: ({ event }) => event.currentUser,
304
+ },
305
+ },
306
+ {
307
+ guard: "isNotMismatched",
308
+ actions: {
309
+ type: "setCurrentUser",
310
+ params: ({ event }) => event.currentUser,
311
+ },
312
+ },
313
+ {
314
+ target: "unauthenticated",
315
+ },
316
+ ],
317
+
318
+ sessionInfo: {
319
+ actions: {
320
+ type: "patchSessionInfo",
321
+ params: ({ event }) => event.sessionInfo,
322
+ },
323
+ },
324
+
325
+ sessionExpired: {
326
+ target: "unauthenticated",
327
+ },
328
+ },
329
+
330
+ states: {
331
+ ineligible: {
332
+ description: "The user is not eligible for sync (e.g. unverified)",
333
+ },
334
+
335
+ sync: {
336
+ description: "The user is eligible for the sync feature.",
337
+ invoke: {
338
+ id: "sync",
339
+ src: "sync",
340
+ },
341
+
342
+ initial: "syncing",
343
+
344
+ states: {
345
+ syncing: {
346
+ description: "The sync operation is in progress.",
347
+ initial: "pulling",
348
+ entry: "setCurrentSync",
349
+ exit: "flushCurrentSync",
350
+
351
+ on: {
352
+ push: {
353
+ actions: "queuePush",
354
+ },
355
+ },
356
+
357
+ states: {
358
+ pulling: {
359
+ invoke: {
360
+ src: "pullChanges",
361
+ input: ({ context }) => ({
362
+ sinceTs: context.lastSuccessfulSyncTs,
363
+ currentUser: context.currentUser!,
364
+ }),
365
+ onDone: {
366
+ target: "pushing",
367
+ actions: [
368
+ {
369
+ type: "markSyncStepResult",
370
+ params: ({ event }) => ({
371
+ pull: event.output,
372
+ }),
373
+ },
374
+ ],
375
+ },
376
+ onError: {
377
+ target: "failed",
378
+ actions: {
379
+ type: "markSyncStepResult",
380
+ params: ({ event }) => ({
381
+ pull: caughtToResult(event.error),
382
+ }),
383
+ },
384
+ },
385
+ },
386
+ },
387
+ pushing: {
388
+ entry: "clearPushQueue",
389
+
390
+ invoke: {
391
+ src: "pushChanges",
392
+ input: ({ context }) => {
393
+ assertNonNullish(
394
+ context.currentSync,
395
+ "Setting pushChanges input but context.currentSync is unset.",
396
+ );
397
+
398
+ assertNonNullish(
399
+ context.currentUser,
400
+ "Setting pushChanges input but context.currentUser is unset.",
401
+ );
402
+
403
+ // This value might be unset if no push was made during
404
+ // this sync attempt.
405
+ const pulledItems =
406
+ context.currentSync.pull?.valueOrNull()?.pulled ?? [];
407
+
408
+ return {
409
+ ignoredItems: pulledItems,
410
+ currentSyncTs: context.currentSync.startedTs,
411
+ sinceTs: context.lastSuccessfulSyncTs,
412
+ currentUser: context.currentUser,
413
+ };
414
+ },
415
+ onDone: {
416
+ target: "synced",
417
+ actions: {
418
+ type: "markSyncStepResult",
419
+ params: ({ event }) => ({
420
+ push: event.output,
421
+ }),
422
+ },
423
+ },
424
+ onError: {
425
+ target: "failed",
426
+ actions: {
427
+ type: "markSyncStepResult",
428
+ params: ({ event }) => ({
429
+ push: caughtToResult(event.error),
430
+ }),
431
+ },
432
+ },
433
+ },
434
+ },
435
+ synced: {
436
+ type: "final",
437
+ entry: "setLastSuccessfulSync",
438
+ },
439
+
440
+ failed: {
441
+ type: "final",
442
+ },
443
+ },
444
+
445
+ onDone: "idle",
446
+ },
447
+
448
+ idle: {
449
+ description: "The app is listening for sync events.",
450
+ always: {
451
+ guard: "shouldPushOnceIdle",
452
+ target: "waiting",
453
+ },
454
+ on: {
455
+ push: "waiting",
456
+ pull: "syncing",
457
+ },
458
+ },
459
+
460
+ waiting: {
461
+ description:
462
+ "We are waiting for further push events to batch them.",
463
+
464
+ after: {
465
+ 1500: {
466
+ target: "syncing.pushing",
467
+ },
468
+ },
469
+
470
+ on: {
471
+ push: {
472
+ target: "waiting",
473
+ reenter: true,
474
+ },
475
+ },
476
+ },
477
+ },
478
+ },
479
+ },
480
+ },
481
+ },
482
+ });
@@ -0,0 +1,45 @@
1
+ import type { Failure, Success } from "../async-op.ts";
2
+ import type { CurrentUser, FailurePayload, SessionInfo } from "../types.ts";
3
+
4
+ export type EmitterActor<Outgoing> = {
5
+ sendBack: (event: Outgoing) => void;
6
+ };
7
+
8
+ export type PullResult =
9
+ | Success<{ pulled: SyncedItem[] }>
10
+ | Failure<FailurePayload>;
11
+
12
+ export type PushResult =
13
+ | Success<{ pushed: SyncedItem[]; pulled: SyncedItem[] }>
14
+ | Failure<FailurePayload>;
15
+
16
+ export type SyncedItem = {
17
+ id: string;
18
+ name: string;
19
+ updatedTs: number;
20
+ deleted: boolean;
21
+ };
22
+
23
+ export type SyncAttempt = {
24
+ startedTs: number;
25
+ pull: PullResult | null;
26
+ push: PushResult | null;
27
+ };
28
+
29
+ export type MachineContext = {
30
+ currentSync: SyncAttempt | null;
31
+ lastSuccessfulSyncTs: number | null;
32
+ currentUser: CurrentUser | null;
33
+ sessionInfo: SessionInfo | null;
34
+ pushOnceIdle: boolean;
35
+ syncLog: SyncAttempt[];
36
+ };
37
+
38
+ export type MachineEvent =
39
+ | { type: "push" }
40
+ | { type: "pull" }
41
+ | { type: "currentUser"; currentUser: CurrentUser }
42
+ | { type: "serverUser"; serverUser: CurrentUser }
43
+ | { type: "sessionInfo"; sessionInfo: SessionInfo }
44
+ | { type: "sessionExpired" }
45
+ | { type: "reset" };
@@ -0,0 +1,54 @@
1
+ import type { UserGameData } from "@indietabletop/types";
2
+ import { Failure } from "../async-op.js";
3
+ import type { FailurePayload } from "../types.ts";
4
+ import type { SyncedItem } from "./types.ts";
5
+
6
+ /**
7
+ * Flattens the UserGameData structure to a flat SyncedItem[].
8
+ */
9
+ export function toSyncedItems(data: UserGameData) {
10
+ return Object.values(data).flatMap((groups) => {
11
+ return Object.values(groups).flatMap((items) => {
12
+ return items.map((item): SyncedItem => {
13
+ if (!("deleted" in item)) {
14
+ return {
15
+ id: item.id,
16
+ name: item.id,
17
+ deleted: false,
18
+ updatedTs: item.updatedTs,
19
+ };
20
+ }
21
+
22
+ return {
23
+ id: item.id,
24
+ name: item.name,
25
+ deleted: item.deleted,
26
+ updatedTs: item.updatedTs,
27
+ };
28
+ });
29
+ });
30
+ });
31
+ }
32
+
33
+ export function assertNonNullish<T>(
34
+ value: T,
35
+ message: string,
36
+ ): asserts value is NonNullable<T> {
37
+ if (value == null) {
38
+ throw new Error(message);
39
+ }
40
+ }
41
+
42
+ function isFailurePayload(
43
+ error: unknown,
44
+ ): error is { cause: Failure<FailurePayload> } {
45
+ return error instanceof Error && error.cause instanceof Failure;
46
+ }
47
+
48
+ export function caughtToResult(error: unknown): Failure<FailurePayload> {
49
+ if (isFailurePayload(error)) {
50
+ return error.cause;
51
+ }
52
+
53
+ return new Failure<FailurePayload>({ type: "UNKNOWN_ERROR" });
54
+ }
@@ -0,0 +1,10 @@
1
+ import { FormProvider, type FormProviderProps } from "@ariakit/react";
2
+ import { type Decorator } from "@storybook/react-vite";
3
+
4
+ export function form(props?: FormProviderProps): Decorator {
5
+ return (Story) => (
6
+ <FormProvider {...props}>
7
+ <Story />
8
+ </FormProvider>
9
+ );
10
+ }
package/lib/structs.ts ADDED
@@ -0,0 +1,3 @@
1
+ // Structs are exported from here for backwards compat. Generally, types
2
+ // should be used directly from the types package.
3
+ export { currentUser, redeemedPledge, sessionInfo } from "@indietabletop/types";
@@ -7,4 +7,6 @@
7
7
  * This function uses the same semantics like the nullish coalescing operators
8
8
  * like `??` and `??=`.
9
9
  */
10
- export declare function isNullish(value: unknown): value is null | undefined;
10
+ export function isNullish(value: unknown): value is null | undefined {
11
+ return value === null || value === undefined;
12
+ }
@@ -1,9 +1,12 @@
1
- import { Infer } from 'superstruct';
2
- import { currentUser, redeemedPledge, sessionInfo } from './structs.js';
3
- type Brand<B> = {
4
- __brand: B;
5
- };
1
+ import type { Infer } from "superstruct";
2
+ import { currentUser, redeemedPledge, sessionInfo } from "./structs.js";
3
+
4
+ // Generic type helpers
5
+
6
+ type Brand<B> = { __brand: B };
7
+
6
8
  export type Branded<T, B> = T & Brand<B>;
9
+
7
10
  /**
8
11
  * Make properties in union K required in T.
9
12
  *
@@ -20,6 +23,7 @@ export type Branded<T, B> = T & Brand<B>;
20
23
  * ```
21
24
  */
22
25
  export type RequiredPick<T, K extends keyof T> = T & Required<Pick<T, K>>;
26
+
23
27
  /**
24
28
  * A branded string.
25
29
  *
@@ -28,13 +32,20 @@ export type RequiredPick<T, K extends keyof T> = T & Required<Pick<T, K>>;
28
32
  * that exists in our codebase) or sanitized user-generated content.
29
33
  */
30
34
  export type TrustedHtml = Branded<string, "TrustedHtml">;
35
+
36
+ // Common ITC types
37
+
31
38
  export type CurrentUser = Infer<ReturnType<typeof currentUser>>;
39
+
32
40
  export type SessionInfo = Infer<ReturnType<typeof sessionInfo>>;
41
+
33
42
  export type RedeemedPledge = Infer<ReturnType<typeof redeemedPledge>>;
34
- export type FailurePayload = {
35
- type: "API_ERROR";
36
- code: number;
37
- } | {
38
- type: "NETWORK_ERROR" | "UNKNOWN_ERROR" | "VALIDATION_ERROR";
39
- };
40
- export {};
43
+
44
+ export type FailurePayload =
45
+ | {
46
+ type: "API_ERROR";
47
+ code: number;
48
+ }
49
+ | {
50
+ type: "NETWORK_ERROR" | "UNKNOWN_ERROR" | "VALIDATION_ERROR";
51
+ };
package/lib/unique.ts ADDED
@@ -0,0 +1,24 @@
1
+ type UniqueKey = string | number;
2
+
3
+ /**
4
+ * Returns an array of unique items, determining uniqueness via the getKey
5
+ * function.
6
+ *
7
+ * Note that the first unique item is returned, all others are omitted
8
+ * (assuming that they are unique, so it shouldn't matter).
9
+ */
10
+ export function uniqueBy<T>(items: T[], getKey: (item: T) => UniqueKey): T[] {
11
+ const seen = new Set<UniqueKey>();
12
+ const returnItems: T[] = [];
13
+
14
+ for (const item of items) {
15
+ const uniqueKey = getKey(item);
16
+
17
+ if (!seen.has(uniqueKey)) {
18
+ returnItems.push(item);
19
+ seen.add(uniqueKey);
20
+ }
21
+ }
22
+
23
+ return returnItems;
24
+ }
@@ -0,0 +1,16 @@
1
+ import { useCallback, useState } from "react";
2
+ import { type AsyncOp, Failure, Pending, Success } from "./async-op.js";
3
+
4
+ export function useAsyncOp<T, E>() {
5
+ const [op, setOp] = useState<AsyncOp<T, E>>(new Pending());
6
+
7
+ const setSuccess = useCallback((value: T) => {
8
+ setOp(new Success(value));
9
+ }, []);
10
+
11
+ const setFailure = useCallback((failure: E) => {
12
+ setOp(new Failure(failure));
13
+ }, []);
14
+
15
+ return { op, setSuccess, setFailure };
16
+ }