payment-kit 1.28.0 → 1.29.1

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.
Files changed (76) hide show
  1. package/api/src/crons/index.ts +22 -0
  2. package/api/src/crons/retry-pending-events.ts +58 -0
  3. package/api/src/integrations/app-store/apple-root-certs.ts +26 -0
  4. package/api/src/integrations/app-store/client.ts +369 -0
  5. package/api/src/integrations/app-store/handlers/index.ts +46 -0
  6. package/api/src/integrations/app-store/handlers/subscription.ts +635 -0
  7. package/api/src/integrations/app-store/node-apple-receipt-verify.d.ts +17 -0
  8. package/api/src/integrations/app-store/notification-routing.ts +18 -0
  9. package/api/src/integrations/app-store/signed-data-verifier.ts +150 -0
  10. package/api/src/integrations/google-play/client.ts +276 -0
  11. package/api/src/integrations/google-play/handlers/index.ts +69 -0
  12. package/api/src/integrations/google-play/handlers/subscription.ts +565 -0
  13. package/api/src/integrations/google-play/handlers/voided.ts +106 -0
  14. package/api/src/integrations/google-play/setup.ts +43 -0
  15. package/api/src/integrations/google-play/verify.ts +251 -0
  16. package/api/src/integrations/iap-reconcile.ts +415 -0
  17. package/api/src/libs/audit.ts +38 -8
  18. package/api/src/libs/entitlement.ts +399 -0
  19. package/api/src/libs/env.ts +2 -0
  20. package/api/src/libs/security.ts +51 -0
  21. package/api/src/libs/subscription.ts +13 -1
  22. package/api/src/libs/util.ts +13 -0
  23. package/api/src/queues/event.ts +25 -19
  24. package/api/src/queues/webhook.ts +12 -2
  25. package/api/src/routes/entitlements.ts +105 -0
  26. package/api/src/routes/events.ts +2 -2
  27. package/api/src/routes/index.ts +12 -2
  28. package/api/src/routes/integrations/app-store.ts +267 -0
  29. package/api/src/routes/integrations/google-play.ts +324 -0
  30. package/api/src/routes/payment-methods.ts +130 -0
  31. package/api/src/store/migrations/20260526-iap-foundation.ts +105 -0
  32. package/api/src/store/models/customer.ts +14 -0
  33. package/api/src/store/models/entitlement-grant.ts +118 -0
  34. package/api/src/store/models/entitlement-product.ts +48 -0
  35. package/api/src/store/models/entitlement.ts +86 -0
  36. package/api/src/store/models/index.ts +9 -0
  37. package/api/src/store/models/invoice.ts +20 -0
  38. package/api/src/store/models/payment-method.ts +62 -1
  39. package/api/src/store/models/refund.ts +10 -0
  40. package/api/src/store/models/subscription.ts +14 -0
  41. package/api/src/store/models/types.ts +32 -0
  42. package/api/tests/integrations/app-store/client.spec.ts +335 -0
  43. package/api/tests/integrations/app-store/handlers.spec.ts +480 -0
  44. package/api/tests/integrations/app-store/notifications.spec.ts +381 -0
  45. package/api/tests/integrations/app-store/signed-data-verifier.spec.ts +72 -0
  46. package/api/tests/integrations/app-store/webhook-routing.spec.ts +27 -0
  47. package/api/tests/integrations/google-play/handlers.spec.ts +341 -0
  48. package/api/tests/integrations/google-play/verify.spec.ts +215 -0
  49. package/api/tests/integrations/iap-reconcile.spec.ts +237 -0
  50. package/api/tests/libs/entitlement.spec.ts +347 -0
  51. package/blocklet.yml +1 -1
  52. package/cloudflare/docs/2026-06-10-bundle-size-analysis.md +288 -0
  53. package/cloudflare/migrations/0004_iap_foundation.sql +72 -0
  54. package/cloudflare/migrations/0005_iap_tenant_backfill.sql +112 -0
  55. package/cloudflare/run-build.js +23 -1
  56. package/cloudflare/shims/blocklet-sdk/verify-session.ts +44 -0
  57. package/cloudflare/shims/node-fetch.ts +35 -0
  58. package/cloudflare/shims/queue.ts +28 -2
  59. package/cloudflare/shims/sequelize-d1/model.ts +19 -0
  60. package/cloudflare/shims/sequelize-d1/operators.ts +14 -1
  61. package/cloudflare/tests/shims/queue-delayed-persist.spec.ts +87 -0
  62. package/cloudflare/worker.ts +59 -4
  63. package/cloudflare/wrangler.jsonc +7 -1
  64. package/cloudflare/wrangler.staging.json +2 -1
  65. package/package.json +10 -6
  66. package/scripts/seed-google-play.ts +79 -0
  67. package/src/components/payment-method/app-store.tsx +103 -0
  68. package/src/components/payment-method/form.tsx +7 -1
  69. package/src/components/payment-method/google-play.tsx +85 -0
  70. package/src/components/subscription/list.tsx +20 -0
  71. package/src/locales/en.tsx +63 -0
  72. package/src/locales/zh.tsx +63 -0
  73. package/src/pages/admin/billing/subscriptions/detail.tsx +80 -0
  74. package/src/pages/admin/customers/customers/detail.tsx +6 -0
  75. package/src/pages/admin/settings/payment-methods/create.tsx +12 -0
  76. package/src/pages/admin/settings/payment-methods/index.tsx +1 -1
@@ -1310,6 +1310,55 @@ export default flat({
1310
1310
  tip: 'Number of blocks required since transaction execution',
1311
1311
  },
1312
1312
  },
1313
+ google_play: {
1314
+ package_name: {
1315
+ label: 'Package Name',
1316
+ tip: 'e.g. com.example.app, configured in Play Console',
1317
+ },
1318
+ service_account_json: {
1319
+ label: 'Service Account JSON',
1320
+ tip: 'Paste the full service account credentials JSON downloaded from Google Cloud Console',
1321
+ invalidJson: 'Not valid JSON',
1322
+ missingFields: 'JSON is missing client_email or private_key',
1323
+ detectedClient: 'Detected client',
1324
+ },
1325
+ pubsub_topic_name: {
1326
+ label: 'Pub/Sub Topic (optional)',
1327
+ tip: 'projects/<project-id>/topics/<topic-name>, for receiving RTDN',
1328
+ },
1329
+ },
1330
+ app_store: {
1331
+ bundle_id: {
1332
+ label: 'Bundle ID',
1333
+ tip: 'e.g. com.example.app, configured in App Store Connect',
1334
+ },
1335
+ environment: {
1336
+ label: 'Environment',
1337
+ tip: 'StoreKit 2 JWS environment must match this setting',
1338
+ production: 'Production',
1339
+ sandbox: 'Sandbox',
1340
+ },
1341
+ shared_secret: {
1342
+ label: 'Shared Secret (for StoreKit 1)',
1343
+ tip: 'App-Specific Shared Secret for legacy receipt verification. Not needed for StoreKit 2 JWS. Find it at App Store Connect → App Information → App-Specific Shared Secret',
1344
+ },
1345
+ serverApi: {
1346
+ heading: 'Server API Credentials (optional)',
1347
+ tip: 'Not required for StoreKit 2 JWS verification. Only needed when querying App Store Server API for subscription status. Provide all three or none.',
1348
+ },
1349
+ issuer_id: {
1350
+ label: 'Issuer ID',
1351
+ tip: 'App Store Connect API Issuer ID',
1352
+ },
1353
+ key_id: {
1354
+ label: 'Key ID',
1355
+ tip: 'App Store Connect API Key ID',
1356
+ },
1357
+ private_key_pem: {
1358
+ label: 'Private Key (.p8 contents)',
1359
+ tip: 'Paste the contents of the .p8 file downloaded from App Store Connect',
1360
+ },
1361
+ },
1313
1362
  },
1314
1363
  paymentCurrency: {
1315
1364
  name: 'Payment Currency',
@@ -1484,7 +1533,19 @@ export default flat({
1484
1533
  attention: 'Past due subscriptions',
1485
1534
  product: 'Product',
1486
1535
  collectionMethod: 'Billing',
1536
+ channel: 'Channel',
1487
1537
  currentPeriod: 'Current Period',
1538
+ iap: {
1539
+ googlePlayTitle: 'Google Play Purchase',
1540
+ appStoreTitle: 'App Store Purchase',
1541
+ purchaseToken: 'Purchase Token',
1542
+ orderId: 'Order ID',
1543
+ productId: 'Product ID',
1544
+ originalTransactionId: 'Original Transaction ID',
1545
+ transactionId: 'Transaction ID',
1546
+ expiryTime: 'Expires At',
1547
+ environment: 'Environment',
1548
+ },
1488
1549
  trialingPeriod: 'Trial Period',
1489
1550
  trialEnd: 'Trial ends {prefix} {date}',
1490
1551
  willEnd: 'Will end {prefix} {date}',
@@ -1605,6 +1666,8 @@ export default flat({
1605
1666
  email: 'Email',
1606
1667
  phone: 'Phone',
1607
1668
  invoicePrefix: 'Invoice Prefix',
1669
+ googlePlayUuid: 'Google Play UUID',
1670
+ appStoreUuid: 'App Store UUID',
1608
1671
  balance: 'Balance ({currency})',
1609
1672
  summary: {
1610
1673
  refund: 'Refunds',
@@ -1265,6 +1265,55 @@ export default flat({
1265
1265
  tip: '交易标记为确认需要的区块数',
1266
1266
  },
1267
1267
  },
1268
+ google_play: {
1269
+ package_name: {
1270
+ label: '应用包名',
1271
+ tip: '例如 com.example.app,在 Play Console 中配置',
1272
+ },
1273
+ service_account_json: {
1274
+ label: '服务账号 JSON',
1275
+ tip: '从 Google Cloud Console 下载的 service account credentials JSON,整段粘贴',
1276
+ invalidJson: '不是合法的 JSON',
1277
+ missingFields: 'JSON 缺少 client_email 或 private_key 字段',
1278
+ detectedClient: '识别到客户端',
1279
+ },
1280
+ pubsub_topic_name: {
1281
+ label: 'Pub/Sub 主题名(可选)',
1282
+ tip: 'projects/<project-id>/topics/<topic-name>,用于接收 RTDN',
1283
+ },
1284
+ },
1285
+ app_store: {
1286
+ bundle_id: {
1287
+ label: '应用 Bundle ID',
1288
+ tip: '例如 com.example.app,在 App Store Connect 中配置',
1289
+ },
1290
+ environment: {
1291
+ label: '环境',
1292
+ tip: 'StoreKit 2 JWS 中携带的 environment 必须与此匹配',
1293
+ production: '正式环境',
1294
+ sandbox: '沙盒环境',
1295
+ },
1296
+ shared_secret: {
1297
+ label: 'Shared Secret(StoreKit 1 用)',
1298
+ tip: 'App-Specific Shared Secret,用于 legacy receipt 校验。StoreKit 2 JWS 不需要。在 App Store Connect → App Information → App-Specific Shared Secret',
1299
+ },
1300
+ serverApi: {
1301
+ heading: 'Server API 凭据(可选)',
1302
+ tip: 'StoreKit 2 JWS 校验不需要凭据;只在调用 App Store Server API 查询订阅状态时使用。三项需同时填写。',
1303
+ },
1304
+ issuer_id: {
1305
+ label: 'Issuer ID',
1306
+ tip: 'App Store Connect API 的 Issuer ID',
1307
+ },
1308
+ key_id: {
1309
+ label: 'Key ID',
1310
+ tip: 'App Store Connect API 的 Key ID',
1311
+ },
1312
+ private_key_pem: {
1313
+ label: '私钥 (.p8 内容)',
1314
+ tip: '从 App Store Connect 下载的 .p8 文件内容,整段粘贴',
1315
+ },
1316
+ },
1268
1317
  },
1269
1318
  paymentCurrency: {
1270
1319
  name: '支付货币',
@@ -1452,7 +1501,19 @@ export default flat({
1452
1501
  product: '产品',
1453
1502
  attention: '将过期的订阅',
1454
1503
  collectionMethod: '计费',
1504
+ channel: '渠道',
1455
1505
  currentPeriod: '当前周期',
1506
+ iap: {
1507
+ googlePlayTitle: 'Google Play 订阅详情',
1508
+ appStoreTitle: 'App Store 订阅详情',
1509
+ purchaseToken: '购买令牌',
1510
+ orderId: '订单号',
1511
+ productId: '产品 ID',
1512
+ originalTransactionId: '原始交易 ID',
1513
+ transactionId: '本次交易 ID',
1514
+ expiryTime: '过期时间',
1515
+ environment: '环境',
1516
+ },
1456
1517
  trialingPeriod: '试用期',
1457
1518
  trialEnd: '试用期结束于 {date}',
1458
1519
  willEnd: '将于 {date} 结束',
@@ -1569,6 +1630,8 @@ export default flat({
1569
1630
  email: '电子邮件',
1570
1631
  phone: '电话',
1571
1632
  invoicePrefix: '账单前缀',
1633
+ googlePlayUuid: 'Google Play UUID',
1634
+ appStoreUuid: 'App Store UUID',
1572
1635
  balance: '余额 ({currency})',
1573
1636
  summary: {
1574
1637
  refund: '退款金额',
@@ -335,6 +335,86 @@ export default function SubscriptionDetail(props: { id: string }) {
335
335
  )}
336
336
  </InfoRowGroup>
337
337
  </Box>
338
+
339
+ {/* IAP details — Google Play / App Store */}
340
+ {(data.payment_details?.google_play || data.payment_details?.app_store) && (
341
+ <>
342
+ <Divider />
343
+ <Box className="section" sx={{ containerType: 'inline-size' }}>
344
+ <SectionHeader
345
+ title={
346
+ data.payment_details?.google_play
347
+ ? t('admin.subscription.iap.googlePlayTitle')
348
+ : t('admin.subscription.iap.appStoreTitle')
349
+ }
350
+ />
351
+ <InfoRowGroup
352
+ sx={{
353
+ '.info-row-value': { wordBreak: 'break-all' },
354
+ }}>
355
+ {(data as any).channel && (
356
+ <InfoRow label={t('admin.subscription.channel')} value={(data as any).channel} />
357
+ )}
358
+ {data.payment_details?.google_play && (
359
+ <>
360
+ <InfoRow
361
+ label={t('admin.subscription.iap.purchaseToken')}
362
+ value={data.payment_details.google_play.purchase_token}
363
+ />
364
+ <InfoRow
365
+ label={t('admin.subscription.iap.orderId')}
366
+ value={data.payment_details.google_play.order_id || '—'}
367
+ />
368
+ <InfoRow
369
+ label={t('admin.subscription.iap.productId')}
370
+ value={data.payment_details.google_play.product_id}
371
+ />
372
+ <InfoRow
373
+ label={t('admin.subscription.iap.expiryTime')}
374
+ value={
375
+ data.payment_details.google_play.expiry_time_millis
376
+ ? formatTime(Number(data.payment_details.google_play.expiry_time_millis))
377
+ : '—'
378
+ }
379
+ />
380
+ <InfoRow
381
+ label={t('admin.subscription.iap.environment')}
382
+ value={data.payment_details.google_play.environment || (data as any).environment || '—'}
383
+ />
384
+ </>
385
+ )}
386
+ {data.payment_details?.app_store && (
387
+ <>
388
+ <InfoRow
389
+ label={t('admin.subscription.iap.originalTransactionId')}
390
+ value={data.payment_details.app_store.original_transaction_id}
391
+ />
392
+ <InfoRow
393
+ label={t('admin.subscription.iap.transactionId')}
394
+ value={data.payment_details.app_store.transaction_id || '—'}
395
+ />
396
+ <InfoRow
397
+ label={t('admin.subscription.iap.productId')}
398
+ value={data.payment_details.app_store.product_id}
399
+ />
400
+ <InfoRow
401
+ label={t('admin.subscription.iap.expiryTime')}
402
+ value={
403
+ data.payment_details.app_store.expires_at
404
+ ? formatTime(data.payment_details.app_store.expires_at * 1000)
405
+ : '—'
406
+ }
407
+ />
408
+ <InfoRow
409
+ label={t('admin.subscription.iap.environment')}
410
+ value={data.payment_details.app_store.environment || (data as any).environment || '—'}
411
+ />
412
+ </>
413
+ )}
414
+ </InfoRowGroup>
415
+ </Box>
416
+ </>
417
+ )}
338
418
  <Divider />
339
419
  {/* Discount Information */}
340
420
  {(data as any).discountStats && (
@@ -349,6 +349,12 @@ export default function CustomerDetail(props: { id: string }) {
349
349
  <InfoRow label={t('common.createdAt')} value={formatTime(data.customer.created_at)} />
350
350
 
351
351
  <InfoRow label={t('admin.customer.invoicePrefix')} value={data.customer.invoice_prefix} />
352
+ {(data.customer as any).google_play_uuid && (
353
+ <InfoRow label={t('admin.customer.googlePlayUuid')} value={(data.customer as any).google_play_uuid} />
354
+ )}
355
+ {(data.customer as any).app_store_uuid && (
356
+ <InfoRow label={t('admin.customer.appStoreUuid')} value={(data.customer as any).app_store_uuid} />
357
+ )}
352
358
  <InfoRow label={t('common.updatedAt')} value={formatTime(data.customer.updated_at)} />
353
359
  {state.editing.customer && (
354
360
  <EditCustomer
@@ -52,6 +52,18 @@ export default function PaymentMethodCreate() {
52
52
  explorer_host: '',
53
53
  logo: '',
54
54
  },
55
+ google_play: {
56
+ package_name: '',
57
+ service_account_json: '',
58
+ pubsub_topic_name: '',
59
+ },
60
+ app_store: {
61
+ bundle_id: '',
62
+ environment: 'sandbox',
63
+ issuer_id: '',
64
+ key_id: '',
65
+ private_key_pem: '',
66
+ },
55
67
  },
56
68
  },
57
69
  });
@@ -453,7 +453,7 @@ export default function PaymentMethods() {
453
453
  addons={
454
454
  <>
455
455
  <Switch checked={method.active} disabled sx={{ cursor: 'default' }} />
456
- {method.type !== 'arcblock' && (
456
+ {!['arcblock', 'google_play', 'app_store'].includes(method.type) && (
457
457
  <IconButton
458
458
  onClick={(e) => {
459
459
  e.stopPropagation();