@indietabletop/appkit 5.2.0 → 5.3.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.
- package/lib/EnumMapper.ts +50 -0
- package/lib/account/JoinCard.tsx +8 -11
- package/lib/client.ts +63 -5
- package/lib/index.ts +2 -0
- package/lib/structs.ts +3 -27
- package/lib/useInvokeClient.ts +54 -0
- package/package.json +2 -2
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
type InputValues<K extends string | number, V> = Record<K, V> | Array<[K, V]>;
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Handles mapping enums to arbitrary values.
|
|
5
|
+
*/
|
|
6
|
+
export class EnumMapper<K extends string | number, V> {
|
|
7
|
+
private map: Map<K, V>;
|
|
8
|
+
private fallback: V;
|
|
9
|
+
|
|
10
|
+
constructor(values: InputValues<K, V>, fallback: V) {
|
|
11
|
+
const entries = Array.isArray(values)
|
|
12
|
+
? values
|
|
13
|
+
: (Object.entries(values) as Array<[K, V]>);
|
|
14
|
+
this.map = new Map(entries);
|
|
15
|
+
this.fallback = fallback;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Returns a value corresponding to the provided key.
|
|
20
|
+
*
|
|
21
|
+
* If no match is found, returns the fallback value provided in the constructor.
|
|
22
|
+
*
|
|
23
|
+
* Note that TypeScript will make sure that we only ever use the right types
|
|
24
|
+
* as the enum param, but it can still happen at runtime that different
|
|
25
|
+
* values are provided.
|
|
26
|
+
*/
|
|
27
|
+
get(key: K | undefined): V {
|
|
28
|
+
if (key === undefined) {
|
|
29
|
+
return this.fallback;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const value = this.map.get(key);
|
|
33
|
+
if (value === undefined) {
|
|
34
|
+
return this.fallback;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* All enums known by this mapper.
|
|
42
|
+
*/
|
|
43
|
+
get enums() {
|
|
44
|
+
return Array.from(this.map.keys());
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
static from<K extends string>(values: InputValues<K, string>) {
|
|
48
|
+
return new EnumMapper(values, "Unknown");
|
|
49
|
+
}
|
|
50
|
+
}
|
package/lib/account/JoinCard.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Form, useStoreState } from "@ariakit/react";
|
|
2
2
|
import { type Dispatch, type SetStateAction, useState } from "react";
|
|
3
|
-
import { Link
|
|
3
|
+
import { Link } from "wouter";
|
|
4
4
|
import { useAppConfig, useClient } from "../AppConfig/AppConfig.tsx";
|
|
5
5
|
import { cx } from "../class-names.ts";
|
|
6
6
|
import { interactiveText } from "../common.css.ts";
|
|
@@ -163,14 +163,7 @@ type SubmitCodeStepProps = {
|
|
|
163
163
|
|
|
164
164
|
function SubmitCodeStep(props: SubmitCodeStepProps) {
|
|
165
165
|
const { tokenId, email, setStep } = props;
|
|
166
|
-
const [_, navigate] = useLocation();
|
|
167
166
|
const client = useClient();
|
|
168
|
-
const redirectPath = useRedirectPath();
|
|
169
|
-
|
|
170
|
-
// Use redirect path if found in search params, otherwise go the success step.
|
|
171
|
-
const onSuccess = redirectPath
|
|
172
|
-
? () => navigate(redirectPath)
|
|
173
|
-
: () => setStep({ type: "SUCCESS" });
|
|
174
167
|
|
|
175
168
|
const { form, submitName } = useForm({
|
|
176
169
|
defaultValues: {
|
|
@@ -185,7 +178,9 @@ function SubmitCodeStep(props: SubmitCodeStepProps) {
|
|
|
185
178
|
});
|
|
186
179
|
});
|
|
187
180
|
},
|
|
188
|
-
onSuccess
|
|
181
|
+
onSuccess() {
|
|
182
|
+
setStep({ type: "SUCCESS" });
|
|
183
|
+
},
|
|
189
184
|
});
|
|
190
185
|
|
|
191
186
|
return (
|
|
@@ -218,6 +213,8 @@ function SubmitCodeStep(props: SubmitCodeStepProps) {
|
|
|
218
213
|
|
|
219
214
|
function SuccessStep() {
|
|
220
215
|
const { hrefs } = useAppConfig();
|
|
216
|
+
const redirectPath = useRedirectPath();
|
|
217
|
+
|
|
221
218
|
return (
|
|
222
219
|
<Letterhead>
|
|
223
220
|
<LetterheadHeader>
|
|
@@ -227,8 +224,8 @@ function SuccessStep() {
|
|
|
227
224
|
</LetterheadParagraph>
|
|
228
225
|
</LetterheadHeader>
|
|
229
226
|
|
|
230
|
-
<Link href={hrefs.dashboard()} {...cx(button())}>
|
|
231
|
-
Go to dashboard
|
|
227
|
+
<Link href={redirectPath ?? hrefs.dashboard()} {...cx(button())}>
|
|
228
|
+
{redirectPath ? "Continue" : "Go to dashboard"}
|
|
232
229
|
</Link>
|
|
233
230
|
</Letterhead>
|
|
234
231
|
);
|
package/lib/client.ts
CHANGED
|
@@ -1,8 +1,23 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import {
|
|
2
|
+
currentUser,
|
|
3
|
+
featureUnlock,
|
|
4
|
+
ownedProduct,
|
|
5
|
+
redeemedPledge,
|
|
6
|
+
sessionInfo,
|
|
7
|
+
type FeatureUnlock,
|
|
8
|
+
type SpaceGits,
|
|
9
|
+
} from "@indietabletop/types";
|
|
10
|
+
import {
|
|
11
|
+
array,
|
|
12
|
+
mask,
|
|
13
|
+
object,
|
|
14
|
+
string,
|
|
15
|
+
Struct,
|
|
16
|
+
unknown,
|
|
17
|
+
type Infer,
|
|
18
|
+
} from "superstruct";
|
|
19
|
+
import { Failure, Success } from "./async-op.ts";
|
|
20
|
+
import type { CurrentUser, FailurePayload, SessionInfo } from "./types.ts";
|
|
6
21
|
|
|
7
22
|
export type UserGameData = {
|
|
8
23
|
spacegits?: {
|
|
@@ -571,4 +586,47 @@ export class IndieTabletopClient {
|
|
|
571
586
|
},
|
|
572
587
|
);
|
|
573
588
|
}
|
|
589
|
+
|
|
590
|
+
async getUnlocks<T extends FeatureUnlock["gameCode"]>(gameCode: T) {
|
|
591
|
+
return this.fetch(
|
|
592
|
+
`/v1/me/unlocks/${gameCode}`,
|
|
593
|
+
object({
|
|
594
|
+
unlocks: array(
|
|
595
|
+
featureUnlock() as unknown as Struct<
|
|
596
|
+
Extract<FeatureUnlock, { gameCode: T }>,
|
|
597
|
+
null
|
|
598
|
+
>,
|
|
599
|
+
),
|
|
600
|
+
}),
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
async getOwnedProduct(productCode: string) {
|
|
605
|
+
return await this.fetch(`/v1/me/products/${productCode}`, ownedProduct());
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
async startCheckoutSession(payload: {
|
|
609
|
+
products: { code: string; quantity: number }[];
|
|
610
|
+
successUrl: string;
|
|
611
|
+
cancelUrl: string;
|
|
612
|
+
}) {
|
|
613
|
+
return await this.fetch(
|
|
614
|
+
`/v1/stripe/sessions`,
|
|
615
|
+
object({ redirectTo: string() }),
|
|
616
|
+
{
|
|
617
|
+
method: "POST",
|
|
618
|
+
json: payload,
|
|
619
|
+
},
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
async getCheckoutSession(sessionId: string) {
|
|
624
|
+
return await this.fetch(
|
|
625
|
+
`/v1/stripe/sessions/${sessionId}`,
|
|
626
|
+
object({
|
|
627
|
+
status: string(),
|
|
628
|
+
products: array(ownedProduct()),
|
|
629
|
+
}),
|
|
630
|
+
);
|
|
631
|
+
}
|
|
574
632
|
}
|
package/lib/index.ts
CHANGED
|
@@ -35,6 +35,7 @@ export * from "./use-media-query.ts";
|
|
|
35
35
|
export * from "./use-reverting-state.ts";
|
|
36
36
|
export * from "./use-scroll-restoration.ts";
|
|
37
37
|
export * from "./useEnsureValue.ts";
|
|
38
|
+
export * from "./useInvokeClient.ts";
|
|
38
39
|
export * from "./useIsVisible.ts";
|
|
39
40
|
|
|
40
41
|
// Utils
|
|
@@ -64,5 +65,6 @@ export * from "./utm.ts";
|
|
|
64
65
|
export * from "./validations.ts";
|
|
65
66
|
|
|
66
67
|
// Other
|
|
68
|
+
export * from "./EnumMapper.ts";
|
|
67
69
|
export * from "./ModernIDB/index.ts";
|
|
68
70
|
export * from "./store/index.tsx";
|
package/lib/structs.ts
CHANGED
|
@@ -1,27 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export
|
|
4
|
-
return object({
|
|
5
|
-
id: string(),
|
|
6
|
-
email: string(),
|
|
7
|
-
isVerified: boolean(),
|
|
8
|
-
prefersScrollbarVisibility: optional(enums(["ALWAYS"])),
|
|
9
|
-
});
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function sessionInfo() {
|
|
13
|
-
return object({
|
|
14
|
-
expiresTs: number(),
|
|
15
|
-
createdTs: number(),
|
|
16
|
-
});
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function redeemedPledge() {
|
|
20
|
-
return object({
|
|
21
|
-
id: string(),
|
|
22
|
-
downloadUrl: string(),
|
|
23
|
-
downloadUrlExpiresTs: number(),
|
|
24
|
-
contactSubscribed: boolean(),
|
|
25
|
-
email: string(),
|
|
26
|
-
});
|
|
27
|
-
}
|
|
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";
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { useCallback } from "react";
|
|
2
|
+
import type { SWRConfiguration, SWRResponse } from "swr";
|
|
3
|
+
import useSWR from "swr";
|
|
4
|
+
import type { Pending } from "./async-op.ts";
|
|
5
|
+
import type { IndieTabletopClient } from "./client.ts";
|
|
6
|
+
import { swrResponseToResult } from "./result/swr.ts";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Creates a `useInvokeClient` hook that makes it ergonomic to invoke
|
|
10
|
+
* client methods.
|
|
11
|
+
*
|
|
12
|
+
* Note that only methods starting with `get` are picked up. This hook is
|
|
13
|
+
* intended for making easy fetches, not for form submissions.
|
|
14
|
+
*/
|
|
15
|
+
export function createUseInvokeClient<C extends IndieTabletopClient>(c: C) {
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
17
|
+
type Fn = (...args: any) => any;
|
|
18
|
+
|
|
19
|
+
return function useInvokeClient<K extends keyof C & `get${string}`>(
|
|
20
|
+
/**
|
|
21
|
+
* ITC Client's method name.
|
|
22
|
+
*/
|
|
23
|
+
method: K,
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Method's arguments.
|
|
27
|
+
*
|
|
28
|
+
* **IMPORTANT**: Must be an array or primitive values as it is directly
|
|
29
|
+
* used as a `useCallback` dependecy array.
|
|
30
|
+
*/
|
|
31
|
+
args: C[K] extends Fn ? Parameters<C[K]> : never,
|
|
32
|
+
|
|
33
|
+
options?: {
|
|
34
|
+
performFetch?: boolean;
|
|
35
|
+
swr?: SWRConfiguration;
|
|
36
|
+
},
|
|
37
|
+
): C[K] extends Fn
|
|
38
|
+
? [Awaited<ReturnType<C[K]>> | Pending, SWRResponse<unknown, unknown>]
|
|
39
|
+
: never {
|
|
40
|
+
const { performFetch = true } = options ?? {};
|
|
41
|
+
|
|
42
|
+
const callback = useCallback(() => {
|
|
43
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
|
44
|
+
return (c[method] as Fn)(...args);
|
|
45
|
+
|
|
46
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
47
|
+
}, [method, ...args]);
|
|
48
|
+
|
|
49
|
+
const cacheKey = [method, ...args].join(":");
|
|
50
|
+
const swr = useSWR(performFetch ? cacheKey : null, callback, options?.swr);
|
|
51
|
+
|
|
52
|
+
return [swrResponseToResult(swr), swr] as never;
|
|
53
|
+
};
|
|
54
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@indietabletop/appkit",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.3.0",
|
|
4
4
|
"description": "A collection of modules used in apps built by Indie Tabletop Club",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"@ariakit/react": "^0.4.17",
|
|
46
46
|
"@indietabletop/tooling": "^5.0.0",
|
|
47
|
-
"@indietabletop/types": "^1.
|
|
47
|
+
"@indietabletop/types": "^1.1.0",
|
|
48
48
|
"@vanilla-extract/css": "^1.17.2",
|
|
49
49
|
"@vanilla-extract/dynamic": "^2.1.3",
|
|
50
50
|
"@vanilla-extract/recipes": "^0.5.7",
|