@itzsudhan/creem-expo 0.1.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/README.md +45 -0
- package/app.plugin.js +202 -0
- package/dist/chunk-BF74I2QD.mjs +857 -0
- package/dist/express-BAx2zfw7.d.mts +84 -0
- package/dist/express-jb3wXcce.d.ts +84 -0
- package/dist/index.d.mts +164 -0
- package/dist/index.d.ts +164 -0
- package/dist/index.js +1132 -0
- package/dist/index.mjs +1078 -0
- package/dist/server/express.d.mts +5 -0
- package/dist/server/express.d.ts +5 -0
- package/dist/server/express.js +888 -0
- package/dist/server/express.mjs +10 -0
- package/dist/server/index.d.mts +15 -0
- package/dist/server/index.d.ts +15 -0
- package/dist/server/index.js +898 -0
- package/dist/server/index.mjs +25 -0
- package/dist/types-NcFyNrWp.d.mts +271 -0
- package/dist/types-NcFyNrWp.d.ts +271 -0
- package/package.json +106 -0
- package/src/client/apiClient.ts +195 -0
- package/src/client/components/CreemCheckoutButton.tsx +91 -0
- package/src/client/components/CreemCheckoutModal.tsx +81 -0
- package/src/client/components/CreemManageSubscriptionButton.tsx +58 -0
- package/src/client/context.tsx +57 -0
- package/src/client/hooks/useCreemCheckout.ts +478 -0
- package/src/client/hooks/useCreemSubscription.ts +194 -0
- package/src/client/utils/checkoutState.ts +99 -0
- package/src/client/utils/linking.ts +232 -0
- package/src/errors.ts +61 -0
- package/src/index.ts +19 -0
- package/src/server/core.ts +815 -0
- package/src/server/createCreemClient.ts +16 -0
- package/src/server/express.ts +187 -0
- package/src/server/fetchHandlers.ts +191 -0
- package/src/server/index.ts +6 -0
- package/src/server/json.ts +44 -0
- package/src/server/signatures.ts +18 -0
- package/src/types.ts +402 -0
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
import { useEffect, useReducer, useRef, useState } from "react";
|
|
2
|
+
import * as Linking from "expo-linking";
|
|
3
|
+
import * as WebBrowser from "expo-web-browser";
|
|
4
|
+
import type { WebViewNavigation } from "react-native-webview/lib/WebViewTypes";
|
|
5
|
+
import { CreemError } from "../../errors";
|
|
6
|
+
import type {
|
|
7
|
+
CheckoutPresentation,
|
|
8
|
+
CheckoutVerificationResult,
|
|
9
|
+
CheckoutSessionResult,
|
|
10
|
+
CreateCheckoutInput,
|
|
11
|
+
LaunchCheckoutInput,
|
|
12
|
+
ResourceStatus,
|
|
13
|
+
StartCheckoutOptions,
|
|
14
|
+
VerifyCheckoutInput,
|
|
15
|
+
VerifyCheckoutOptions,
|
|
16
|
+
} from "../../types";
|
|
17
|
+
import { useCreemContext } from "../context";
|
|
18
|
+
import {
|
|
19
|
+
appendMetadataToUrl,
|
|
20
|
+
buildCheckoutUrlFromSessionId,
|
|
21
|
+
buildReturnUrl,
|
|
22
|
+
buildSuccessResult,
|
|
23
|
+
detectCheckoutStatusFromUrl,
|
|
24
|
+
} from "../utils/linking";
|
|
25
|
+
import {
|
|
26
|
+
checkoutReducer,
|
|
27
|
+
initialCheckoutState,
|
|
28
|
+
} from "../utils/checkoutState";
|
|
29
|
+
|
|
30
|
+
type ActiveRedirects = {
|
|
31
|
+
successUrl: string;
|
|
32
|
+
cancelUrl?: string;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const DEFAULT_VERIFY_TIMEOUT_MS = 12_000;
|
|
36
|
+
const DEFAULT_VERIFY_POLL_INTERVAL_MS = 1_200;
|
|
37
|
+
|
|
38
|
+
const createDirectSession = (
|
|
39
|
+
input: LaunchCheckoutInput,
|
|
40
|
+
checkoutUrl: string,
|
|
41
|
+
successUrl: string
|
|
42
|
+
): CheckoutSessionResult => {
|
|
43
|
+
const checkoutUrlObject = new URL(checkoutUrl);
|
|
44
|
+
const checkoutId =
|
|
45
|
+
input.sessionId ??
|
|
46
|
+
checkoutUrlObject.searchParams.get("session_id") ??
|
|
47
|
+
checkoutUrlObject.searchParams.get("checkout_id") ??
|
|
48
|
+
"direct_checkout";
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
checkoutId,
|
|
52
|
+
checkoutUrl,
|
|
53
|
+
requestId: input.sessionId,
|
|
54
|
+
successUrl,
|
|
55
|
+
status: "pending",
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const resolveDirectCheckoutUrl = (input: LaunchCheckoutInput) => {
|
|
60
|
+
if (input.checkoutUrl) {
|
|
61
|
+
return input.checkoutUrl;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (input.sessionId) {
|
|
65
|
+
return buildCheckoutUrlFromSessionId(input.sessionId);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
throw new CreemError(
|
|
69
|
+
"INVALID_CHECKOUT_OPTIONS",
|
|
70
|
+
"Provide either `checkoutUrl` or `sessionId` when launching an existing Creem checkout."
|
|
71
|
+
);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const normalizeCheckoutError = (
|
|
75
|
+
error: unknown,
|
|
76
|
+
code: "CHECKOUT_LAUNCH_FAILED" | "INVALID_CHECKOUT_OPTIONS",
|
|
77
|
+
fallbackMessage: string
|
|
78
|
+
) => {
|
|
79
|
+
if (error instanceof CreemError) {
|
|
80
|
+
return error;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (error instanceof Error) {
|
|
84
|
+
return new CreemError(code, error.message || fallbackMessage, {
|
|
85
|
+
cause: error,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return new CreemError(code, fallbackMessage, {
|
|
90
|
+
cause: error,
|
|
91
|
+
});
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const normalizeVerificationError = (error: unknown, fallbackMessage: string) => {
|
|
95
|
+
if (error instanceof CreemError) {
|
|
96
|
+
return error;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (error instanceof Error) {
|
|
100
|
+
return new CreemError("CHECKOUT_VERIFICATION_FAILED", error.message || fallbackMessage, {
|
|
101
|
+
cause: error,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return new CreemError("CHECKOUT_VERIFICATION_FAILED", fallbackMessage, {
|
|
106
|
+
cause: error,
|
|
107
|
+
});
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const resolveVerifyOptions = (
|
|
111
|
+
value: boolean | VerifyCheckoutOptions | undefined
|
|
112
|
+
): { enabled: boolean; pollIntervalMs: number; timeoutMs: number } => {
|
|
113
|
+
if (!value) {
|
|
114
|
+
return {
|
|
115
|
+
enabled: false,
|
|
116
|
+
pollIntervalMs: DEFAULT_VERIFY_POLL_INTERVAL_MS,
|
|
117
|
+
timeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (value === true) {
|
|
122
|
+
return {
|
|
123
|
+
enabled: true,
|
|
124
|
+
pollIntervalMs: DEFAULT_VERIFY_POLL_INTERVAL_MS,
|
|
125
|
+
timeoutMs: DEFAULT_VERIFY_TIMEOUT_MS,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
enabled: true,
|
|
131
|
+
pollIntervalMs: value.pollIntervalMs ?? DEFAULT_VERIFY_POLL_INTERVAL_MS,
|
|
132
|
+
timeoutMs: value.timeoutMs ?? DEFAULT_VERIFY_TIMEOUT_MS,
|
|
133
|
+
};
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const wait = (timeMs: number) =>
|
|
137
|
+
new Promise<void>((resolve) => {
|
|
138
|
+
setTimeout(resolve, timeMs);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
export const __internal = {
|
|
142
|
+
createDirectSession,
|
|
143
|
+
resolveDirectCheckoutUrl,
|
|
144
|
+
normalizeCheckoutError,
|
|
145
|
+
normalizeVerificationError,
|
|
146
|
+
resolveVerifyOptions,
|
|
147
|
+
wait,
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export const useCreemCheckout = () => {
|
|
151
|
+
const {
|
|
152
|
+
apiClient,
|
|
153
|
+
defaultPresentation,
|
|
154
|
+
returnPath,
|
|
155
|
+
defaultVerifyOnSuccess,
|
|
156
|
+
logger,
|
|
157
|
+
} = useCreemContext();
|
|
158
|
+
const [state, dispatch] = useReducer(checkoutReducer, initialCheckoutState);
|
|
159
|
+
const activeRedirectsRef = useRef<ActiveRedirects | null>(null);
|
|
160
|
+
const activeVerifyOptionsRef = useRef<boolean | VerifyCheckoutOptions | undefined>(
|
|
161
|
+
defaultVerifyOnSuccess
|
|
162
|
+
);
|
|
163
|
+
const [verification, setVerification] = useState<CheckoutVerificationResult | null>(null);
|
|
164
|
+
const [verificationStatus, setVerificationStatus] =
|
|
165
|
+
useState<ResourceStatus>("idle");
|
|
166
|
+
const [verificationError, setVerificationError] = useState<Error | null>(null);
|
|
167
|
+
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
WebBrowser.maybeCompleteAuthSession();
|
|
170
|
+
}, []);
|
|
171
|
+
|
|
172
|
+
const clearActiveCheckoutContext = () => {
|
|
173
|
+
activeRedirectsRef.current = null;
|
|
174
|
+
activeVerifyOptionsRef.current = defaultVerifyOnSuccess;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const clearVerification = () => {
|
|
178
|
+
setVerification(null);
|
|
179
|
+
setVerificationStatus("idle");
|
|
180
|
+
setVerificationError(null);
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const setActiveRedirects = (
|
|
184
|
+
successUrl: string,
|
|
185
|
+
cancelUrl: string | undefined,
|
|
186
|
+
verifyOptions: boolean | VerifyCheckoutOptions | undefined
|
|
187
|
+
) => {
|
|
188
|
+
activeRedirectsRef.current = {
|
|
189
|
+
successUrl,
|
|
190
|
+
cancelUrl,
|
|
191
|
+
};
|
|
192
|
+
activeVerifyOptionsRef.current = verifyOptions;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const runCheckoutVerification = async (
|
|
196
|
+
checkoutId: string,
|
|
197
|
+
options?: VerifyCheckoutOptions
|
|
198
|
+
) => {
|
|
199
|
+
setVerificationStatus("loading");
|
|
200
|
+
setVerificationError(null);
|
|
201
|
+
|
|
202
|
+
const pollIntervalMs = options?.pollIntervalMs ?? DEFAULT_VERIFY_POLL_INTERVAL_MS;
|
|
203
|
+
const timeoutMs = options?.timeoutMs ?? DEFAULT_VERIFY_TIMEOUT_MS;
|
|
204
|
+
const startedAt = Date.now();
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
while (true) {
|
|
208
|
+
const nextVerification = await apiClient.verifyCheckout({
|
|
209
|
+
checkoutId,
|
|
210
|
+
});
|
|
211
|
+
setVerification(nextVerification);
|
|
212
|
+
|
|
213
|
+
if (nextVerification.verified || Date.now() - startedAt >= timeoutMs) {
|
|
214
|
+
setVerificationStatus("ready");
|
|
215
|
+
return nextVerification;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
await wait(pollIntervalMs);
|
|
219
|
+
}
|
|
220
|
+
} catch (error) {
|
|
221
|
+
const normalizedError = normalizeVerificationError(
|
|
222
|
+
error,
|
|
223
|
+
"Failed to verify the Creem checkout after redirect."
|
|
224
|
+
);
|
|
225
|
+
setVerificationError(normalizedError);
|
|
226
|
+
setVerificationStatus("error");
|
|
227
|
+
logger?.error?.("creem checkout verification failed", normalizedError);
|
|
228
|
+
throw normalizedError;
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const completeFromUrl = async (session: CheckoutSessionResult, url: string) => {
|
|
233
|
+
const verifyOptions = resolveVerifyOptions(activeVerifyOptionsRef.current);
|
|
234
|
+
clearActiveCheckoutContext();
|
|
235
|
+
|
|
236
|
+
let verificationResult: CheckoutVerificationResult | null = null;
|
|
237
|
+
|
|
238
|
+
if (verifyOptions.enabled) {
|
|
239
|
+
try {
|
|
240
|
+
verificationResult = await runCheckoutVerification(session.checkoutId, verifyOptions);
|
|
241
|
+
} catch {
|
|
242
|
+
verificationResult = null;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const result = buildSuccessResult(session, url, verificationResult);
|
|
247
|
+
dispatch({ type: "success", result });
|
|
248
|
+
logger?.info?.("creem checkout completed", result);
|
|
249
|
+
return result;
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const cancelCheckout = (session?: CheckoutSessionResult, url?: string) => {
|
|
253
|
+
clearActiveCheckoutContext();
|
|
254
|
+
dispatch({ type: "cancelled", session });
|
|
255
|
+
logger?.info?.("creem checkout cancelled", {
|
|
256
|
+
url,
|
|
257
|
+
checkoutId: session?.checkoutId,
|
|
258
|
+
});
|
|
259
|
+
return session;
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const detectReturnStatus = (url: string) => {
|
|
263
|
+
const activeRedirects = activeRedirectsRef.current;
|
|
264
|
+
|
|
265
|
+
if (!activeRedirects) {
|
|
266
|
+
return undefined;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return detectCheckoutStatusFromUrl(
|
|
270
|
+
url,
|
|
271
|
+
activeRedirects.successUrl,
|
|
272
|
+
activeRedirects.cancelUrl
|
|
273
|
+
);
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
const presentCheckout = async (
|
|
277
|
+
session: CheckoutSessionResult,
|
|
278
|
+
redirects: ActiveRedirects,
|
|
279
|
+
presentation: CheckoutPresentation,
|
|
280
|
+
verifyOptions: boolean | VerifyCheckoutOptions | undefined
|
|
281
|
+
) => {
|
|
282
|
+
setActiveRedirects(redirects.successUrl, redirects.cancelUrl, verifyOptions);
|
|
283
|
+
|
|
284
|
+
if (presentation === "webview") {
|
|
285
|
+
dispatch({
|
|
286
|
+
type: "present",
|
|
287
|
+
session,
|
|
288
|
+
returnUrl: redirects.successUrl,
|
|
289
|
+
});
|
|
290
|
+
return session;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
dispatch({ type: "browser-session", session });
|
|
294
|
+
const browserResult = await WebBrowser.openAuthSessionAsync(
|
|
295
|
+
session.checkoutUrl,
|
|
296
|
+
redirects.successUrl
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
if (browserResult.type === "success" && "url" in browserResult && browserResult.url) {
|
|
300
|
+
const detectedStatus = detectReturnStatus(browserResult.url);
|
|
301
|
+
|
|
302
|
+
if (detectedStatus === "cancelled") {
|
|
303
|
+
return cancelCheckout(session, browserResult.url);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return completeFromUrl(session, browserResult.url);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (browserResult.type === "cancel" || browserResult.type === "dismiss") {
|
|
310
|
+
cancelCheckout(session);
|
|
311
|
+
return session;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return session;
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const startCheckout = async (
|
|
318
|
+
input: CreateCheckoutInput,
|
|
319
|
+
options?: StartCheckoutOptions
|
|
320
|
+
) => {
|
|
321
|
+
clearVerification();
|
|
322
|
+
dispatch({ type: "start" });
|
|
323
|
+
|
|
324
|
+
try {
|
|
325
|
+
const resolvedReturnUrl =
|
|
326
|
+
input.returnUrl ??
|
|
327
|
+
buildReturnUrl(options?.returnPath ?? returnPath, Linking.createURL);
|
|
328
|
+
const session = await apiClient.createCheckoutSession({
|
|
329
|
+
...input,
|
|
330
|
+
returnUrl: resolvedReturnUrl,
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
return await presentCheckout(
|
|
334
|
+
session,
|
|
335
|
+
{
|
|
336
|
+
successUrl: resolvedReturnUrl,
|
|
337
|
+
},
|
|
338
|
+
options?.presentation ?? defaultPresentation,
|
|
339
|
+
options?.verifyOnSuccess ?? defaultVerifyOnSuccess
|
|
340
|
+
);
|
|
341
|
+
} catch (error) {
|
|
342
|
+
clearActiveCheckoutContext();
|
|
343
|
+
const normalizedError = normalizeCheckoutError(
|
|
344
|
+
error,
|
|
345
|
+
"CHECKOUT_LAUNCH_FAILED",
|
|
346
|
+
"Failed to start the Creem checkout."
|
|
347
|
+
);
|
|
348
|
+
logger?.error?.("creem checkout failed", normalizedError);
|
|
349
|
+
dispatch({ type: "error", error: normalizedError });
|
|
350
|
+
throw normalizedError;
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
const launchCheckout = async (
|
|
355
|
+
input: LaunchCheckoutInput,
|
|
356
|
+
options?: StartCheckoutOptions
|
|
357
|
+
) => {
|
|
358
|
+
clearVerification();
|
|
359
|
+
dispatch({ type: "start" });
|
|
360
|
+
|
|
361
|
+
try {
|
|
362
|
+
const successUrl =
|
|
363
|
+
input.returnUrl ??
|
|
364
|
+
buildReturnUrl(options?.returnPath ?? returnPath, Linking.createURL);
|
|
365
|
+
const cancelUrl =
|
|
366
|
+
input.cancelUrl ??
|
|
367
|
+
buildReturnUrl(options?.cancelPath ?? "creem/cancel", Linking.createURL);
|
|
368
|
+
const checkoutUrl = appendMetadataToUrl(
|
|
369
|
+
resolveDirectCheckoutUrl(input),
|
|
370
|
+
input.metadata
|
|
371
|
+
);
|
|
372
|
+
const session = createDirectSession(input, checkoutUrl, successUrl);
|
|
373
|
+
|
|
374
|
+
return await presentCheckout(
|
|
375
|
+
session,
|
|
376
|
+
{
|
|
377
|
+
successUrl,
|
|
378
|
+
cancelUrl,
|
|
379
|
+
},
|
|
380
|
+
options?.presentation ?? defaultPresentation,
|
|
381
|
+
options?.verifyOnSuccess ?? defaultVerifyOnSuccess
|
|
382
|
+
);
|
|
383
|
+
} catch (error) {
|
|
384
|
+
clearActiveCheckoutContext();
|
|
385
|
+
const normalizedError = normalizeCheckoutError(
|
|
386
|
+
error,
|
|
387
|
+
error instanceof CreemError &&
|
|
388
|
+
error.code === "INVALID_CHECKOUT_OPTIONS"
|
|
389
|
+
? "INVALID_CHECKOUT_OPTIONS"
|
|
390
|
+
: "CHECKOUT_LAUNCH_FAILED",
|
|
391
|
+
"Failed to launch the Creem checkout."
|
|
392
|
+
);
|
|
393
|
+
logger?.error?.("creem direct checkout failed", normalizedError);
|
|
394
|
+
dispatch({ type: "error", error: normalizedError });
|
|
395
|
+
throw normalizedError;
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
const handleReturnUrl = (url: string) => {
|
|
400
|
+
if (!state.session) {
|
|
401
|
+
return false;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const detectedStatus = detectReturnStatus(url);
|
|
405
|
+
|
|
406
|
+
if (!detectedStatus) {
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (detectedStatus === "success") {
|
|
411
|
+
completeFromUrl(state.session, url);
|
|
412
|
+
return true;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
cancelCheckout(state.session, url);
|
|
416
|
+
return true;
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
const handleWebViewNavigation = (navigation: Pick<WebViewNavigation, "url">) =>
|
|
420
|
+
handleReturnUrl(navigation.url);
|
|
421
|
+
|
|
422
|
+
const verifyCheckout = async (
|
|
423
|
+
input?: VerifyCheckoutInput,
|
|
424
|
+
options?: VerifyCheckoutOptions
|
|
425
|
+
) => {
|
|
426
|
+
const checkoutId = input?.checkoutId ?? state.session?.checkoutId;
|
|
427
|
+
|
|
428
|
+
if (!checkoutId) {
|
|
429
|
+
throw new CreemError(
|
|
430
|
+
"CHECKOUT_VERIFICATION_FAILED",
|
|
431
|
+
"No Creem checkout is available to verify yet."
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return runCheckoutVerification(checkoutId, options);
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
const dismiss = () => {
|
|
439
|
+
cancelCheckout(state.session ?? undefined);
|
|
440
|
+
};
|
|
441
|
+
|
|
442
|
+
const reset = () => {
|
|
443
|
+
clearActiveCheckoutContext();
|
|
444
|
+
clearVerification();
|
|
445
|
+
dispatch({ type: "reset" });
|
|
446
|
+
};
|
|
447
|
+
|
|
448
|
+
return {
|
|
449
|
+
status: state.status,
|
|
450
|
+
loading: state.status === "creating" || verificationStatus === "loading",
|
|
451
|
+
verifying: verificationStatus === "loading",
|
|
452
|
+
error: state.error,
|
|
453
|
+
session: state.session,
|
|
454
|
+
result: state.result,
|
|
455
|
+
verification,
|
|
456
|
+
verificationStatus,
|
|
457
|
+
verificationError,
|
|
458
|
+
webViewState: {
|
|
459
|
+
visible: state.webViewVisible,
|
|
460
|
+
checkoutUrl: state.session?.checkoutUrl ?? null,
|
|
461
|
+
returnUrl: state.webViewReturnUrl,
|
|
462
|
+
},
|
|
463
|
+
modalVisible: state.webViewVisible,
|
|
464
|
+
modalProps: {
|
|
465
|
+
visible: state.webViewVisible,
|
|
466
|
+
checkoutUrl: state.session?.checkoutUrl ?? null,
|
|
467
|
+
onRequestClose: dismiss,
|
|
468
|
+
onReturnUrl: handleReturnUrl,
|
|
469
|
+
},
|
|
470
|
+
startCheckout,
|
|
471
|
+
launchCheckout,
|
|
472
|
+
verifyCheckout,
|
|
473
|
+
handleReturnUrl,
|
|
474
|
+
handleWebViewNavigation,
|
|
475
|
+
dismiss,
|
|
476
|
+
reset,
|
|
477
|
+
};
|
|
478
|
+
};
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
import * as WebBrowser from "expo-web-browser";
|
|
3
|
+
import { CreemError } from "../../errors";
|
|
4
|
+
import { useCreemContext } from "../context";
|
|
5
|
+
import type {
|
|
6
|
+
CancelSubscriptionInput,
|
|
7
|
+
CustomerPortalResult,
|
|
8
|
+
PortalStatus,
|
|
9
|
+
SubscriptionActionName,
|
|
10
|
+
SubscriptionMutationStatus,
|
|
11
|
+
ResourceStatus,
|
|
12
|
+
PauseSubscriptionInput,
|
|
13
|
+
ResumeSubscriptionInput,
|
|
14
|
+
SubscriptionSnapshot,
|
|
15
|
+
UseCreemSubscriptionOptions,
|
|
16
|
+
UpgradeSubscriptionInput,
|
|
17
|
+
} from "../../types";
|
|
18
|
+
|
|
19
|
+
export const useCreemSubscription = (
|
|
20
|
+
options?: UseCreemSubscriptionOptions
|
|
21
|
+
) => {
|
|
22
|
+
const { apiClient, logger } = useCreemContext();
|
|
23
|
+
const [status, setStatus] = useState<ResourceStatus>("idle");
|
|
24
|
+
const [actionStatus, setActionStatus] =
|
|
25
|
+
useState<SubscriptionMutationStatus>("idle");
|
|
26
|
+
const [portalStatus, setPortalStatus] = useState<PortalStatus>("idle");
|
|
27
|
+
const [subscription, setSubscription] = useState<SubscriptionSnapshot | null>(null);
|
|
28
|
+
const [error, setError] = useState<Error | null>(null);
|
|
29
|
+
const [portal, setPortal] = useState<CustomerPortalResult | null>(null);
|
|
30
|
+
const [lastAction, setLastAction] = useState<SubscriptionActionName | null>(null);
|
|
31
|
+
|
|
32
|
+
const normalizeError = (
|
|
33
|
+
value: unknown,
|
|
34
|
+
code:
|
|
35
|
+
| "PORTAL_OPEN_FAILED"
|
|
36
|
+
| "SUBSCRIPTION_FETCH_FAILED"
|
|
37
|
+
| "SUBSCRIPTION_MUTATION_FAILED",
|
|
38
|
+
fallback: string
|
|
39
|
+
) => {
|
|
40
|
+
if (value instanceof CreemError) {
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (value instanceof Error) {
|
|
45
|
+
return new CreemError(code, value.message || fallback, {
|
|
46
|
+
cause: value,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return new CreemError(code, fallback, {
|
|
51
|
+
cause: value,
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const refresh = async () => {
|
|
56
|
+
try {
|
|
57
|
+
setStatus("loading");
|
|
58
|
+
setError(null);
|
|
59
|
+
setLastAction("refresh");
|
|
60
|
+
const nextSubscription = await apiClient.getSubscription();
|
|
61
|
+
setSubscription(nextSubscription);
|
|
62
|
+
setStatus("ready");
|
|
63
|
+
return nextSubscription;
|
|
64
|
+
} catch (refreshError) {
|
|
65
|
+
const normalizedError = normalizeError(
|
|
66
|
+
refreshError,
|
|
67
|
+
"SUBSCRIPTION_FETCH_FAILED",
|
|
68
|
+
"Failed to load Creem subscription."
|
|
69
|
+
);
|
|
70
|
+
setStatus("error");
|
|
71
|
+
setError(normalizedError);
|
|
72
|
+
logger?.error?.("creem subscription refresh failed", normalizedError);
|
|
73
|
+
throw normalizedError;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const runMutation = async <TInput>(
|
|
78
|
+
action: SubscriptionActionName,
|
|
79
|
+
operation: (input: TInput) => Promise<SubscriptionSnapshot>,
|
|
80
|
+
input: TInput,
|
|
81
|
+
fallbackMessage: string
|
|
82
|
+
) => {
|
|
83
|
+
try {
|
|
84
|
+
setActionStatus("loading");
|
|
85
|
+
setError(null);
|
|
86
|
+
setLastAction(action);
|
|
87
|
+
const nextSubscription = await operation(input);
|
|
88
|
+
setSubscription(nextSubscription);
|
|
89
|
+
setStatus("ready");
|
|
90
|
+
setActionStatus("ready");
|
|
91
|
+
return nextSubscription;
|
|
92
|
+
} catch (mutationError) {
|
|
93
|
+
const typedError = normalizeError(
|
|
94
|
+
mutationError,
|
|
95
|
+
"SUBSCRIPTION_MUTATION_FAILED",
|
|
96
|
+
fallbackMessage
|
|
97
|
+
);
|
|
98
|
+
setActionStatus("error");
|
|
99
|
+
setError(typedError);
|
|
100
|
+
logger?.error?.(`creem subscription ${action} failed`, typedError);
|
|
101
|
+
throw typedError;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
if (options?.enabled === false || options?.autoFetch === false) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
refresh().catch(() => {
|
|
111
|
+
// The hook already stores the error state.
|
|
112
|
+
});
|
|
113
|
+
}, [options?.autoFetch, options?.enabled]);
|
|
114
|
+
|
|
115
|
+
const openPortal = async () => {
|
|
116
|
+
try {
|
|
117
|
+
setPortalStatus("opening");
|
|
118
|
+
const nextPortal = await apiClient.createCustomerPortal();
|
|
119
|
+
setPortal(nextPortal);
|
|
120
|
+
const browserResult = await WebBrowser.openBrowserAsync(
|
|
121
|
+
nextPortal.customerPortalLink
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
if (browserResult.type === "cancel" || browserResult.type === "dismiss") {
|
|
125
|
+
setPortalStatus("cancelled");
|
|
126
|
+
return nextPortal;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
setPortalStatus("opened");
|
|
130
|
+
return nextPortal;
|
|
131
|
+
} catch (portalError) {
|
|
132
|
+
const normalizedError = normalizeError(
|
|
133
|
+
portalError,
|
|
134
|
+
"PORTAL_OPEN_FAILED",
|
|
135
|
+
"Failed to open Creem customer portal."
|
|
136
|
+
);
|
|
137
|
+
setPortalStatus("error");
|
|
138
|
+
setError(normalizedError);
|
|
139
|
+
logger?.error?.("creem portal open failed", normalizedError);
|
|
140
|
+
throw normalizedError;
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const cancel = (input: CancelSubscriptionInput = {}) =>
|
|
145
|
+
runMutation(
|
|
146
|
+
"cancel",
|
|
147
|
+
apiClient.cancelSubscription,
|
|
148
|
+
input,
|
|
149
|
+
"Failed to cancel the Creem subscription."
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
const pause = (input: PauseSubscriptionInput = {}) =>
|
|
153
|
+
runMutation(
|
|
154
|
+
"pause",
|
|
155
|
+
apiClient.pauseSubscription,
|
|
156
|
+
input,
|
|
157
|
+
"Failed to pause the Creem subscription."
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const resume = (input: ResumeSubscriptionInput = {}) =>
|
|
161
|
+
runMutation(
|
|
162
|
+
"resume",
|
|
163
|
+
apiClient.resumeSubscription,
|
|
164
|
+
input,
|
|
165
|
+
"Failed to resume the Creem subscription."
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
const upgrade = (input: UpgradeSubscriptionInput) =>
|
|
169
|
+
runMutation(
|
|
170
|
+
"upgrade",
|
|
171
|
+
apiClient.upgradeSubscription,
|
|
172
|
+
input,
|
|
173
|
+
"Failed to upgrade the Creem subscription."
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
status,
|
|
178
|
+
loading: status === "loading",
|
|
179
|
+
actionStatus,
|
|
180
|
+
portalStatus,
|
|
181
|
+
lastAction,
|
|
182
|
+
subscription,
|
|
183
|
+
portal,
|
|
184
|
+
error,
|
|
185
|
+
refresh,
|
|
186
|
+
getSubscription: refresh,
|
|
187
|
+
cancel,
|
|
188
|
+
cancelSubscription: cancel,
|
|
189
|
+
pause,
|
|
190
|
+
resume,
|
|
191
|
+
upgrade,
|
|
192
|
+
openPortal,
|
|
193
|
+
};
|
|
194
|
+
};
|