@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,99 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CheckoutFlowResult,
|
|
3
|
+
CheckoutSessionResult,
|
|
4
|
+
CheckoutStatus,
|
|
5
|
+
} from "../../types";
|
|
6
|
+
|
|
7
|
+
export type CheckoutState = {
|
|
8
|
+
status: CheckoutStatus;
|
|
9
|
+
error: Error | null;
|
|
10
|
+
session: CheckoutSessionResult | null;
|
|
11
|
+
result: CheckoutFlowResult | null;
|
|
12
|
+
webViewVisible: boolean;
|
|
13
|
+
webViewReturnUrl: string | null;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type CheckoutAction =
|
|
17
|
+
| { type: "start" }
|
|
18
|
+
| { type: "present"; session: CheckoutSessionResult; returnUrl: string }
|
|
19
|
+
| { type: "browser-session"; session: CheckoutSessionResult }
|
|
20
|
+
| { type: "success"; result: CheckoutFlowResult }
|
|
21
|
+
| { type: "cancelled"; session?: CheckoutSessionResult }
|
|
22
|
+
| { type: "error"; error: Error }
|
|
23
|
+
| { type: "reset" };
|
|
24
|
+
|
|
25
|
+
export const initialCheckoutState: CheckoutState = {
|
|
26
|
+
status: "idle",
|
|
27
|
+
error: null,
|
|
28
|
+
session: null,
|
|
29
|
+
result: null,
|
|
30
|
+
webViewVisible: false,
|
|
31
|
+
webViewReturnUrl: null,
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const checkoutReducer = (
|
|
35
|
+
state: CheckoutState,
|
|
36
|
+
action: CheckoutAction
|
|
37
|
+
): CheckoutState => {
|
|
38
|
+
switch (action.type) {
|
|
39
|
+
case "start":
|
|
40
|
+
return {
|
|
41
|
+
...state,
|
|
42
|
+
status: "creating",
|
|
43
|
+
error: null,
|
|
44
|
+
result: null,
|
|
45
|
+
webViewVisible: false,
|
|
46
|
+
webViewReturnUrl: null,
|
|
47
|
+
};
|
|
48
|
+
case "present":
|
|
49
|
+
return {
|
|
50
|
+
...state,
|
|
51
|
+
session: action.session,
|
|
52
|
+
status: "presented",
|
|
53
|
+
webViewVisible: true,
|
|
54
|
+
webViewReturnUrl: action.returnUrl,
|
|
55
|
+
};
|
|
56
|
+
case "browser-session":
|
|
57
|
+
return {
|
|
58
|
+
...state,
|
|
59
|
+
session: action.session,
|
|
60
|
+
status: "presented",
|
|
61
|
+
webViewVisible: false,
|
|
62
|
+
webViewReturnUrl: null,
|
|
63
|
+
};
|
|
64
|
+
case "success":
|
|
65
|
+
return {
|
|
66
|
+
...state,
|
|
67
|
+
status: "success",
|
|
68
|
+
error: null,
|
|
69
|
+
result: action.result,
|
|
70
|
+
session: action.result.session ?? null,
|
|
71
|
+
webViewVisible: false,
|
|
72
|
+
webViewReturnUrl: null,
|
|
73
|
+
};
|
|
74
|
+
case "cancelled":
|
|
75
|
+
return {
|
|
76
|
+
...state,
|
|
77
|
+
status: "cancelled",
|
|
78
|
+
result: {
|
|
79
|
+
type: "cancelled",
|
|
80
|
+
session: action.session ?? state.session ?? undefined,
|
|
81
|
+
},
|
|
82
|
+
session: action.session ?? state.session ?? null,
|
|
83
|
+
webViewVisible: false,
|
|
84
|
+
webViewReturnUrl: null,
|
|
85
|
+
};
|
|
86
|
+
case "error":
|
|
87
|
+
return {
|
|
88
|
+
...state,
|
|
89
|
+
status: "error",
|
|
90
|
+
error: action.error,
|
|
91
|
+
webViewVisible: false,
|
|
92
|
+
webViewReturnUrl: null,
|
|
93
|
+
};
|
|
94
|
+
case "reset":
|
|
95
|
+
return initialCheckoutState;
|
|
96
|
+
default:
|
|
97
|
+
return state;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import * as Linking from "expo-linking";
|
|
2
|
+
import type {
|
|
3
|
+
CheckoutRedirectStatus,
|
|
4
|
+
CheckoutReturnParams,
|
|
5
|
+
CheckoutSessionResult,
|
|
6
|
+
CheckoutVerificationResult,
|
|
7
|
+
CreateCreemRedirectOptions,
|
|
8
|
+
CreemRedirectUrls,
|
|
9
|
+
JsonValue,
|
|
10
|
+
} from "../../types";
|
|
11
|
+
|
|
12
|
+
const STATUS_QUERY_KEYS = ["status", "checkout_status", "creem_status"];
|
|
13
|
+
const SUCCESS_QUERY_VALUES = new Set(["complete", "completed", "paid", "success"]);
|
|
14
|
+
const CANCEL_QUERY_VALUES = new Set([
|
|
15
|
+
"abort",
|
|
16
|
+
"abandoned",
|
|
17
|
+
"cancel",
|
|
18
|
+
"cancelled",
|
|
19
|
+
"canceled",
|
|
20
|
+
"dismiss",
|
|
21
|
+
"dismissed",
|
|
22
|
+
"failed",
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
const normalizePath = (path?: string | null) => {
|
|
26
|
+
if (!path) {
|
|
27
|
+
return "";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const trimmed = path.replace(/^\/+|\/+$/g, "");
|
|
31
|
+
return trimmed ? `/${trimmed}` : "";
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const normalizeUrl = (rawUrl: string) => {
|
|
35
|
+
try {
|
|
36
|
+
const parsed = Linking.parse(rawUrl);
|
|
37
|
+
const scheme = parsed.scheme ? `${parsed.scheme.toLowerCase()}://` : "";
|
|
38
|
+
const host = parsed.hostname ? parsed.hostname.toLowerCase() : "";
|
|
39
|
+
const path = normalizePath(parsed.path);
|
|
40
|
+
|
|
41
|
+
return `${scheme}${host}${path}`;
|
|
42
|
+
} catch {
|
|
43
|
+
return "";
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const toStringValue = (
|
|
48
|
+
value: JsonValue | string | string[] | undefined
|
|
49
|
+
): string | undefined => {
|
|
50
|
+
if (Array.isArray(value)) {
|
|
51
|
+
return typeof value[0] === "string" ? value[0] : undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return typeof value === "string" ? value : value == null ? undefined : String(value);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const stringifyMetadataValue = (value: JsonValue) => {
|
|
58
|
+
if (
|
|
59
|
+
value === null ||
|
|
60
|
+
typeof value === "string" ||
|
|
61
|
+
typeof value === "number" ||
|
|
62
|
+
typeof value === "boolean"
|
|
63
|
+
) {
|
|
64
|
+
return value === null ? "null" : String(value);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return JSON.stringify(value);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const readStatusFromQuery = (
|
|
71
|
+
url: string
|
|
72
|
+
): CheckoutRedirectStatus | undefined => {
|
|
73
|
+
const parsed = Linking.parse(url);
|
|
74
|
+
const query = parsed.queryParams ?? {};
|
|
75
|
+
|
|
76
|
+
for (const key of STATUS_QUERY_KEYS) {
|
|
77
|
+
const rawValue = toStringValue(query[key]);
|
|
78
|
+
|
|
79
|
+
if (!rawValue) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const normalized = rawValue.toLowerCase();
|
|
84
|
+
|
|
85
|
+
if (SUCCESS_QUERY_VALUES.has(normalized)) {
|
|
86
|
+
return "success";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (CANCEL_QUERY_VALUES.has(normalized)) {
|
|
90
|
+
return "cancelled";
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return undefined;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const setSearchParam = (rawUrl: string, key: string, value: string) => {
|
|
98
|
+
try {
|
|
99
|
+
const parsed = new URL(rawUrl);
|
|
100
|
+
parsed.searchParams.set(key, value);
|
|
101
|
+
return parsed.toString();
|
|
102
|
+
} catch {
|
|
103
|
+
const [base, existingQuery = ""] = rawUrl.split("?");
|
|
104
|
+
const params = new URLSearchParams(existingQuery);
|
|
105
|
+
params.set(key, value);
|
|
106
|
+
return `${base}?${params.toString()}`;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export const __internal = {
|
|
111
|
+
normalizePath,
|
|
112
|
+
normalizeUrl,
|
|
113
|
+
toStringValue,
|
|
114
|
+
stringifyMetadataValue,
|
|
115
|
+
readStatusFromQuery,
|
|
116
|
+
setSearchParam,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export const buildReturnUrl = (
|
|
120
|
+
returnPath: string,
|
|
121
|
+
createUrl: (path: string) => string
|
|
122
|
+
) => createUrl(returnPath);
|
|
123
|
+
|
|
124
|
+
export const buildCheckoutUrlFromSessionId = (
|
|
125
|
+
sessionId: string,
|
|
126
|
+
checkoutBaseUrl = "https://www.creem.io/checkout"
|
|
127
|
+
) => {
|
|
128
|
+
const parsed = new URL(checkoutBaseUrl);
|
|
129
|
+
parsed.searchParams.set("session_id", sessionId);
|
|
130
|
+
return parsed.toString();
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export const createCreemRedirectUrls = (
|
|
134
|
+
scheme: string,
|
|
135
|
+
options: CreateCreemRedirectOptions = {}
|
|
136
|
+
): CreemRedirectUrls => {
|
|
137
|
+
const successPath = options.successPath ?? "creem/callback";
|
|
138
|
+
const cancelPath = options.cancelPath ?? "creem/cancel";
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
successUrl: Linking.createURL(successPath, { scheme }),
|
|
142
|
+
cancelUrl: Linking.createURL(cancelPath, { scheme }),
|
|
143
|
+
successPath,
|
|
144
|
+
cancelPath,
|
|
145
|
+
};
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export const matchesReturnUrl = (candidateUrl: string, returnUrl: string) => {
|
|
149
|
+
const candidateNormalized = normalizeUrl(candidateUrl);
|
|
150
|
+
const returnNormalized = normalizeUrl(returnUrl);
|
|
151
|
+
|
|
152
|
+
if (!candidateNormalized || !returnNormalized) {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
candidateNormalized === returnNormalized ||
|
|
158
|
+
candidateNormalized.startsWith(`${returnNormalized}/`)
|
|
159
|
+
);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
export const detectCheckoutStatusFromUrl = (
|
|
163
|
+
url: string,
|
|
164
|
+
successUrl?: string,
|
|
165
|
+
cancelUrl?: string
|
|
166
|
+
): CheckoutRedirectStatus | undefined => {
|
|
167
|
+
const queryStatus = readStatusFromQuery(url);
|
|
168
|
+
|
|
169
|
+
if (queryStatus) {
|
|
170
|
+
return queryStatus;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (successUrl && matchesReturnUrl(url, successUrl)) {
|
|
174
|
+
return "success";
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (cancelUrl && matchesReturnUrl(url, cancelUrl)) {
|
|
178
|
+
return "cancelled";
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return undefined;
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
export const parseCheckoutReturnUrl = (url: string): CheckoutReturnParams => {
|
|
185
|
+
const parsed = new URL(url);
|
|
186
|
+
const entries = Object.fromEntries(parsed.searchParams.entries());
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
...entries,
|
|
190
|
+
checkoutId: entries.checkout_id ?? entries.checkoutId,
|
|
191
|
+
orderId: entries.order_id ?? entries.orderId,
|
|
192
|
+
customerId: entries.customer_id ?? entries.customerId,
|
|
193
|
+
subscriptionId: entries.subscription_id ?? entries.subscriptionId,
|
|
194
|
+
productId: entries.product_id ?? entries.productId,
|
|
195
|
+
signature: entries.signature,
|
|
196
|
+
};
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
export const appendMetadataToUrl = (
|
|
200
|
+
url: string,
|
|
201
|
+
metadata?: Record<string, JsonValue>
|
|
202
|
+
) => {
|
|
203
|
+
if (!metadata || Object.keys(metadata).length === 0) {
|
|
204
|
+
return url;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return Object.entries(metadata).reduce(
|
|
208
|
+
(nextUrl, [key, value]) =>
|
|
209
|
+
setSearchParam(nextUrl, `meta_${key}`, stringifyMetadataValue(value)),
|
|
210
|
+
url
|
|
211
|
+
);
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
export const buildSuccessResult = (
|
|
215
|
+
session: CheckoutSessionResult,
|
|
216
|
+
returnUrl: string,
|
|
217
|
+
verification?: CheckoutVerificationResult | null
|
|
218
|
+
) => ({
|
|
219
|
+
type: "success" as const,
|
|
220
|
+
session,
|
|
221
|
+
returnUrl,
|
|
222
|
+
params: parseCheckoutReturnUrl(returnUrl),
|
|
223
|
+
verification: verification ?? null,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
export const getPrimaryReturnId = (params: CheckoutReturnParams) =>
|
|
227
|
+
params.subscriptionId ?? params.customerId ?? params.checkoutId;
|
|
228
|
+
|
|
229
|
+
export const getReturnSignature = (url: string) => {
|
|
230
|
+
const parsed = new URL(url);
|
|
231
|
+
return toStringValue(parsed.searchParams.get("signature") ?? undefined);
|
|
232
|
+
};
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export type CreemErrorCode =
|
|
2
|
+
| "CHECKOUT_CANCELLED"
|
|
3
|
+
| "CHECKOUT_LAUNCH_FAILED"
|
|
4
|
+
| "CHECKOUT_VERIFICATION_FAILED"
|
|
5
|
+
| "INVALID_CHECKOUT_OPTIONS"
|
|
6
|
+
| "PORTAL_OPEN_FAILED"
|
|
7
|
+
| "SUBSCRIPTION_FETCH_FAILED"
|
|
8
|
+
| "SUBSCRIPTION_MUTATION_FAILED";
|
|
9
|
+
|
|
10
|
+
type CreemErrorOptions = {
|
|
11
|
+
cause?: unknown;
|
|
12
|
+
details?: unknown;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export class CreemError extends Error {
|
|
16
|
+
readonly code: CreemErrorCode;
|
|
17
|
+
readonly cause?: unknown;
|
|
18
|
+
readonly details?: unknown;
|
|
19
|
+
|
|
20
|
+
constructor(
|
|
21
|
+
code: CreemErrorCode,
|
|
22
|
+
message: string,
|
|
23
|
+
options: CreemErrorOptions = {}
|
|
24
|
+
) {
|
|
25
|
+
super(message);
|
|
26
|
+
this.name = "CreemError";
|
|
27
|
+
this.code = code;
|
|
28
|
+
this.cause = options.cause;
|
|
29
|
+
this.details = options.details;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type CreemApiErrorOptions = {
|
|
34
|
+
status: number;
|
|
35
|
+
message: string;
|
|
36
|
+
code?: string | null;
|
|
37
|
+
details?: unknown;
|
|
38
|
+
responseBody?: string | null;
|
|
39
|
+
url?: string;
|
|
40
|
+
cause?: unknown;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export class CreemApiError extends Error {
|
|
44
|
+
readonly status: number;
|
|
45
|
+
readonly code?: string | null;
|
|
46
|
+
readonly details?: unknown;
|
|
47
|
+
readonly responseBody?: string | null;
|
|
48
|
+
readonly url?: string;
|
|
49
|
+
readonly cause?: unknown;
|
|
50
|
+
|
|
51
|
+
constructor(options: CreemApiErrorOptions) {
|
|
52
|
+
super(options.message);
|
|
53
|
+
this.name = "CreemApiError";
|
|
54
|
+
this.status = options.status;
|
|
55
|
+
this.code = options.code;
|
|
56
|
+
this.details = options.details;
|
|
57
|
+
this.responseBody = options.responseBody;
|
|
58
|
+
this.url = options.url;
|
|
59
|
+
this.cause = options.cause;
|
|
60
|
+
}
|
|
61
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export * from "./types";
|
|
2
|
+
export { CreemApiError, CreemError } from "./errors";
|
|
3
|
+
export { createCreemApiClient } from "./client/apiClient";
|
|
4
|
+
export { CreemProvider, useCreemContext } from "./client/context";
|
|
5
|
+
export { useCreemCheckout } from "./client/hooks/useCreemCheckout";
|
|
6
|
+
export { useCreemSubscription } from "./client/hooks/useCreemSubscription";
|
|
7
|
+
export { CreemCheckoutButton } from "./client/components/CreemCheckoutButton";
|
|
8
|
+
export { CreemCheckoutModal } from "./client/components/CreemCheckoutModal";
|
|
9
|
+
export { CreemManageSubscriptionButton } from "./client/components/CreemManageSubscriptionButton";
|
|
10
|
+
export { useCreemSubscription as useSubscription } from "./client/hooks/useCreemSubscription";
|
|
11
|
+
export { CreemCheckoutModal as CreemCheckoutSheet } from "./client/components/CreemCheckoutModal";
|
|
12
|
+
export {
|
|
13
|
+
appendMetadataToUrl,
|
|
14
|
+
buildCheckoutUrlFromSessionId,
|
|
15
|
+
createCreemRedirectUrls,
|
|
16
|
+
detectCheckoutStatusFromUrl,
|
|
17
|
+
matchesReturnUrl,
|
|
18
|
+
parseCheckoutReturnUrl,
|
|
19
|
+
} from "./client/utils/linking";
|