payment-kit 1.27.2 → 1.29.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/__blocklet__.js +37 -0
- package/api/ocap-1.30-subpath-shims.d.ts +35 -0
- package/api/src/crons/index.ts +32 -0
- package/api/src/crons/metering-subscription-detection.ts +12 -14
- package/api/src/crons/overdue-detection.ts +51 -74
- package/api/src/crons/retry-pending-events.ts +58 -0
- package/api/src/integrations/app-store/apple-root-certs.ts +26 -0
- package/api/src/integrations/app-store/client.ts +369 -0
- package/api/src/integrations/app-store/handlers/index.ts +46 -0
- package/api/src/integrations/app-store/handlers/subscription.ts +635 -0
- package/api/src/integrations/app-store/node-apple-receipt-verify.d.ts +17 -0
- package/api/src/integrations/app-store/notification-routing.ts +18 -0
- package/api/src/integrations/app-store/signed-data-verifier.ts +150 -0
- package/api/src/integrations/arcblock/nft.ts +6 -2
- package/api/src/integrations/arcblock/stake.ts +3 -2
- package/api/src/integrations/arcblock/token.ts +4 -4
- package/api/src/integrations/blocklet/notification.ts +1 -1
- package/api/src/integrations/ethereum/tx.ts +29 -0
- package/api/src/integrations/google-play/client.ts +276 -0
- package/api/src/integrations/google-play/handlers/index.ts +69 -0
- package/api/src/integrations/google-play/handlers/subscription.ts +565 -0
- package/api/src/integrations/google-play/handlers/voided.ts +106 -0
- package/api/src/integrations/google-play/setup.ts +43 -0
- package/api/src/integrations/google-play/verify.ts +251 -0
- package/api/src/integrations/iap-reconcile.ts +415 -0
- package/api/src/integrations/stripe/handlers/invoice.ts +70 -53
- package/api/src/integrations/stripe/handlers/payment-intent.ts +8 -1
- package/api/src/integrations/stripe/resource.ts +8 -0
- package/api/src/libs/audit.ts +70 -24
- package/api/src/libs/auth.ts +49 -2
- package/api/src/libs/chain-error.ts +31 -0
- package/api/src/libs/entitlement.ts +399 -0
- package/api/src/libs/env.ts +2 -0
- package/api/src/libs/error.ts +15 -0
- package/api/src/libs/event.ts +42 -1
- package/api/src/libs/invoice.ts +69 -34
- package/api/src/libs/notification/template/customer-auto-recharge-daily-limit-exceeded.ts +1 -3
- package/api/src/libs/notification/template/customer-auto-recharge-failed.ts +1 -3
- package/api/src/libs/notification/template/customer-credit-grant-granted.ts +1 -3
- package/api/src/libs/notification/template/customer-credit-insufficient.ts +1 -3
- package/api/src/libs/notification/template/customer-credit-low-balance.ts +1 -3
- package/api/src/libs/notification/template/customer-revenue-succeeded.ts +1 -3
- package/api/src/libs/notification/template/customer-reward-succeeded.ts +1 -3
- package/api/src/libs/notification/template/one-time-payment-refund-succeeded.ts +1 -3
- package/api/src/libs/notification/template/one-time-payment-succeeded.ts +1 -3
- package/api/src/libs/notification/template/subscription-renew-failed.ts +1 -3
- package/api/src/libs/notification/template/subscription-slippage-exceeded.ts +1 -3
- package/api/src/libs/notification/template/subscription-slippage-warning.ts +1 -3
- package/api/src/libs/notification/template/subscription-succeeded.ts +1 -1
- package/api/src/libs/pagination.ts +14 -9
- package/api/src/libs/payment.ts +25 -10
- package/api/src/libs/security.ts +51 -0
- package/api/src/libs/session.ts +1 -1
- package/api/src/libs/subscription.ts +13 -1
- package/api/src/libs/timing.ts +35 -0
- package/api/src/libs/util.ts +29 -15
- package/api/src/libs/wallet-migration.ts +72 -53
- package/api/src/queues/auto-recharge.ts +1 -1
- package/api/src/queues/credit-consume.ts +94 -12
- package/api/src/queues/credit-grant.ts +4 -0
- package/api/src/queues/event.ts +39 -21
- package/api/src/queues/invoice.ts +1 -0
- package/api/src/queues/payment.ts +83 -15
- package/api/src/queues/refund.ts +84 -71
- package/api/src/queues/subscription.ts +1 -0
- package/api/src/queues/webhook.ts +12 -2
- package/api/src/routes/checkout-sessions.ts +82 -43
- package/api/src/routes/connect/change-payment.ts +2 -0
- package/api/src/routes/connect/change-plan.ts +2 -0
- package/api/src/routes/connect/pay.ts +12 -3
- package/api/src/routes/connect/setup.ts +3 -1
- package/api/src/routes/connect/shared.ts +52 -39
- package/api/src/routes/connect/subscribe.ts +4 -1
- package/api/src/routes/credit-grants.ts +25 -17
- package/api/src/routes/donations.ts +2 -2
- package/api/src/routes/entitlements.ts +105 -0
- package/api/src/routes/events.ts +2 -2
- package/api/src/routes/index.ts +12 -2
- package/api/src/routes/integrations/app-store.ts +267 -0
- package/api/src/routes/integrations/google-play.ts +324 -0
- package/api/src/routes/meter-events.ts +16 -6
- package/api/src/routes/payment-links.ts +1 -1
- package/api/src/routes/payment-methods.ts +131 -1
- package/api/src/routes/settings.ts +1 -1
- package/api/src/routes/tax-rates.ts +1 -1
- package/api/src/store/migrations/20260526-iap-foundation.ts +105 -0
- package/api/src/store/models/customer.ts +37 -1
- package/api/src/store/models/entitlement-grant.ts +118 -0
- package/api/src/store/models/entitlement-product.ts +48 -0
- package/api/src/store/models/entitlement.ts +86 -0
- package/api/src/store/models/index.ts +9 -0
- package/api/src/store/models/invoice.ts +20 -0
- package/api/src/store/models/payment-method.ts +66 -1
- package/api/src/store/models/price.ts +23 -14
- package/api/src/store/models/refund.ts +10 -0
- package/api/src/store/models/subscription.ts +14 -0
- package/api/src/store/models/types.ts +32 -0
- package/api/tests/integrations/app-store/client.spec.ts +335 -0
- package/api/tests/integrations/app-store/handlers.spec.ts +480 -0
- package/api/tests/integrations/app-store/notifications.spec.ts +381 -0
- package/api/tests/integrations/app-store/signed-data-verifier.spec.ts +72 -0
- package/api/tests/integrations/app-store/webhook-routing.spec.ts +27 -0
- package/api/tests/integrations/google-play/handlers.spec.ts +341 -0
- package/api/tests/integrations/google-play/verify.spec.ts +215 -0
- package/api/tests/integrations/iap-reconcile.spec.ts +237 -0
- package/api/tests/libs/entitlement.spec.ts +347 -0
- package/api/tests/libs/wallet-migration.spec.ts +4 -4
- package/api/tests/queues/credit-consume-batch.spec.ts +5 -2
- package/api/tests/queues/credit-consume.spec.ts +8 -4
- package/api/tests/routes/credit-grants.spec.ts +1 -0
- package/blocklet.yml +1 -1
- package/cloudflare/MIGRATION-CHALLENGES.md +676 -0
- package/cloudflare/MIGRATION-RUNBOOK.md +777 -0
- package/cloudflare/README.md +499 -0
- package/cloudflare/STAGING-MIGRATION-GUIDE.md +602 -0
- package/cloudflare/build.ts +151 -0
- package/cloudflare/did-connect-auth.ts +527 -0
- package/cloudflare/docs/2026-04-22-sdk-1.30.9-upgrade-retro.md +324 -0
- package/cloudflare/docs/2026-04-24-queue-ops-followup.md +218 -0
- package/cloudflare/docs/cf-queues-ops-alert-analysis.md +663 -0
- package/cloudflare/docs/cf-workers-local-dev-and-fixes.md +284 -0
- package/cloudflare/docs/cleanup-tasks-2026-05.md +62 -0
- package/cloudflare/docs/payment-kit-platform-analysis-2026-04-20.md +354 -0
- package/cloudflare/frontend-shims/buffer-polyfill.ts +9 -0
- package/cloudflare/frontend-shims/js-sdk.ts +43 -0
- package/cloudflare/frontend-shims/mime-types.ts +46 -0
- package/cloudflare/frontend-shims/session.ts +24 -0
- package/cloudflare/frontend-shims/vite-plugin-noop.ts +6 -0
- package/cloudflare/index.html +40 -0
- package/cloudflare/migrate-to-d1.js +252 -0
- package/cloudflare/migrations/0001_initial_schema.sql +82 -0
- package/cloudflare/migrations/0002_indexes.sql +75 -0
- package/cloudflare/migrations/0003_locks_and_constraints.sql +18 -0
- package/cloudflare/migrations/0004_iap_foundation.sql +72 -0
- package/cloudflare/migrations/0005_iap_tenant_backfill.sql +112 -0
- package/cloudflare/run-build.js +391 -0
- package/cloudflare/scripts/test-decrypt.js +102 -0
- package/cloudflare/shims/arcblock-ws.ts +20 -0
- package/cloudflare/shims/axios-http-adapter.ts +4 -0
- package/cloudflare/shims/axios-lite.ts +117 -0
- package/cloudflare/shims/blocklet-sdk/auth-service.ts +33 -0
- package/cloudflare/shims/blocklet-sdk/cdn.ts +3 -0
- package/cloudflare/shims/blocklet-sdk/component-api.ts +35 -0
- package/cloudflare/shims/blocklet-sdk/component.ts +18 -0
- package/cloudflare/shims/blocklet-sdk/config.ts +8 -0
- package/cloudflare/shims/blocklet-sdk/did.ts +14 -0
- package/cloudflare/shims/blocklet-sdk/env.ts +12 -0
- package/cloudflare/shims/blocklet-sdk/eventbus.ts +3 -0
- package/cloudflare/shims/blocklet-sdk/fallback.ts +3 -0
- package/cloudflare/shims/blocklet-sdk/index.ts +11 -0
- package/cloudflare/shims/blocklet-sdk/logger.ts +11 -0
- package/cloudflare/shims/blocklet-sdk/middlewares.ts +15 -0
- package/cloudflare/shims/blocklet-sdk/notification.ts +11 -0
- package/cloudflare/shims/blocklet-sdk/security.ts +53 -0
- package/cloudflare/shims/blocklet-sdk/session.ts +8 -0
- package/cloudflare/shims/blocklet-sdk/verify-session.ts +44 -0
- package/cloudflare/shims/blocklet-sdk/verify-sign.ts +38 -0
- package/cloudflare/shims/blocklet-sdk/wallet-authenticator.ts +3 -0
- package/cloudflare/shims/blocklet-sdk/wallet-handler.ts +6 -0
- package/cloudflare/shims/blocklet-sdk/wallet.ts +103 -0
- package/cloudflare/shims/cookie-parser.ts +3 -0
- package/cloudflare/shims/cors.ts +21 -0
- package/cloudflare/shims/cron.ts +189 -0
- package/cloudflare/shims/crypto-js-warn.ts +7 -0
- package/cloudflare/shims/did-space-js.ts +17 -0
- package/cloudflare/shims/did-space.ts +11 -0
- package/cloudflare/shims/error.ts +18 -0
- package/cloudflare/shims/express-compat/index.ts +80 -0
- package/cloudflare/shims/express-compat/types.ts +41 -0
- package/cloudflare/shims/fastq.ts +105 -0
- package/cloudflare/shims/lock.ts +115 -0
- package/cloudflare/shims/mime-types.ts +56 -0
- package/cloudflare/shims/nedb-storage.ts +9 -0
- package/cloudflare/shims/node-child-process.ts +9 -0
- package/cloudflare/shims/node-fs.ts +20 -0
- package/cloudflare/shims/node-http.ts +13 -0
- package/cloudflare/shims/node-https.ts +4 -0
- package/cloudflare/shims/node-misc.ts +15 -0
- package/cloudflare/shims/node-net.ts +8 -0
- package/cloudflare/shims/node-os.ts +14 -0
- package/cloudflare/shims/node-tty.ts +8 -0
- package/cloudflare/shims/node-zlib.ts +17 -0
- package/cloudflare/shims/noop.ts +26 -0
- package/cloudflare/shims/payment-vendor.ts +14 -0
- package/cloudflare/shims/querystring.ts +12 -0
- package/cloudflare/shims/queue.ts +611 -0
- package/cloudflare/shims/rolldown-runtime.ts +43 -0
- package/cloudflare/shims/sequelize-d1/datatypes.ts +24 -0
- package/cloudflare/shims/sequelize-d1/helpers.ts +46 -0
- package/cloudflare/shims/sequelize-d1/index.ts +34 -0
- package/cloudflare/shims/sequelize-d1/model.ts +1176 -0
- package/cloudflare/shims/sequelize-d1/operators.ts +306 -0
- package/cloudflare/shims/sequelize-d1/retry.ts +85 -0
- package/cloudflare/shims/sequelize-d1/sequelize-class.ts +119 -0
- package/cloudflare/shims/sequelize-d1/timing.ts +81 -0
- package/cloudflare/shims/sequelize-d1/types.ts +35 -0
- package/cloudflare/shims/stripe-cf.ts +29 -0
- package/cloudflare/shims/ws-lite.ts +103 -0
- package/cloudflare/shims/xss.ts +3 -0
- package/cloudflare/tests/shims/cron.spec.ts +210 -0
- package/cloudflare/tests/shims/queue-delayed-persist.spec.ts +87 -0
- package/cloudflare/tests/shims/queue-scheduled.spec.ts +186 -0
- package/cloudflare/vite.config.ts +162 -0
- package/cloudflare/worker.ts +1608 -0
- package/cloudflare/wrangler.json +63 -0
- package/cloudflare/wrangler.jsonc +75 -0
- package/cloudflare/wrangler.staging.json +67 -0
- package/cloudflare/wrangler.toml +28 -0
- package/jest.config.js +4 -12
- package/package.json +30 -22
- package/scripts/seed-google-play.ts +79 -0
- package/src/app.tsx +62 -4
- package/src/components/customer/link.tsx +9 -13
- package/src/components/customer/notification-preference.tsx +3 -2
- package/src/components/filter-toolbar.tsx +4 -0
- package/src/components/invoice/list.tsx +9 -1
- package/src/components/invoice-pdf/utils.ts +2 -1
- package/src/components/layout/admin.tsx +39 -5
- package/src/components/layout/user-cf.tsx +77 -0
- package/src/components/payment-intent/actions.tsx +23 -3
- package/src/components/payment-method/app-store.tsx +103 -0
- package/src/components/payment-method/form.tsx +7 -1
- package/src/components/payment-method/google-play.tsx +85 -0
- package/src/components/safe-did-address.tsx +75 -0
- package/src/components/subscription/list.tsx +20 -0
- package/src/libs/patch-user-card.ts +25 -0
- package/src/libs/util.ts +5 -7
- package/src/locales/en.tsx +63 -0
- package/src/locales/zh.tsx +63 -0
- package/src/pages/admin/billing/meter-events/index.tsx +4 -0
- package/src/pages/admin/billing/subscriptions/detail.tsx +80 -0
- package/src/pages/admin/customers/customers/detail.tsx +8 -2
- package/src/pages/admin/customers/customers/index.tsx +2 -2
- package/src/pages/admin/overview.tsx +3 -1
- package/src/pages/admin/settings/payment-methods/create.tsx +12 -0
- package/src/pages/admin/settings/payment-methods/index.tsx +1 -1
- package/src/pages/customer/subscription/detail.tsx +4 -4
- package/tsconfig.api.json +1 -6
- package/tsconfig.json +3 -4
- package/tsconfig.types.json +2 -1
- package/vite.config.ts +6 -1
|
@@ -7,6 +7,8 @@ import { CreationOptional, DataTypes, InferAttributes, InferCreationAttributes,
|
|
|
7
7
|
import Stripe from 'stripe';
|
|
8
8
|
import type { LiteralUnion } from 'type-fest';
|
|
9
9
|
|
|
10
|
+
import { AppStoreClient } from '../../integrations/app-store/client';
|
|
11
|
+
import { GooglePlayClient } from '../../integrations/google-play/client';
|
|
10
12
|
import { STRIPE_API_VERSION, createIdGenerator } from '../../libs/util';
|
|
11
13
|
import { sequelize } from '../sequelize';
|
|
12
14
|
import type { PaymentMethodSettings } from './types';
|
|
@@ -17,6 +19,8 @@ const nextId = createIdGenerator('pm', 24);
|
|
|
17
19
|
const stripeClients = new Map<string, Stripe>();
|
|
18
20
|
const evmClients = new Map<string, JsonRpcProvider>();
|
|
19
21
|
const ocapClients = new Map<string, OcapClient>();
|
|
22
|
+
const googlePlayClients = new Map<string, GooglePlayClient>();
|
|
23
|
+
const appStoreClients = new Map<string, AppStoreClient>();
|
|
20
24
|
|
|
21
25
|
// eslint-disable-next-line prettier/prettier
|
|
22
26
|
export class PaymentMethod extends Model<InferAttributes<PaymentMethod>, InferCreationAttributes<PaymentMethod>> {
|
|
@@ -27,7 +31,10 @@ export class PaymentMethod extends Model<InferAttributes<PaymentMethod>, InferCr
|
|
|
27
31
|
declare livemode: boolean;
|
|
28
32
|
declare locked: boolean;
|
|
29
33
|
|
|
30
|
-
declare type: LiteralUnion<
|
|
34
|
+
declare type: LiteralUnion<
|
|
35
|
+
'stripe' | 'arcblock' | 'ethereum' | 'bitcoin' | 'base' | 'app_store' | 'google_play',
|
|
36
|
+
string
|
|
37
|
+
>;
|
|
31
38
|
|
|
32
39
|
declare name: string;
|
|
33
40
|
|
|
@@ -153,6 +160,15 @@ export class PaymentMethod extends Model<InferAttributes<PaymentMethod>, InferCr
|
|
|
153
160
|
tmp.stripe.secret_key = security.encrypt(tmp.stripe.secret_key);
|
|
154
161
|
tmp.stripe.webhook_signing_secret = security.encrypt(tmp.stripe.webhook_signing_secret);
|
|
155
162
|
}
|
|
163
|
+
if (tmp.google_play) {
|
|
164
|
+
tmp.google_play.service_account_json = security.encrypt(tmp.google_play.service_account_json);
|
|
165
|
+
}
|
|
166
|
+
if (tmp.app_store?.private_key_pem) {
|
|
167
|
+
tmp.app_store.private_key_pem = security.encrypt(tmp.app_store.private_key_pem);
|
|
168
|
+
}
|
|
169
|
+
if (tmp.app_store?.shared_secret) {
|
|
170
|
+
tmp.app_store.shared_secret = security.encrypt(tmp.app_store.shared_secret);
|
|
171
|
+
}
|
|
156
172
|
|
|
157
173
|
return tmp;
|
|
158
174
|
}
|
|
@@ -163,6 +179,15 @@ export class PaymentMethod extends Model<InferAttributes<PaymentMethod>, InferCr
|
|
|
163
179
|
tmp.stripe.secret_key = security.decrypt(tmp.stripe.secret_key);
|
|
164
180
|
tmp.stripe.webhook_signing_secret = security.decrypt(tmp.stripe.webhook_signing_secret);
|
|
165
181
|
}
|
|
182
|
+
if (tmp.google_play) {
|
|
183
|
+
tmp.google_play.service_account_json = security.decrypt(tmp.google_play.service_account_json);
|
|
184
|
+
}
|
|
185
|
+
if (tmp.app_store?.private_key_pem) {
|
|
186
|
+
tmp.app_store.private_key_pem = security.decrypt(tmp.app_store.private_key_pem);
|
|
187
|
+
}
|
|
188
|
+
if (tmp.app_store?.shared_secret) {
|
|
189
|
+
tmp.app_store.shared_secret = security.decrypt(tmp.app_store.shared_secret);
|
|
190
|
+
}
|
|
166
191
|
|
|
167
192
|
return tmp;
|
|
168
193
|
}
|
|
@@ -207,6 +232,10 @@ export class PaymentMethod extends Model<InferAttributes<PaymentMethod>, InferCr
|
|
|
207
232
|
const host = this.settings.arcblock?.api_host;
|
|
208
233
|
const cached = ocapClients.has(host);
|
|
209
234
|
if (!cached) {
|
|
235
|
+
// 1.30.9: the default `@ocap/client` entry went CBOR-only, but current ABT
|
|
236
|
+
// wallets only decode protobuf-encoded tx bytes. Use `@ocap/client/legacy`
|
|
237
|
+
// (protobuf client) so encode / decode / broadcast all stay protobuf,
|
|
238
|
+
// matching the wallet signatures and what the chain node expects to hash.
|
|
210
239
|
const created = new OcapClient(host);
|
|
211
240
|
ocapClients.set(host, created);
|
|
212
241
|
return created;
|
|
@@ -236,6 +265,42 @@ export class PaymentMethod extends Model<InferAttributes<PaymentMethod>, InferCr
|
|
|
236
265
|
return client as JsonRpcProvider;
|
|
237
266
|
}
|
|
238
267
|
|
|
268
|
+
getGooglePlayClient() {
|
|
269
|
+
if (this.type !== 'google_play') {
|
|
270
|
+
throw new Error('payment method is not google_play');
|
|
271
|
+
}
|
|
272
|
+
if (!this.settings.google_play) {
|
|
273
|
+
throw new Error('payment method config insufficient for google_play');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (googlePlayClients.has(this.id)) {
|
|
277
|
+
return googlePlayClients.get(this.id) as GooglePlayClient;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const settings = PaymentMethod.decryptSettings(this.settings);
|
|
281
|
+
const client = GooglePlayClient.fromSettings(settings.google_play!);
|
|
282
|
+
googlePlayClients.set(this.id, client);
|
|
283
|
+
return client;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
getAppStoreClient() {
|
|
287
|
+
if (this.type !== 'app_store') {
|
|
288
|
+
throw new Error('payment method is not app_store');
|
|
289
|
+
}
|
|
290
|
+
if (!this.settings.app_store) {
|
|
291
|
+
throw new Error('payment method config insufficient for app_store');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (appStoreClients.has(this.id)) {
|
|
295
|
+
return appStoreClients.get(this.id) as AppStoreClient;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const settings = PaymentMethod.decryptSettings(this.settings);
|
|
299
|
+
const client = AppStoreClient.fromSettings(settings.app_store!);
|
|
300
|
+
appStoreClients.set(this.id, client);
|
|
301
|
+
return client;
|
|
302
|
+
}
|
|
303
|
+
|
|
239
304
|
public static async supportAutoCharge(id: string) {
|
|
240
305
|
const method = await PaymentMethod.findByPk(id);
|
|
241
306
|
return method && CHARGE_SUPPORTED_CHAIN_TYPES.includes(method.type);
|
|
@@ -438,23 +438,32 @@ export class Price extends Model<InferAttributes<Price>, InferCreationAttributes
|
|
|
438
438
|
}
|
|
439
439
|
});
|
|
440
440
|
|
|
441
|
-
// expand upsell
|
|
441
|
+
// expand upsell — batch load to avoid N+1 queries
|
|
442
442
|
if (upsell) {
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
443
|
+
const upsellToIds = [...new Set(prices.map((x) => x.upsell?.upsells_to_id).filter(Boolean))] as string[];
|
|
444
|
+
const meterIds = [...new Set(prices.map((x) => x.recurring?.meter_id).filter(Boolean))] as string[];
|
|
445
|
+
|
|
446
|
+
const [upsellPrices, meters] = await Promise.all([
|
|
447
|
+
upsellToIds.length > 0 ? Price.findAll({ where: { id: upsellToIds } }) : [],
|
|
448
|
+
meterIds.length > 0 ? Meter.findAll({ where: { id: meterIds } }) : [],
|
|
449
|
+
]);
|
|
450
|
+
|
|
451
|
+
const upsellPriceMap = new Map(upsellPrices.map((p) => [p.id, p]));
|
|
452
|
+
const meterMap = new Map(meters.map((m) => [m.id, m]));
|
|
453
|
+
|
|
454
|
+
prices.forEach((x) => {
|
|
455
|
+
if (x.upsell?.upsells_to_id) {
|
|
456
|
+
const to = upsellPriceMap.get(x.upsell.upsells_to_id);
|
|
457
|
+
if (to) {
|
|
453
458
|
// @ts-ignore
|
|
454
|
-
x.
|
|
459
|
+
x.upsell.upsells_to = to;
|
|
455
460
|
}
|
|
456
|
-
}
|
|
457
|
-
|
|
461
|
+
}
|
|
462
|
+
if (x.recurring?.meter_id) {
|
|
463
|
+
// @ts-ignore
|
|
464
|
+
x.meter = meterMap.get(x.recurring.meter_id) ?? null;
|
|
465
|
+
}
|
|
466
|
+
});
|
|
458
467
|
}
|
|
459
468
|
|
|
460
469
|
// @ts-ignore
|
|
@@ -68,6 +68,11 @@ export class Refund extends Model<InferAttributes<Refund>, InferCreationAttribut
|
|
|
68
68
|
declare updated_at: CreationOptional<Date>;
|
|
69
69
|
declare type: LiteralUnion<'refund' | 'stake_return', string>;
|
|
70
70
|
|
|
71
|
+
// Refund origin (added for IAP support).
|
|
72
|
+
// merchant_initiated —商户主动调 API 退款 (Stripe / 链上)
|
|
73
|
+
// platform_initiated — Apple/Google webhook 推过来的退款,我们被动接收
|
|
74
|
+
declare source?: LiteralUnion<'merchant_initiated' | 'platform_initiated', string>;
|
|
75
|
+
|
|
71
76
|
public static readonly GENESIS_ATTRIBUTES = {
|
|
72
77
|
id: {
|
|
73
78
|
type: DataTypes.STRING(30),
|
|
@@ -192,6 +197,11 @@ export class Refund extends Model<InferAttributes<Refund>, InferCreationAttribut
|
|
|
192
197
|
allowNull: true,
|
|
193
198
|
defaultValue: 'refund',
|
|
194
199
|
},
|
|
200
|
+
source: {
|
|
201
|
+
type: DataTypes.STRING(30),
|
|
202
|
+
allowNull: true,
|
|
203
|
+
defaultValue: 'merchant_initiated',
|
|
204
|
+
},
|
|
195
205
|
},
|
|
196
206
|
{
|
|
197
207
|
sequelize,
|
|
@@ -129,6 +129,11 @@ export class Subscription extends Model<InferAttributes<Subscription>, InferCrea
|
|
|
129
129
|
|
|
130
130
|
declare recovered_from?: string;
|
|
131
131
|
|
|
132
|
+
// Payment channel (D-005). Stripe / arcblock / ethereum / base / app_store / google_play
|
|
133
|
+
declare channel?: LiteralUnion<'stripe' | 'arcblock' | 'ethereum' | 'base' | 'app_store' | 'google_play', string>;
|
|
134
|
+
// IAP platform environment (D-005). Orthogonal to livemode.
|
|
135
|
+
declare environment?: LiteralUnion<'production' | 'sandbox', string>;
|
|
136
|
+
|
|
132
137
|
// TODO: following fields not supported
|
|
133
138
|
// application
|
|
134
139
|
// application_fee_percent
|
|
@@ -346,6 +351,15 @@ export class Subscription extends Model<InferAttributes<Subscription>, InferCrea
|
|
|
346
351
|
allowNull: true,
|
|
347
352
|
defaultValue: null,
|
|
348
353
|
},
|
|
354
|
+
channel: {
|
|
355
|
+
type: DataTypes.STRING(20),
|
|
356
|
+
allowNull: true,
|
|
357
|
+
},
|
|
358
|
+
environment: {
|
|
359
|
+
type: DataTypes.STRING(20),
|
|
360
|
+
allowNull: true,
|
|
361
|
+
defaultValue: 'production',
|
|
362
|
+
},
|
|
349
363
|
},
|
|
350
364
|
{
|
|
351
365
|
sequelize,
|
|
@@ -310,6 +310,22 @@ export type PaymentMethodSettings = {
|
|
|
310
310
|
native_symbol: string;
|
|
311
311
|
confirmation: number;
|
|
312
312
|
};
|
|
313
|
+
google_play?: {
|
|
314
|
+
package_name: string;
|
|
315
|
+
service_account_json: string; // encrypted JSON string of GCP service account credentials
|
|
316
|
+
pubsub_topic_name: string; // projects/<project>/topics/<topic>
|
|
317
|
+
};
|
|
318
|
+
app_store?: {
|
|
319
|
+
bundle_id: string;
|
|
320
|
+
environment: LiteralUnion<'production' | 'sandbox', string>;
|
|
321
|
+
/** App-Specific Shared Secret — only needed when verifying StoreKit 1 (legacy) receipts via Apple's verifyReceipt endpoint. StoreKit 2 JWS path does not use this. */
|
|
322
|
+
shared_secret?: string; // encrypted
|
|
323
|
+
// App Store Server API credentials — only needed when calling Apple back for state refresh.
|
|
324
|
+
// StoreKit 2 JWS verification (the happy path) does NOT need any of these.
|
|
325
|
+
issuer_id?: string;
|
|
326
|
+
key_id?: string;
|
|
327
|
+
private_key_pem?: string; // encrypted .p8 file contents
|
|
328
|
+
};
|
|
313
329
|
};
|
|
314
330
|
|
|
315
331
|
export type VaultConfig = {
|
|
@@ -366,6 +382,22 @@ export type PaymentDetails = {
|
|
|
366
382
|
block_height: number;
|
|
367
383
|
confirmations: number;
|
|
368
384
|
};
|
|
385
|
+
google_play?: {
|
|
386
|
+
purchase_token: string;
|
|
387
|
+
order_id?: string;
|
|
388
|
+
product_id: string;
|
|
389
|
+
subscription_id?: string;
|
|
390
|
+
expiry_time_millis?: string;
|
|
391
|
+
environment?: LiteralUnion<'production' | 'sandbox', string>;
|
|
392
|
+
};
|
|
393
|
+
app_store?: {
|
|
394
|
+
original_transaction_id: string;
|
|
395
|
+
transaction_id?: string;
|
|
396
|
+
product_id: string;
|
|
397
|
+
web_order_line_item_id?: string;
|
|
398
|
+
environment?: LiteralUnion<'Production' | 'Sandbox', string>;
|
|
399
|
+
expires_at?: number; // unix seconds
|
|
400
|
+
};
|
|
369
401
|
};
|
|
370
402
|
|
|
371
403
|
export type PaymentBeneficiary = {
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
// Unit tests for AppStoreClient.
|
|
2
|
+
// All synthetic JWS fixtures here can't pass real Apple signature verification,
|
|
3
|
+
// so we set APP_STORE_SKIP_SIGNATURE_VERIFY=true to exercise the decode/validate
|
|
4
|
+
// surface. A dedicated test below confirms real-mode rejects synthetic JWSes.
|
|
5
|
+
|
|
6
|
+
/* eslint-disable import/first */
|
|
7
|
+
process.env.APP_STORE_SKIP_SIGNATURE_VERIFY = 'true';
|
|
8
|
+
|
|
9
|
+
import { AppStoreClient, AppStoreSettings } from '../../../src/integrations/app-store/client';
|
|
10
|
+
|
|
11
|
+
const mockConfigApple = jest.fn();
|
|
12
|
+
const mockValidateAppleReceipt = jest.fn();
|
|
13
|
+
jest.mock('node-apple-receipt-verify', () => ({
|
|
14
|
+
config: (...args: any[]) => mockConfigApple(...args),
|
|
15
|
+
validate: (...args: any[]) => mockValidateAppleReceipt(...args),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
mockConfigApple.mockReset();
|
|
20
|
+
mockValidateAppleReceipt.mockReset();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const baseSettings: AppStoreSettings = {
|
|
24
|
+
bundle_id: 'com.example.app',
|
|
25
|
+
environment: 'sandbox',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const makeJws = (payload: object, header: object = { alg: 'ES256' }): string => {
|
|
29
|
+
const h = Buffer.from(JSON.stringify(header)).toString('base64url');
|
|
30
|
+
const p = Buffer.from(JSON.stringify(payload)).toString('base64url');
|
|
31
|
+
return `${h}.${p}.signature-placeholder`;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
describe('AppStoreClient.fromSettings', () => {
|
|
35
|
+
it('rejects missing bundle_id', () => {
|
|
36
|
+
expect(() => AppStoreClient.fromSettings({ ...baseSettings, bundle_id: '' })).toThrow(/bundle_id/);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('rejects bad environment', () => {
|
|
40
|
+
expect(() => AppStoreClient.fromSettings({ ...baseSettings, environment: 'staging' as any })).toThrow(
|
|
41
|
+
/environment/
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('accepts minimal valid settings (no Server API creds)', () => {
|
|
46
|
+
const c = AppStoreClient.fromSettings(baseSettings);
|
|
47
|
+
expect(c.bundleId).toBe('com.example.app');
|
|
48
|
+
expect(c.environment).toBe('sandbox');
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('AppStoreClient.verifyJwsTransaction (mock-phase decode)', () => {
|
|
53
|
+
let client: AppStoreClient;
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
client = AppStoreClient.fromSettings(baseSettings);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('decodes a well-formed JWS payload', async () => {
|
|
59
|
+
const jws = makeJws({
|
|
60
|
+
transactionId: 'txn_1',
|
|
61
|
+
originalTransactionId: 'orig_1',
|
|
62
|
+
productId: 'pro_monthly',
|
|
63
|
+
expiresDate: 1800000000000,
|
|
64
|
+
appAccountToken: 'uuid-from-client',
|
|
65
|
+
environment: 'Sandbox',
|
|
66
|
+
bundleId: 'com.example.app',
|
|
67
|
+
});
|
|
68
|
+
const decoded = await client.verifyJwsTransaction(jws);
|
|
69
|
+
expect(decoded.transactionId).toBe('txn_1');
|
|
70
|
+
expect(decoded.originalTransactionId).toBe('orig_1');
|
|
71
|
+
expect(decoded.productId).toBe('pro_monthly');
|
|
72
|
+
expect(decoded.appAccountToken).toBe('uuid-from-client');
|
|
73
|
+
expect(decoded.environment).toBe('Sandbox');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('rejects malformed JWS (not 3 segments)', async () => {
|
|
77
|
+
await expect(client.verifyJwsTransaction('not-a-jws')).rejects.toThrow(/format invalid/);
|
|
78
|
+
await expect(client.verifyJwsTransaction('a.b')).rejects.toThrow(/format invalid/);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('rejects JWS whose payload is not valid JSON', async () => {
|
|
82
|
+
const h = Buffer.from(JSON.stringify({ alg: 'ES256' })).toString('base64url');
|
|
83
|
+
const broken = Buffer.from('this is not json').toString('base64url');
|
|
84
|
+
await expect(client.verifyJwsTransaction(`${h}.${broken}.sig`)).rejects.toThrow(/not valid JSON/);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('rejects JWS whose bundleId disagrees with configured bundle_id', async () => {
|
|
88
|
+
const jws = makeJws({
|
|
89
|
+
transactionId: 'txn_1',
|
|
90
|
+
originalTransactionId: 'orig_1',
|
|
91
|
+
productId: 'pro_monthly',
|
|
92
|
+
environment: 'Sandbox',
|
|
93
|
+
bundleId: 'com.evil.app',
|
|
94
|
+
});
|
|
95
|
+
await expect(client.verifyJwsTransaction(jws)).rejects.toThrow(/bundleId mismatch/);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('passes when JWS payload omits bundleId (older StoreKit 2 payloads)', async () => {
|
|
99
|
+
const jws = makeJws({
|
|
100
|
+
transactionId: 'txn_1',
|
|
101
|
+
originalTransactionId: 'orig_1',
|
|
102
|
+
productId: 'pro_monthly',
|
|
103
|
+
environment: 'Sandbox',
|
|
104
|
+
});
|
|
105
|
+
const decoded = await client.verifyJwsTransaction(jws);
|
|
106
|
+
expect(decoded.transactionId).toBe('txn_1');
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('AppStoreClient write methods', () => {
|
|
111
|
+
it('refundSubscription is gated unless APP_STORE_WRITE_ENABLED', async () => {
|
|
112
|
+
const c = AppStoreClient.fromSettings(baseSettings);
|
|
113
|
+
await expect(c.refundSubscription('orig_1')).rejects.toThrow(/APP_STORE_WRITE_ENABLED/);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('cancelSubscription is gated unless APP_STORE_WRITE_ENABLED', async () => {
|
|
117
|
+
const c = AppStoreClient.fromSettings(baseSettings);
|
|
118
|
+
await expect(c.cancelSubscription('orig_1')).rejects.toThrow(/APP_STORE_WRITE_ENABLED/);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('getSubscriptionStatus errors clearly when Server API creds are not configured', async () => {
|
|
122
|
+
const c = AppStoreClient.fromSettings(baseSettings);
|
|
123
|
+
await expect(c.getSubscriptionStatus('orig_1')).rejects.toThrow(/issuer_id\/key_id\/private_key_pem/);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// `getSubscriptionStatus` against a mocked Apple SDK — we don't hit the real API.
|
|
128
|
+
const mockGetAllSubscriptionStatuses = jest.fn();
|
|
129
|
+
jest.mock('../../../src/integrations/app-store/signed-data-verifier', () => {
|
|
130
|
+
const actual = jest.requireActual('../../../src/integrations/app-store/signed-data-verifier');
|
|
131
|
+
return {
|
|
132
|
+
...actual,
|
|
133
|
+
getAllSubscriptionStatuses: (...args: any[]) => mockGetAllSubscriptionStatuses(...args),
|
|
134
|
+
};
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe('AppStoreClient.getSubscriptionStatus (Server API)', () => {
|
|
138
|
+
beforeEach(() => {
|
|
139
|
+
mockGetAllSubscriptionStatuses.mockReset();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const credSettings: AppStoreSettings = {
|
|
143
|
+
bundle_id: 'com.example.app',
|
|
144
|
+
environment: 'sandbox',
|
|
145
|
+
issuer_id: 'iss_1',
|
|
146
|
+
key_id: 'KEY_ID_1',
|
|
147
|
+
private_key_pem: '-----BEGIN PRIVATE KEY-----\nfake\n-----END PRIVATE KEY-----',
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
it('returns decoded transaction payload when Apple returns matching txn', async () => {
|
|
151
|
+
const inner = (() => {
|
|
152
|
+
const h = Buffer.from(JSON.stringify({ alg: 'ES256' })).toString('base64url');
|
|
153
|
+
const p = Buffer.from(
|
|
154
|
+
JSON.stringify({
|
|
155
|
+
transactionId: 'txn_latest',
|
|
156
|
+
originalTransactionId: 'orig_X',
|
|
157
|
+
productId: 'sub_06',
|
|
158
|
+
bundleId: 'com.example.app',
|
|
159
|
+
environment: 'Sandbox',
|
|
160
|
+
expiresDate: 1900000000000,
|
|
161
|
+
})
|
|
162
|
+
).toString('base64url');
|
|
163
|
+
return `${h}.${p}.sig`;
|
|
164
|
+
})();
|
|
165
|
+
mockGetAllSubscriptionStatuses.mockResolvedValue({
|
|
166
|
+
data: [{ lastTransactions: [{ originalTransactionId: 'orig_X', signedTransactionInfo: inner }] }],
|
|
167
|
+
});
|
|
168
|
+
const c = AppStoreClient.fromSettings(credSettings);
|
|
169
|
+
const result = await c.getSubscriptionStatus('orig_X');
|
|
170
|
+
expect(result?.transactionId).toBe('txn_latest');
|
|
171
|
+
expect(mockGetAllSubscriptionStatuses).toHaveBeenCalledWith(
|
|
172
|
+
'orig_X',
|
|
173
|
+
expect.objectContaining({ bundleId: 'com.example.app' })
|
|
174
|
+
);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('returns null when Apple returns no matching transaction', async () => {
|
|
178
|
+
mockGetAllSubscriptionStatuses.mockResolvedValue({ data: [] });
|
|
179
|
+
const c = AppStoreClient.fromSettings(credSettings);
|
|
180
|
+
expect(await c.getSubscriptionStatus('orig_unknown')).toBeNull();
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe('AppStoreClient.verifyNotificationPayload (mock-phase decode)', () => {
|
|
185
|
+
let client: AppStoreClient;
|
|
186
|
+
beforeEach(() => {
|
|
187
|
+
client = AppStoreClient.fromSettings(baseSettings);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('decodes a well-formed signedPayload', async () => {
|
|
191
|
+
const jws = makeJws({
|
|
192
|
+
notificationType: 'DID_RENEW',
|
|
193
|
+
subtype: 'BILLING_RECOVERY',
|
|
194
|
+
notificationUUID: 'uuid-notif-1',
|
|
195
|
+
version: '2.0',
|
|
196
|
+
signedDate: 1700000000000,
|
|
197
|
+
data: {
|
|
198
|
+
bundleId: 'com.example.app',
|
|
199
|
+
environment: 'Sandbox',
|
|
200
|
+
signedTransactionInfo: 'inner-jws',
|
|
201
|
+
status: 1,
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
const decoded = await client.verifyNotificationPayload(jws);
|
|
205
|
+
expect(decoded.notificationType).toBe('DID_RENEW');
|
|
206
|
+
expect(decoded.subtype).toBe('BILLING_RECOVERY');
|
|
207
|
+
expect(decoded.data.environment).toBe('Sandbox');
|
|
208
|
+
expect(decoded.data.signedTransactionInfo).toBe('inner-jws');
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('rejects malformed JWS (not 3 segments)', async () => {
|
|
212
|
+
await expect(client.verifyNotificationPayload('a.b')).rejects.toThrow(/JWS format invalid/);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('rejects when notification bundleId disagrees with configured one', async () => {
|
|
216
|
+
const jws = makeJws({
|
|
217
|
+
notificationType: 'DID_RENEW',
|
|
218
|
+
notificationUUID: 'uuid-1',
|
|
219
|
+
version: '2.0',
|
|
220
|
+
signedDate: 1700000000000,
|
|
221
|
+
data: { bundleId: 'com.evil.app', environment: 'Sandbox' },
|
|
222
|
+
});
|
|
223
|
+
await expect(client.verifyNotificationPayload(jws)).rejects.toThrow(/bundleId mismatch/);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
describe('AppStoreClient.verifyLegacyReceipt', () => {
|
|
228
|
+
const withSecret: AppStoreSettings = { ...baseSettings, shared_secret: 'sk_test_secret' };
|
|
229
|
+
|
|
230
|
+
it('refuses when shared_secret is not configured', async () => {
|
|
231
|
+
const c = AppStoreClient.fromSettings(baseSettings);
|
|
232
|
+
await expect(c.verifyLegacyReceipt('base64-receipt')).rejects.toThrow(/shared_secret is required/);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('configures node-apple-receipt-verify with provided secret + both envs', async () => {
|
|
236
|
+
mockValidateAppleReceipt.mockResolvedValue([
|
|
237
|
+
{
|
|
238
|
+
bundleId: 'com.example.app',
|
|
239
|
+
productId: 'sub_06',
|
|
240
|
+
transactionId: 'txn_1',
|
|
241
|
+
originalTransactionId: 'orig_1',
|
|
242
|
+
purchaseDate: 1700000000000,
|
|
243
|
+
expirationDate: 1800000000000,
|
|
244
|
+
},
|
|
245
|
+
]);
|
|
246
|
+
const c = AppStoreClient.fromSettings(withSecret);
|
|
247
|
+
await c.verifyLegacyReceipt('base64-receipt');
|
|
248
|
+
expect(mockConfigApple).toHaveBeenCalledWith(
|
|
249
|
+
expect.objectContaining({
|
|
250
|
+
secret: 'sk_test_secret',
|
|
251
|
+
environment: ['production', 'sandbox'],
|
|
252
|
+
})
|
|
253
|
+
);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('normalizes one-item receipt to AppStoreTransactionPayload', async () => {
|
|
257
|
+
mockValidateAppleReceipt.mockResolvedValue([
|
|
258
|
+
{
|
|
259
|
+
bundleId: 'com.example.app',
|
|
260
|
+
productId: 'sub_06',
|
|
261
|
+
transactionId: 'txn_1',
|
|
262
|
+
originalTransactionId: 'orig_1',
|
|
263
|
+
purchaseDate: 1700000000000,
|
|
264
|
+
expirationDate: 1800000000000,
|
|
265
|
+
},
|
|
266
|
+
]);
|
|
267
|
+
const c = AppStoreClient.fromSettings(withSecret);
|
|
268
|
+
const result = await c.verifyLegacyReceipt('base64-receipt');
|
|
269
|
+
expect(result).toEqual({
|
|
270
|
+
transactionId: 'txn_1',
|
|
271
|
+
originalTransactionId: 'orig_1',
|
|
272
|
+
productId: 'sub_06',
|
|
273
|
+
purchaseDate: 1700000000000,
|
|
274
|
+
expiresDate: 1800000000000,
|
|
275
|
+
appAccountToken: undefined,
|
|
276
|
+
environment: 'Sandbox',
|
|
277
|
+
bundleId: 'com.example.app',
|
|
278
|
+
webOrderLineItemId: undefined,
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('picks the item with the latest expirationDate when receipt contains multiple', async () => {
|
|
283
|
+
mockValidateAppleReceipt.mockResolvedValue([
|
|
284
|
+
{ productId: 'sub_06', transactionId: 'old', expirationDate: 1500000000000 },
|
|
285
|
+
{ productId: 'sub_06', transactionId: 'new', expirationDate: 1800000000000 },
|
|
286
|
+
{ productId: 'sub_06', transactionId: 'mid', expirationDate: 1600000000000 },
|
|
287
|
+
]);
|
|
288
|
+
const c = AppStoreClient.fromSettings(withSecret);
|
|
289
|
+
const result = await c.verifyLegacyReceipt('base64-receipt');
|
|
290
|
+
expect(result.transactionId).toBe('new');
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('filters by expectedProductIds when provided', async () => {
|
|
294
|
+
mockValidateAppleReceipt.mockResolvedValue([
|
|
295
|
+
{ productId: 'other_sku', transactionId: 'wrong', expirationDate: 1900000000000 },
|
|
296
|
+
{ productId: 'sub_06', transactionId: 'right', expirationDate: 1800000000000 },
|
|
297
|
+
]);
|
|
298
|
+
const c = AppStoreClient.fromSettings(withSecret);
|
|
299
|
+
const result = await c.verifyLegacyReceipt('base64-receipt', { expectedProductIds: ['sub_06'] });
|
|
300
|
+
expect(result.transactionId).toBe('right');
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('throws when no item matches expectedProductIds', async () => {
|
|
304
|
+
mockValidateAppleReceipt.mockResolvedValue([
|
|
305
|
+
{ productId: 'unknown_sku', transactionId: 'x', expirationDate: 1800000000000 },
|
|
306
|
+
]);
|
|
307
|
+
const c = AppStoreClient.fromSettings(withSecret);
|
|
308
|
+
await expect(c.verifyLegacyReceipt('base64-receipt', { expectedProductIds: ['sub_06'] })).rejects.toThrow(
|
|
309
|
+
/no matching purchases/
|
|
310
|
+
);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('rejects when receipt bundleId disagrees with configured bundle_id', async () => {
|
|
314
|
+
mockValidateAppleReceipt.mockResolvedValue([
|
|
315
|
+
{
|
|
316
|
+
bundleId: 'com.evil.app',
|
|
317
|
+
productId: 'sub_06',
|
|
318
|
+
transactionId: 'txn_1',
|
|
319
|
+
originalTransactionId: 'orig_1',
|
|
320
|
+
expirationDate: 1800000000000,
|
|
321
|
+
},
|
|
322
|
+
]);
|
|
323
|
+
const c = AppStoreClient.fromSettings(withSecret);
|
|
324
|
+
await expect(c.verifyLegacyReceipt('base64-receipt')).rejects.toThrow(/bundleId mismatch/);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it('falls back to transactionId when originalTransactionId is absent', async () => {
|
|
328
|
+
mockValidateAppleReceipt.mockResolvedValue([
|
|
329
|
+
{ productId: 'sub_06', transactionId: 'txn_only', expirationDate: 1800000000000 },
|
|
330
|
+
]);
|
|
331
|
+
const c = AppStoreClient.fromSettings(withSecret);
|
|
332
|
+
const result = await c.verifyLegacyReceipt('base64-receipt');
|
|
333
|
+
expect(result.originalTransactionId).toBe('txn_only');
|
|
334
|
+
});
|
|
335
|
+
});
|