@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,857 @@
|
|
|
1
|
+
// src/server/express.ts
|
|
2
|
+
import express, {
|
|
3
|
+
Router
|
|
4
|
+
} from "express";
|
|
5
|
+
|
|
6
|
+
// src/server/json.ts
|
|
7
|
+
var CreemHttpError = class extends Error {
|
|
8
|
+
status;
|
|
9
|
+
details;
|
|
10
|
+
constructor(status, message, details) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.status = status;
|
|
13
|
+
this.details = details;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
var json = (body, init) => new Response(JSON.stringify(body), {
|
|
17
|
+
...init,
|
|
18
|
+
headers: {
|
|
19
|
+
"content-type": "application/json; charset=utf-8",
|
|
20
|
+
...init?.headers ?? {}
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
var errorResponse = (error) => {
|
|
24
|
+
if (error instanceof CreemHttpError) {
|
|
25
|
+
return json(
|
|
26
|
+
{
|
|
27
|
+
error: {
|
|
28
|
+
message: error.message,
|
|
29
|
+
details: error.details ?? null
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
{ status: error.status }
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
return json(
|
|
36
|
+
{
|
|
37
|
+
error: {
|
|
38
|
+
message: error instanceof Error ? error.message : "An unexpected Creem server error occurred."
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
{ status: 500 }
|
|
42
|
+
);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// src/server/signatures.ts
|
|
46
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
47
|
+
var verifyWebhookSignature = (rawBody, signature, secret) => {
|
|
48
|
+
const computedSignature = createHmac("sha256", secret).update(rawBody).digest("hex");
|
|
49
|
+
const providedBuffer = Buffer.from(signature, "hex");
|
|
50
|
+
const computedBuffer = Buffer.from(computedSignature, "hex");
|
|
51
|
+
if (providedBuffer.length !== computedBuffer.length) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
return timingSafeEqual(providedBuffer, computedBuffer);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// src/server/core.ts
|
|
58
|
+
var KNOWN_WEBHOOK_HANDLER_KEYS = {
|
|
59
|
+
"checkout.completed": "onCheckoutCompleted",
|
|
60
|
+
"dispute.created": "onDisputeCreated",
|
|
61
|
+
"refund.created": "onRefundCreated",
|
|
62
|
+
"subscription.active": "onSubscriptionActive",
|
|
63
|
+
"subscription.canceled": "onSubscriptionCanceled",
|
|
64
|
+
"subscription.expired": "onSubscriptionExpired",
|
|
65
|
+
"subscription.paid": "onSubscriptionPaid",
|
|
66
|
+
"subscription.past_due": "onSubscriptionPastDue",
|
|
67
|
+
"subscription.paused": "onSubscriptionPaused",
|
|
68
|
+
"subscription.trialing": "onSubscriptionTrialing",
|
|
69
|
+
"subscription.unpaid": "onSubscriptionUnpaid",
|
|
70
|
+
"subscription.update": "onSubscriptionUpdate"
|
|
71
|
+
};
|
|
72
|
+
var CREEM_USER_ID_KEY = "creemExpoUserId";
|
|
73
|
+
var nowIso = (clock) => (clock ? clock() : /* @__PURE__ */ new Date()).toISOString();
|
|
74
|
+
var asRecord = (value) => typeof value === "object" && value !== null ? value : {};
|
|
75
|
+
var asOptionalJsonObject = (value) => typeof value === "object" && value !== null ? value : void 0;
|
|
76
|
+
var extractId = (value) => {
|
|
77
|
+
if (typeof value === "string") {
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
if (value && typeof value === "object" && "id" in value) {
|
|
81
|
+
return typeof value.id === "string" ? value.id : void 0;
|
|
82
|
+
}
|
|
83
|
+
return void 0;
|
|
84
|
+
};
|
|
85
|
+
var extractCustomerEmail = (customer) => {
|
|
86
|
+
if (!customer || typeof customer !== "object" || !("email" in customer)) {
|
|
87
|
+
return void 0;
|
|
88
|
+
}
|
|
89
|
+
return typeof customer.email === "string" ? customer.email : void 0;
|
|
90
|
+
};
|
|
91
|
+
var mergeMetadata = (metadata, identity) => ({
|
|
92
|
+
...metadata ?? {},
|
|
93
|
+
[CREEM_USER_ID_KEY]: identity.userId
|
|
94
|
+
});
|
|
95
|
+
var isHttpUrl = (value) => {
|
|
96
|
+
if (!value) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
const protocol = new URL(value).protocol;
|
|
101
|
+
return protocol === "http:" || protocol === "https:";
|
|
102
|
+
} catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
var mapCheckoutEntity = (checkout) => ({
|
|
107
|
+
checkoutId: checkout.id,
|
|
108
|
+
checkoutUrl: checkout.checkoutUrl ?? "",
|
|
109
|
+
productId: extractId(checkout.product),
|
|
110
|
+
requestId: checkout.requestId,
|
|
111
|
+
successUrl: checkout.successUrl ?? null,
|
|
112
|
+
status: checkout.status,
|
|
113
|
+
mode: checkout.mode,
|
|
114
|
+
customerId: extractId(checkout.customer) ?? null,
|
|
115
|
+
subscriptionId: extractId(checkout.subscription) ?? null,
|
|
116
|
+
rawCheckout: checkout
|
|
117
|
+
});
|
|
118
|
+
var mapPortalEntity = (portal, customerId) => ({
|
|
119
|
+
customerId,
|
|
120
|
+
customerPortalLink: portal.customerPortalLink,
|
|
121
|
+
rawPortal: portal
|
|
122
|
+
});
|
|
123
|
+
var isEntitledFromStatus = (status) => status === "active" || status === "trialing" || status === "paid" || status === "scheduled_cancel";
|
|
124
|
+
var isVerifiedCheckoutStatus = (status) => status === "complete" || status === "completed" || status === "paid" || status === "success" || status === "succeeded";
|
|
125
|
+
var buildCheckoutVerification = (checkoutId, checkout, userState) => {
|
|
126
|
+
const matchesLatestCheckout = userState?.latestCheckoutId === checkoutId;
|
|
127
|
+
const source = checkout ? "checkout" : matchesLatestCheckout ? "subscription" : "none";
|
|
128
|
+
const verified = isVerifiedCheckoutStatus(checkout?.status) || Boolean(
|
|
129
|
+
(matchesLatestCheckout || checkout) && (checkout?.orderId ?? checkout?.subscriptionId ?? userState?.latestOrderId ?? userState?.subscriptionId ?? (userState?.entitlementActive ? "entitled" : null))
|
|
130
|
+
);
|
|
131
|
+
return {
|
|
132
|
+
checkoutId,
|
|
133
|
+
verified,
|
|
134
|
+
source,
|
|
135
|
+
checkoutStatus: checkout?.status ?? null,
|
|
136
|
+
customerId: checkout?.customerId ?? userState?.customerId ?? null,
|
|
137
|
+
subscriptionId: checkout?.subscriptionId ?? userState?.subscriptionId ?? null,
|
|
138
|
+
orderId: checkout?.orderId ?? userState?.latestOrderId ?? null,
|
|
139
|
+
productId: checkout?.productId ?? userState?.latestProductId ?? null,
|
|
140
|
+
entitlementActive: userState?.entitlementActive ?? false,
|
|
141
|
+
subscriptionStatus: userState?.subscriptionStatus ?? null,
|
|
142
|
+
updatedAt: checkout?.updatedAt ?? userState?.updatedAt ?? null
|
|
143
|
+
};
|
|
144
|
+
};
|
|
145
|
+
var normalizeWebhookEvent = (payload) => {
|
|
146
|
+
const record = asRecord(payload);
|
|
147
|
+
const object = asRecord(record.object);
|
|
148
|
+
if (typeof record.id !== "string" || typeof record.eventType !== "string") {
|
|
149
|
+
throw new CreemHttpError(400, "Creem webhook payload is missing required fields.");
|
|
150
|
+
}
|
|
151
|
+
const createdAt = typeof record.created_at === "number" ? record.created_at : typeof record.createdAt === "number" ? record.createdAt : Date.now();
|
|
152
|
+
return {
|
|
153
|
+
id: record.id,
|
|
154
|
+
eventType: record.eventType,
|
|
155
|
+
createdAt,
|
|
156
|
+
object
|
|
157
|
+
};
|
|
158
|
+
};
|
|
159
|
+
var buildUserState = (currentState, patch, clock) => ({
|
|
160
|
+
userId: patch.userId ?? currentState?.userId ?? "",
|
|
161
|
+
email: patch.email ?? currentState?.email ?? null,
|
|
162
|
+
customerId: patch.customerId ?? currentState?.customerId ?? null,
|
|
163
|
+
subscriptionId: patch.subscriptionId ?? currentState?.subscriptionId ?? null,
|
|
164
|
+
latestCheckoutId: patch.latestCheckoutId ?? currentState?.latestCheckoutId ?? null,
|
|
165
|
+
latestOrderId: patch.latestOrderId ?? currentState?.latestOrderId ?? null,
|
|
166
|
+
latestProductId: patch.latestProductId ?? currentState?.latestProductId ?? null,
|
|
167
|
+
entitlementActive: patch.entitlementActive ?? currentState?.entitlementActive ?? false,
|
|
168
|
+
subscriptionStatus: patch.subscriptionStatus ?? currentState?.subscriptionStatus ?? null,
|
|
169
|
+
metadata: patch.metadata ?? currentState?.metadata ?? null,
|
|
170
|
+
subscription: patch.subscription ?? currentState?.subscription ?? null,
|
|
171
|
+
updatedAt: nowIso(clock)
|
|
172
|
+
});
|
|
173
|
+
var buildCheckoutRecord = (session, userId, input, clock) => {
|
|
174
|
+
const timestamp = nowIso(clock);
|
|
175
|
+
return {
|
|
176
|
+
checkoutId: session.checkoutId,
|
|
177
|
+
userId,
|
|
178
|
+
productId: session.productId ?? input.productId,
|
|
179
|
+
requestId: session.requestId ?? input.requestId,
|
|
180
|
+
status: session.status,
|
|
181
|
+
checkoutUrl: session.checkoutUrl,
|
|
182
|
+
successUrl: session.successUrl ?? input.returnUrl ?? null,
|
|
183
|
+
customerId: session.customerId ?? input.customer?.id ?? null,
|
|
184
|
+
subscriptionId: session.subscriptionId ?? null,
|
|
185
|
+
orderId: null,
|
|
186
|
+
metadata: input.metadata ?? null,
|
|
187
|
+
createdAt: timestamp,
|
|
188
|
+
updatedAt: timestamp
|
|
189
|
+
};
|
|
190
|
+
};
|
|
191
|
+
var resolveIdentityFromWebhook = async (store, event) => {
|
|
192
|
+
const metadata = asRecord(event.object.metadata);
|
|
193
|
+
const explicitUserId = metadata[CREEM_USER_ID_KEY];
|
|
194
|
+
if (typeof explicitUserId === "string") {
|
|
195
|
+
return store.getUserState(explicitUserId);
|
|
196
|
+
}
|
|
197
|
+
const subscriptionId = extractId(event.object.subscription);
|
|
198
|
+
if (subscriptionId) {
|
|
199
|
+
const fromSubscription = await store.getUserStateBySubscriptionId(subscriptionId);
|
|
200
|
+
if (fromSubscription) {
|
|
201
|
+
return fromSubscription;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const customerId = extractId(event.object.customer);
|
|
205
|
+
if (customerId) {
|
|
206
|
+
const fromCustomer = await store.getUserStateByCustomerId(customerId);
|
|
207
|
+
if (fromCustomer) {
|
|
208
|
+
return fromCustomer;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const checkoutId = extractId(event.object);
|
|
212
|
+
if (checkoutId) {
|
|
213
|
+
const checkout = await store.getCheckout(checkoutId);
|
|
214
|
+
if (checkout) {
|
|
215
|
+
return store.getUserState(checkout.userId);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return null;
|
|
219
|
+
};
|
|
220
|
+
var buildSubscriptionSnapshot = (userId, state, subscription) => ({
|
|
221
|
+
userId,
|
|
222
|
+
email: state?.email ?? null,
|
|
223
|
+
customerId: state?.customerId ?? null,
|
|
224
|
+
subscriptionId: state?.subscriptionId ?? null,
|
|
225
|
+
latestCheckoutId: state?.latestCheckoutId ?? null,
|
|
226
|
+
latestOrderId: state?.latestOrderId ?? null,
|
|
227
|
+
latestProductId: state?.latestProductId ?? null,
|
|
228
|
+
entitlementActive: state?.entitlementActive ?? false,
|
|
229
|
+
subscriptionStatus: subscription?.status ?? state?.subscriptionStatus ?? null,
|
|
230
|
+
source: subscription ? "creem" : state ? "store" : "none",
|
|
231
|
+
subscription: subscription ?? state?.subscription ?? null,
|
|
232
|
+
metadata: state?.metadata ?? null,
|
|
233
|
+
updatedAt: state?.updatedAt ?? null
|
|
234
|
+
});
|
|
235
|
+
var buildSubscriptionStatePatch = ({
|
|
236
|
+
userState,
|
|
237
|
+
identity,
|
|
238
|
+
subscription,
|
|
239
|
+
fallbackProductId
|
|
240
|
+
}) => ({
|
|
241
|
+
userId: identity.userId,
|
|
242
|
+
email: identity.email ?? userState?.email ?? null,
|
|
243
|
+
customerId: extractId(subscription.customer) ?? userState?.customerId ?? identity.customerId ?? null,
|
|
244
|
+
subscriptionId: subscription.id,
|
|
245
|
+
latestProductId: extractId(subscription.product) ?? fallbackProductId ?? userState?.latestProductId ?? null,
|
|
246
|
+
subscriptionStatus: subscription.status,
|
|
247
|
+
entitlementActive: isEntitledFromStatus(subscription.status),
|
|
248
|
+
subscription
|
|
249
|
+
});
|
|
250
|
+
var createCreemServerCore = ({
|
|
251
|
+
creem,
|
|
252
|
+
store,
|
|
253
|
+
webhookSecret,
|
|
254
|
+
resolveIdentity,
|
|
255
|
+
buildSuccessUrl,
|
|
256
|
+
onWebhookEvent,
|
|
257
|
+
webhookHandlers,
|
|
258
|
+
now,
|
|
259
|
+
...namedWebhookHandlers
|
|
260
|
+
}) => {
|
|
261
|
+
const resolveRequestIdentity = async ({
|
|
262
|
+
request,
|
|
263
|
+
platformContext
|
|
264
|
+
}) => resolveIdentity({
|
|
265
|
+
request,
|
|
266
|
+
platformContext
|
|
267
|
+
});
|
|
268
|
+
const dispatchWebhookHandlers = async (context) => {
|
|
269
|
+
const handlerFromMap = webhookHandlers?.[context.event.eventType];
|
|
270
|
+
if (handlerFromMap) {
|
|
271
|
+
await handlerFromMap(context);
|
|
272
|
+
}
|
|
273
|
+
const handlerKey = KNOWN_WEBHOOK_HANDLER_KEYS[context.event.eventType];
|
|
274
|
+
const namedHandler = handlerKey ? namedWebhookHandlers[handlerKey] : void 0;
|
|
275
|
+
if (namedHandler) {
|
|
276
|
+
await namedHandler(context);
|
|
277
|
+
}
|
|
278
|
+
if (onWebhookEvent) {
|
|
279
|
+
await onWebhookEvent(context);
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
return {
|
|
283
|
+
async createCheckoutSession({
|
|
284
|
+
request,
|
|
285
|
+
input,
|
|
286
|
+
platformContext
|
|
287
|
+
}) {
|
|
288
|
+
const identity = await resolveRequestIdentity({ request, platformContext });
|
|
289
|
+
const userState = await store.getUserState(identity.userId);
|
|
290
|
+
const successUrl = isHttpUrl(input.returnUrl) ? input.returnUrl : await buildSuccessUrl({
|
|
291
|
+
request,
|
|
292
|
+
platformContext,
|
|
293
|
+
identity,
|
|
294
|
+
input,
|
|
295
|
+
userState
|
|
296
|
+
});
|
|
297
|
+
const checkout = await creem.checkouts.create({
|
|
298
|
+
productId: input.productId,
|
|
299
|
+
requestId: input.requestId,
|
|
300
|
+
units: input.units,
|
|
301
|
+
discountCode: input.discountCode,
|
|
302
|
+
customer: {
|
|
303
|
+
id: input.customer?.id ?? userState?.customerId ?? identity.customerId ?? void 0,
|
|
304
|
+
email: input.customer?.email ?? identity.email ?? userState?.email ?? void 0
|
|
305
|
+
},
|
|
306
|
+
customFields: input.customFields,
|
|
307
|
+
successUrl,
|
|
308
|
+
metadata: mergeMetadata(input.metadata, identity)
|
|
309
|
+
});
|
|
310
|
+
const session = mapCheckoutEntity(checkout);
|
|
311
|
+
await store.saveCheckout(buildCheckoutRecord(session, identity.userId, input, now));
|
|
312
|
+
const nextState = buildUserState(
|
|
313
|
+
userState,
|
|
314
|
+
{
|
|
315
|
+
userId: identity.userId,
|
|
316
|
+
email: input.customer?.email ?? identity.email ?? userState?.email ?? null,
|
|
317
|
+
customerId: session.customerId ?? userState?.customerId ?? identity.customerId ?? null,
|
|
318
|
+
latestCheckoutId: session.checkoutId,
|
|
319
|
+
latestProductId: session.productId ?? input.productId,
|
|
320
|
+
metadata: mergeMetadata(input.metadata, identity)
|
|
321
|
+
},
|
|
322
|
+
now
|
|
323
|
+
);
|
|
324
|
+
await store.saveUserState(nextState);
|
|
325
|
+
return session;
|
|
326
|
+
},
|
|
327
|
+
async getSubscriptionSnapshot({
|
|
328
|
+
request,
|
|
329
|
+
platformContext
|
|
330
|
+
}) {
|
|
331
|
+
const identity = await resolveRequestIdentity({ request, platformContext });
|
|
332
|
+
const userState = await store.getUserState(identity.userId);
|
|
333
|
+
if (!userState?.subscriptionId) {
|
|
334
|
+
return buildSubscriptionSnapshot(identity.userId, userState, null);
|
|
335
|
+
}
|
|
336
|
+
try {
|
|
337
|
+
const subscription = await creem.subscriptions.get(userState.subscriptionId);
|
|
338
|
+
const nextState = buildUserState(
|
|
339
|
+
userState,
|
|
340
|
+
buildSubscriptionStatePatch({
|
|
341
|
+
userState,
|
|
342
|
+
identity,
|
|
343
|
+
subscription
|
|
344
|
+
}),
|
|
345
|
+
now
|
|
346
|
+
);
|
|
347
|
+
await store.saveUserState(nextState);
|
|
348
|
+
return buildSubscriptionSnapshot(identity.userId, nextState, subscription);
|
|
349
|
+
} catch {
|
|
350
|
+
return buildSubscriptionSnapshot(identity.userId, userState, null);
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
async verifyCheckoutSession({
|
|
354
|
+
request,
|
|
355
|
+
input,
|
|
356
|
+
platformContext
|
|
357
|
+
}) {
|
|
358
|
+
const identity = await resolveRequestIdentity({ request, platformContext });
|
|
359
|
+
const userState = await store.getUserState(identity.userId);
|
|
360
|
+
const checkout = await store.getCheckout(input.checkoutId);
|
|
361
|
+
const checkoutForUser = checkout && checkout.userId === identity.userId ? checkout : null;
|
|
362
|
+
return buildCheckoutVerification(input.checkoutId, checkoutForUser, userState);
|
|
363
|
+
},
|
|
364
|
+
async cancelSubscription({
|
|
365
|
+
request,
|
|
366
|
+
input,
|
|
367
|
+
platformContext
|
|
368
|
+
}) {
|
|
369
|
+
const identity = await resolveRequestIdentity({ request, platformContext });
|
|
370
|
+
const userState = await store.getUserState(identity.userId);
|
|
371
|
+
const subscriptionId = input.subscriptionId ?? userState?.subscriptionId;
|
|
372
|
+
if (!subscriptionId) {
|
|
373
|
+
throw new CreemHttpError(
|
|
374
|
+
400,
|
|
375
|
+
"No Creem subscription is linked to the current user yet."
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
const subscription = await creem.subscriptions.cancel(subscriptionId, {
|
|
379
|
+
mode: input.mode,
|
|
380
|
+
onExecute: input.onExecute
|
|
381
|
+
});
|
|
382
|
+
const nextState = buildUserState(
|
|
383
|
+
userState,
|
|
384
|
+
buildSubscriptionStatePatch({
|
|
385
|
+
userState,
|
|
386
|
+
identity,
|
|
387
|
+
subscription
|
|
388
|
+
}),
|
|
389
|
+
now
|
|
390
|
+
);
|
|
391
|
+
await store.saveUserState(nextState);
|
|
392
|
+
return buildSubscriptionSnapshot(identity.userId, nextState, subscription);
|
|
393
|
+
},
|
|
394
|
+
async pauseSubscription({
|
|
395
|
+
request,
|
|
396
|
+
input,
|
|
397
|
+
platformContext
|
|
398
|
+
}) {
|
|
399
|
+
const identity = await resolveRequestIdentity({ request, platformContext });
|
|
400
|
+
const userState = await store.getUserState(identity.userId);
|
|
401
|
+
const subscriptionId = input.subscriptionId ?? userState?.subscriptionId;
|
|
402
|
+
if (!subscriptionId) {
|
|
403
|
+
throw new CreemHttpError(
|
|
404
|
+
400,
|
|
405
|
+
"No Creem subscription is linked to the current user yet."
|
|
406
|
+
);
|
|
407
|
+
}
|
|
408
|
+
const subscription = await creem.subscriptions.pause(subscriptionId);
|
|
409
|
+
const nextState = buildUserState(
|
|
410
|
+
userState,
|
|
411
|
+
buildSubscriptionStatePatch({
|
|
412
|
+
userState,
|
|
413
|
+
identity,
|
|
414
|
+
subscription
|
|
415
|
+
}),
|
|
416
|
+
now
|
|
417
|
+
);
|
|
418
|
+
await store.saveUserState(nextState);
|
|
419
|
+
return buildSubscriptionSnapshot(identity.userId, nextState, subscription);
|
|
420
|
+
},
|
|
421
|
+
async resumeSubscription({
|
|
422
|
+
request,
|
|
423
|
+
input,
|
|
424
|
+
platformContext
|
|
425
|
+
}) {
|
|
426
|
+
const identity = await resolveRequestIdentity({ request, platformContext });
|
|
427
|
+
const userState = await store.getUserState(identity.userId);
|
|
428
|
+
const subscriptionId = input.subscriptionId ?? userState?.subscriptionId;
|
|
429
|
+
if (!subscriptionId) {
|
|
430
|
+
throw new CreemHttpError(
|
|
431
|
+
400,
|
|
432
|
+
"No Creem subscription is linked to the current user yet."
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
const subscription = await creem.subscriptions.resume(subscriptionId);
|
|
436
|
+
const nextState = buildUserState(
|
|
437
|
+
userState,
|
|
438
|
+
buildSubscriptionStatePatch({
|
|
439
|
+
userState,
|
|
440
|
+
identity,
|
|
441
|
+
subscription
|
|
442
|
+
}),
|
|
443
|
+
now
|
|
444
|
+
);
|
|
445
|
+
await store.saveUserState(nextState);
|
|
446
|
+
return buildSubscriptionSnapshot(identity.userId, nextState, subscription);
|
|
447
|
+
},
|
|
448
|
+
async upgradeSubscription({
|
|
449
|
+
request,
|
|
450
|
+
input,
|
|
451
|
+
platformContext
|
|
452
|
+
}) {
|
|
453
|
+
const identity = await resolveRequestIdentity({ request, platformContext });
|
|
454
|
+
const userState = await store.getUserState(identity.userId);
|
|
455
|
+
const subscriptionId = input.subscriptionId ?? userState?.subscriptionId;
|
|
456
|
+
if (!subscriptionId) {
|
|
457
|
+
throw new CreemHttpError(
|
|
458
|
+
400,
|
|
459
|
+
"No Creem subscription is linked to the current user yet."
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
if (!input.productId) {
|
|
463
|
+
throw new CreemHttpError(400, "`productId` is required to upgrade.");
|
|
464
|
+
}
|
|
465
|
+
const subscription = await creem.subscriptions.upgrade(subscriptionId, {
|
|
466
|
+
productId: input.productId,
|
|
467
|
+
updateBehavior: input.updateBehavior
|
|
468
|
+
});
|
|
469
|
+
const nextState = buildUserState(
|
|
470
|
+
userState,
|
|
471
|
+
buildSubscriptionStatePatch({
|
|
472
|
+
userState,
|
|
473
|
+
identity,
|
|
474
|
+
subscription,
|
|
475
|
+
fallbackProductId: input.productId
|
|
476
|
+
}),
|
|
477
|
+
now
|
|
478
|
+
);
|
|
479
|
+
await store.saveUserState(nextState);
|
|
480
|
+
return buildSubscriptionSnapshot(identity.userId, nextState, subscription);
|
|
481
|
+
},
|
|
482
|
+
async createCustomerPortalLink({
|
|
483
|
+
request,
|
|
484
|
+
platformContext
|
|
485
|
+
}) {
|
|
486
|
+
const identity = await resolveRequestIdentity({ request, platformContext });
|
|
487
|
+
const userState = await store.getUserState(identity.userId);
|
|
488
|
+
const customerId = userState?.customerId ?? identity.customerId;
|
|
489
|
+
if (!customerId) {
|
|
490
|
+
throw new CreemHttpError(
|
|
491
|
+
400,
|
|
492
|
+
"No customer is linked to the current user yet. Complete a checkout first."
|
|
493
|
+
);
|
|
494
|
+
}
|
|
495
|
+
const portal = await creem.customers.generateBillingLinks({
|
|
496
|
+
customerId
|
|
497
|
+
});
|
|
498
|
+
return mapPortalEntity(portal, customerId);
|
|
499
|
+
},
|
|
500
|
+
async handleWebhook({
|
|
501
|
+
request,
|
|
502
|
+
rawBody,
|
|
503
|
+
signature,
|
|
504
|
+
platformContext
|
|
505
|
+
}) {
|
|
506
|
+
if (!verifyWebhookSignature(rawBody, signature, webhookSecret)) {
|
|
507
|
+
throw new CreemHttpError(401, "Invalid Creem webhook signature.");
|
|
508
|
+
}
|
|
509
|
+
const event = normalizeWebhookEvent(JSON.parse(rawBody));
|
|
510
|
+
if (await store.hasProcessedWebhook(event.id)) {
|
|
511
|
+
return {
|
|
512
|
+
ok: true,
|
|
513
|
+
duplicated: true,
|
|
514
|
+
eventId: event.id
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
const existingUserState = await resolveIdentityFromWebhook(store, event);
|
|
518
|
+
let resolvedUserState = existingUserState;
|
|
519
|
+
const checkoutId = extractId(event.object);
|
|
520
|
+
const existingCheckout = checkoutId ? await store.getCheckout(checkoutId) : null;
|
|
521
|
+
const eventMetadata = asOptionalJsonObject(event.object.metadata);
|
|
522
|
+
const subscriptionId = extractId(event.object.subscription);
|
|
523
|
+
const customerId = extractId(event.object.customer);
|
|
524
|
+
const orderId = extractId(event.object.order);
|
|
525
|
+
const productId = extractId(event.object.product);
|
|
526
|
+
const subscriptionStatus = typeof event.object.status === "string" ? event.object.status : null;
|
|
527
|
+
const userId = existingUserState?.userId ?? existingCheckout?.userId ?? (typeof asRecord(eventMetadata)[CREEM_USER_ID_KEY] === "string" ? asRecord(eventMetadata)[CREEM_USER_ID_KEY] : null);
|
|
528
|
+
if (userId) {
|
|
529
|
+
const nextState = buildUserState(
|
|
530
|
+
existingUserState,
|
|
531
|
+
{
|
|
532
|
+
userId,
|
|
533
|
+
email: extractCustomerEmail(event.object.customer) ?? existingUserState?.email ?? null,
|
|
534
|
+
customerId: customerId ?? existingUserState?.customerId ?? null,
|
|
535
|
+
subscriptionId: subscriptionId ?? existingUserState?.subscriptionId ?? null,
|
|
536
|
+
latestCheckoutId: checkoutId ?? existingUserState?.latestCheckoutId ?? null,
|
|
537
|
+
latestOrderId: orderId ?? existingUserState?.latestOrderId ?? null,
|
|
538
|
+
latestProductId: productId ?? existingUserState?.latestProductId ?? null,
|
|
539
|
+
subscriptionStatus,
|
|
540
|
+
entitlementActive: isEntitledFromStatus(subscriptionStatus),
|
|
541
|
+
metadata: eventMetadata ?? existingUserState?.metadata ?? null,
|
|
542
|
+
subscription: "collectionMethod" in event.object ? event.object : existingUserState?.subscription ?? null
|
|
543
|
+
},
|
|
544
|
+
now
|
|
545
|
+
);
|
|
546
|
+
await store.saveUserState(nextState);
|
|
547
|
+
resolvedUserState = nextState;
|
|
548
|
+
if (checkoutId) {
|
|
549
|
+
await store.saveCheckout({
|
|
550
|
+
checkoutId,
|
|
551
|
+
userId,
|
|
552
|
+
productId: productId ?? existingCheckout?.productId,
|
|
553
|
+
requestId: typeof event.object.request_id === "string" ? event.object.request_id : existingCheckout?.requestId,
|
|
554
|
+
status: typeof event.object.status === "string" ? event.object.status : existingCheckout?.status ?? "completed",
|
|
555
|
+
checkoutUrl: existingCheckout?.checkoutUrl ?? "",
|
|
556
|
+
successUrl: existingCheckout?.successUrl ?? null,
|
|
557
|
+
customerId: customerId ?? existingCheckout?.customerId ?? null,
|
|
558
|
+
subscriptionId: subscriptionId ?? existingCheckout?.subscriptionId ?? null,
|
|
559
|
+
orderId: orderId ?? existingCheckout?.orderId ?? null,
|
|
560
|
+
metadata: eventMetadata ?? existingCheckout?.metadata ?? null,
|
|
561
|
+
createdAt: existingCheckout?.createdAt ?? nowIso(now),
|
|
562
|
+
updatedAt: nowIso(now)
|
|
563
|
+
});
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
await dispatchWebhookHandlers({
|
|
567
|
+
request,
|
|
568
|
+
platformContext,
|
|
569
|
+
event,
|
|
570
|
+
userState: resolvedUserState
|
|
571
|
+
});
|
|
572
|
+
await store.markWebhookProcessed({
|
|
573
|
+
eventId: event.id,
|
|
574
|
+
eventType: event.eventType,
|
|
575
|
+
processedAt: nowIso(now)
|
|
576
|
+
});
|
|
577
|
+
return {
|
|
578
|
+
ok: true,
|
|
579
|
+
duplicated: false,
|
|
580
|
+
eventId: event.id
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
};
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
// src/server/fetchHandlers.ts
|
|
587
|
+
var createCreemFetchHandlers = (options) => {
|
|
588
|
+
const core = createCreemServerCore(options);
|
|
589
|
+
const readOptionalJson = async (request) => {
|
|
590
|
+
const rawBody = await request.text();
|
|
591
|
+
return rawBody ? JSON.parse(rawBody) : {};
|
|
592
|
+
};
|
|
593
|
+
return {
|
|
594
|
+
async createCheckoutSession(request, platformContext) {
|
|
595
|
+
try {
|
|
596
|
+
const input = await request.json();
|
|
597
|
+
if (!input?.productId) {
|
|
598
|
+
throw new CreemHttpError(400, "`productId` is required.");
|
|
599
|
+
}
|
|
600
|
+
const session = await core.createCheckoutSession({
|
|
601
|
+
request,
|
|
602
|
+
platformContext,
|
|
603
|
+
input
|
|
604
|
+
});
|
|
605
|
+
return json(session, { status: 200 });
|
|
606
|
+
} catch (error) {
|
|
607
|
+
return errorResponse(error);
|
|
608
|
+
}
|
|
609
|
+
},
|
|
610
|
+
async verifyCheckoutSession(request, platformContext) {
|
|
611
|
+
try {
|
|
612
|
+
const input = await readOptionalJson(request);
|
|
613
|
+
if (!input?.checkoutId) {
|
|
614
|
+
throw new CreemHttpError(400, "`checkoutId` is required.");
|
|
615
|
+
}
|
|
616
|
+
const verification = await core.verifyCheckoutSession({
|
|
617
|
+
request,
|
|
618
|
+
platformContext,
|
|
619
|
+
input
|
|
620
|
+
});
|
|
621
|
+
return json(verification, { status: 200 });
|
|
622
|
+
} catch (error) {
|
|
623
|
+
return errorResponse(error);
|
|
624
|
+
}
|
|
625
|
+
},
|
|
626
|
+
async getSubscription(request, platformContext) {
|
|
627
|
+
try {
|
|
628
|
+
const snapshot = await core.getSubscriptionSnapshot({
|
|
629
|
+
request,
|
|
630
|
+
platformContext
|
|
631
|
+
});
|
|
632
|
+
return json(snapshot, { status: 200 });
|
|
633
|
+
} catch (error) {
|
|
634
|
+
return errorResponse(error);
|
|
635
|
+
}
|
|
636
|
+
},
|
|
637
|
+
async cancelSubscription(request, platformContext) {
|
|
638
|
+
try {
|
|
639
|
+
const input = await readOptionalJson(request);
|
|
640
|
+
const snapshot = await core.cancelSubscription({
|
|
641
|
+
request,
|
|
642
|
+
platformContext,
|
|
643
|
+
input
|
|
644
|
+
});
|
|
645
|
+
return json(snapshot, { status: 200 });
|
|
646
|
+
} catch (error) {
|
|
647
|
+
return errorResponse(error);
|
|
648
|
+
}
|
|
649
|
+
},
|
|
650
|
+
async pauseSubscription(request, platformContext) {
|
|
651
|
+
try {
|
|
652
|
+
const input = await readOptionalJson(request);
|
|
653
|
+
const snapshot = await core.pauseSubscription({
|
|
654
|
+
request,
|
|
655
|
+
platformContext,
|
|
656
|
+
input
|
|
657
|
+
});
|
|
658
|
+
return json(snapshot, { status: 200 });
|
|
659
|
+
} catch (error) {
|
|
660
|
+
return errorResponse(error);
|
|
661
|
+
}
|
|
662
|
+
},
|
|
663
|
+
async resumeSubscription(request, platformContext) {
|
|
664
|
+
try {
|
|
665
|
+
const input = await readOptionalJson(request);
|
|
666
|
+
const snapshot = await core.resumeSubscription({
|
|
667
|
+
request,
|
|
668
|
+
platformContext,
|
|
669
|
+
input
|
|
670
|
+
});
|
|
671
|
+
return json(snapshot, { status: 200 });
|
|
672
|
+
} catch (error) {
|
|
673
|
+
return errorResponse(error);
|
|
674
|
+
}
|
|
675
|
+
},
|
|
676
|
+
async upgradeSubscription(request, platformContext) {
|
|
677
|
+
try {
|
|
678
|
+
const input = await readOptionalJson(request);
|
|
679
|
+
const snapshot = await core.upgradeSubscription({
|
|
680
|
+
request,
|
|
681
|
+
platformContext,
|
|
682
|
+
input
|
|
683
|
+
});
|
|
684
|
+
return json(snapshot, { status: 200 });
|
|
685
|
+
} catch (error) {
|
|
686
|
+
return errorResponse(error);
|
|
687
|
+
}
|
|
688
|
+
},
|
|
689
|
+
async createCustomerPortal(request, platformContext) {
|
|
690
|
+
try {
|
|
691
|
+
const portal = await core.createCustomerPortalLink({
|
|
692
|
+
request,
|
|
693
|
+
platformContext
|
|
694
|
+
});
|
|
695
|
+
return json(portal, { status: 200 });
|
|
696
|
+
} catch (error) {
|
|
697
|
+
return errorResponse(error);
|
|
698
|
+
}
|
|
699
|
+
},
|
|
700
|
+
async handleWebhook(request, platformContext) {
|
|
701
|
+
try {
|
|
702
|
+
const signature = request.headers.get("creem-signature");
|
|
703
|
+
if (!signature) {
|
|
704
|
+
throw new CreemHttpError(401, "Missing Creem webhook signature.");
|
|
705
|
+
}
|
|
706
|
+
const rawBody = await request.text();
|
|
707
|
+
const result = await core.handleWebhook({
|
|
708
|
+
request,
|
|
709
|
+
platformContext,
|
|
710
|
+
rawBody,
|
|
711
|
+
signature
|
|
712
|
+
});
|
|
713
|
+
return json(result, { status: 200 });
|
|
714
|
+
} catch (error) {
|
|
715
|
+
return errorResponse(error);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
};
|
|
719
|
+
};
|
|
720
|
+
|
|
721
|
+
// src/server/express.ts
|
|
722
|
+
var firstHeaderValue = (value) => value?.split(",")[0]?.trim();
|
|
723
|
+
var getRequestOrigin = (request) => {
|
|
724
|
+
const protocol = firstHeaderValue(request.get("x-forwarded-proto")) ?? request.protocol;
|
|
725
|
+
const host = firstHeaderValue(request.get("x-forwarded-host")) ?? request.get("host") ?? "localhost";
|
|
726
|
+
return `${protocol}://${host}`;
|
|
727
|
+
};
|
|
728
|
+
var appendHeaders = (headers, source) => {
|
|
729
|
+
for (const [key, value] of Object.entries(source)) {
|
|
730
|
+
if (typeof value === "undefined") {
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
if (Array.isArray(value)) {
|
|
734
|
+
for (const item of value) {
|
|
735
|
+
headers.append(key, item);
|
|
736
|
+
}
|
|
737
|
+
continue;
|
|
738
|
+
}
|
|
739
|
+
headers.set(key, value);
|
|
740
|
+
}
|
|
741
|
+
return headers;
|
|
742
|
+
};
|
|
743
|
+
var serializeBody = (body) => {
|
|
744
|
+
if (typeof body === "undefined" || body === null) {
|
|
745
|
+
return void 0;
|
|
746
|
+
}
|
|
747
|
+
if (Buffer.isBuffer(body)) {
|
|
748
|
+
return new Uint8Array(body);
|
|
749
|
+
}
|
|
750
|
+
if (typeof body === "string") {
|
|
751
|
+
return body;
|
|
752
|
+
}
|
|
753
|
+
return JSON.stringify(body);
|
|
754
|
+
};
|
|
755
|
+
var toWebRequest = (request) => {
|
|
756
|
+
const origin = getRequestOrigin(request);
|
|
757
|
+
const url = new URL(request.originalUrl || request.url, origin);
|
|
758
|
+
const headers = appendHeaders(new Headers(), request.headers);
|
|
759
|
+
const init = {
|
|
760
|
+
method: request.method,
|
|
761
|
+
headers
|
|
762
|
+
};
|
|
763
|
+
if (!["GET", "HEAD"].includes(request.method.toUpperCase())) {
|
|
764
|
+
const body = serializeBody(request.body);
|
|
765
|
+
if (typeof body !== "undefined") {
|
|
766
|
+
init.body = body;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
return new Request(url.toString(), init);
|
|
770
|
+
};
|
|
771
|
+
var sendWebResponse = async (response, webResponse) => {
|
|
772
|
+
response.status(webResponse.status);
|
|
773
|
+
webResponse.headers.forEach((value, key) => {
|
|
774
|
+
if (key.toLowerCase() === "transfer-encoding") {
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
response.setHeader(key, value);
|
|
778
|
+
});
|
|
779
|
+
response.send(await webResponse.text());
|
|
780
|
+
};
|
|
781
|
+
var __internal = {
|
|
782
|
+
firstHeaderValue,
|
|
783
|
+
getRequestOrigin,
|
|
784
|
+
appendHeaders,
|
|
785
|
+
serializeBody,
|
|
786
|
+
toWebRequest,
|
|
787
|
+
sendWebResponse
|
|
788
|
+
};
|
|
789
|
+
var createCreemExpressRouter = ({
|
|
790
|
+
getPlatformContext,
|
|
791
|
+
...options
|
|
792
|
+
}) => {
|
|
793
|
+
const router = Router();
|
|
794
|
+
const handlers = createCreemFetchHandlers(options);
|
|
795
|
+
const runHandler = (handler) => {
|
|
796
|
+
return async (request, response) => {
|
|
797
|
+
const webRequest = toWebRequest(request);
|
|
798
|
+
const platformContext = getPlatformContext ? getPlatformContext(request, response) : request;
|
|
799
|
+
const webResponse = await handler(webRequest, platformContext);
|
|
800
|
+
await sendWebResponse(response, webResponse);
|
|
801
|
+
};
|
|
802
|
+
};
|
|
803
|
+
router.post(
|
|
804
|
+
"/creem/checkout-sessions",
|
|
805
|
+
express.json(),
|
|
806
|
+
runHandler(handlers.createCheckoutSession)
|
|
807
|
+
);
|
|
808
|
+
router.post(
|
|
809
|
+
"/creem/checkout/verify",
|
|
810
|
+
express.json(),
|
|
811
|
+
runHandler(handlers.verifyCheckoutSession)
|
|
812
|
+
);
|
|
813
|
+
router.get("/creem/subscription", runHandler(handlers.getSubscription));
|
|
814
|
+
router.post(
|
|
815
|
+
"/creem/subscription/cancel",
|
|
816
|
+
express.json(),
|
|
817
|
+
runHandler(handlers.cancelSubscription)
|
|
818
|
+
);
|
|
819
|
+
router.post(
|
|
820
|
+
"/creem/subscription/pause",
|
|
821
|
+
express.json(),
|
|
822
|
+
runHandler(handlers.pauseSubscription)
|
|
823
|
+
);
|
|
824
|
+
router.post(
|
|
825
|
+
"/creem/subscription/resume",
|
|
826
|
+
express.json(),
|
|
827
|
+
runHandler(handlers.resumeSubscription)
|
|
828
|
+
);
|
|
829
|
+
router.post(
|
|
830
|
+
"/creem/subscription/upgrade",
|
|
831
|
+
express.json(),
|
|
832
|
+
runHandler(handlers.upgradeSubscription)
|
|
833
|
+
);
|
|
834
|
+
router.post(
|
|
835
|
+
"/creem/customer-portal",
|
|
836
|
+
express.json(),
|
|
837
|
+
runHandler(handlers.createCustomerPortal)
|
|
838
|
+
);
|
|
839
|
+
router.post(
|
|
840
|
+
"/creem/webhooks",
|
|
841
|
+
express.raw({
|
|
842
|
+
type: "*/*"
|
|
843
|
+
}),
|
|
844
|
+
runHandler(handlers.handleWebhook)
|
|
845
|
+
);
|
|
846
|
+
return router;
|
|
847
|
+
};
|
|
848
|
+
var createCreemRouter = createCreemExpressRouter;
|
|
849
|
+
|
|
850
|
+
export {
|
|
851
|
+
verifyWebhookSignature,
|
|
852
|
+
createCreemServerCore,
|
|
853
|
+
createCreemFetchHandlers,
|
|
854
|
+
__internal,
|
|
855
|
+
createCreemExpressRouter,
|
|
856
|
+
createCreemRouter
|
|
857
|
+
};
|