payment-kit 1.28.0 → 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.
Files changed (74) 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/migrations/0004_iap_foundation.sql +72 -0
  53. package/cloudflare/migrations/0005_iap_tenant_backfill.sql +112 -0
  54. package/cloudflare/run-build.js +1 -0
  55. package/cloudflare/shims/blocklet-sdk/verify-session.ts +44 -0
  56. package/cloudflare/shims/queue.ts +28 -2
  57. package/cloudflare/shims/sequelize-d1/model.ts +19 -0
  58. package/cloudflare/shims/sequelize-d1/operators.ts +14 -1
  59. package/cloudflare/tests/shims/queue-delayed-persist.spec.ts +87 -0
  60. package/cloudflare/worker.ts +59 -4
  61. package/cloudflare/wrangler.jsonc +7 -1
  62. package/cloudflare/wrangler.staging.json +2 -1
  63. package/package.json +10 -6
  64. package/scripts/seed-google-play.ts +79 -0
  65. package/src/components/payment-method/app-store.tsx +103 -0
  66. package/src/components/payment-method/form.tsx +7 -1
  67. package/src/components/payment-method/google-play.tsx +85 -0
  68. package/src/components/subscription/list.tsx +20 -0
  69. package/src/locales/en.tsx +63 -0
  70. package/src/locales/zh.tsx +63 -0
  71. package/src/pages/admin/billing/subscriptions/detail.tsx +80 -0
  72. package/src/pages/admin/customers/customers/detail.tsx +6 -0
  73. package/src/pages/admin/settings/payment-methods/create.tsx +12 -0
  74. package/src/pages/admin/settings/payment-methods/index.tsx +1 -1
@@ -0,0 +1,118 @@
1
+ /* eslint-disable @typescript-eslint/lines-between-class-members */
2
+ import { CreationOptional, DataTypes, InferAttributes, InferCreationAttributes, Model } from 'sequelize';
3
+ import type { LiteralUnion } from 'type-fest';
4
+
5
+ import { createIdGenerator } from '../../libs/util';
6
+
7
+ export const nextEntitlementGrantId = createIdGenerator('eg', 24);
8
+
9
+ // One Subscription (or other source) grants an Entitlement to a Customer.
10
+ // Multiple grants for the same (customer, entitlement) can coexist across channels;
11
+ // the "currently effective" grant is determined by status + active_until.
12
+ // eslint-disable-next-line prettier/prettier
13
+ export class EntitlementGrant extends Model<InferAttributes<EntitlementGrant>, InferCreationAttributes<EntitlementGrant>> {
14
+ declare id: CreationOptional<string>;
15
+ declare livemode: boolean;
16
+
17
+ declare entitlement_id: string;
18
+ declare customer_id: string;
19
+
20
+ declare source_subscription_id?: string;
21
+ declare source_channel: LiteralUnion<
22
+ 'stripe' | 'arcblock' | 'ethereum' | 'base' | 'app_store' | 'google_play',
23
+ string
24
+ >;
25
+
26
+ declare active_from: number;
27
+ declare active_until: number;
28
+
29
+ declare status: LiteralUnion<'active' | 'canceled' | 'expired' | 'voided', string>;
30
+
31
+ declare metadata?: Record<string, any>;
32
+
33
+ declare created_at: CreationOptional<Date>;
34
+ declare updated_at: CreationOptional<Date>;
35
+
36
+ public static readonly GENESIS_ATTRIBUTES = {
37
+ id: {
38
+ type: DataTypes.STRING(30),
39
+ primaryKey: true,
40
+ allowNull: false,
41
+ defaultValue: nextEntitlementGrantId,
42
+ },
43
+ livemode: {
44
+ type: DataTypes.BOOLEAN,
45
+ allowNull: false,
46
+ },
47
+ entitlement_id: {
48
+ type: DataTypes.STRING(30),
49
+ allowNull: false,
50
+ },
51
+ customer_id: {
52
+ type: DataTypes.STRING(18),
53
+ allowNull: false,
54
+ },
55
+ source_subscription_id: {
56
+ type: DataTypes.STRING(30),
57
+ allowNull: true,
58
+ },
59
+ source_channel: {
60
+ type: DataTypes.STRING(20),
61
+ allowNull: false,
62
+ },
63
+ active_from: {
64
+ type: DataTypes.INTEGER,
65
+ allowNull: false,
66
+ },
67
+ active_until: {
68
+ type: DataTypes.INTEGER,
69
+ allowNull: false,
70
+ },
71
+ status: {
72
+ type: DataTypes.STRING(20),
73
+ allowNull: false,
74
+ defaultValue: 'active',
75
+ },
76
+ metadata: {
77
+ type: DataTypes.JSON,
78
+ allowNull: true,
79
+ },
80
+ created_at: {
81
+ type: DataTypes.DATE,
82
+ defaultValue: DataTypes.NOW,
83
+ allowNull: false,
84
+ },
85
+ updated_at: {
86
+ type: DataTypes.DATE,
87
+ defaultValue: DataTypes.NOW,
88
+ allowNull: false,
89
+ },
90
+ };
91
+
92
+ public static initialize(sequelize: any) {
93
+ this.init(EntitlementGrant.GENESIS_ATTRIBUTES, {
94
+ sequelize,
95
+ modelName: 'EntitlementGrant',
96
+ tableName: 'entitlement_grants',
97
+ createdAt: 'created_at',
98
+ updatedAt: 'updated_at',
99
+ });
100
+ }
101
+
102
+ public static associate(models: any) {
103
+ this.belongsTo(models.Entitlement, {
104
+ foreignKey: 'entitlement_id',
105
+ as: 'entitlement',
106
+ });
107
+ this.belongsTo(models.Customer, {
108
+ foreignKey: 'customer_id',
109
+ as: 'customer',
110
+ });
111
+ this.belongsTo(models.Subscription, {
112
+ foreignKey: 'source_subscription_id',
113
+ as: 'subscription',
114
+ });
115
+ }
116
+ }
117
+
118
+ export type TEntitlementGrant = InferAttributes<EntitlementGrant>;
@@ -0,0 +1,48 @@
1
+ /* eslint-disable @typescript-eslint/lines-between-class-members */
2
+ import { CreationOptional, DataTypes, InferAttributes, InferCreationAttributes, Model } from 'sequelize';
3
+
4
+ // Join table: which products unlock which entitlements (many-to-many)
5
+ // eslint-disable-next-line prettier/prettier
6
+ export class EntitlementProduct extends Model<InferAttributes<EntitlementProduct>, InferCreationAttributes<EntitlementProduct>> {
7
+ declare entitlement_id: string;
8
+ declare product_id: string;
9
+ declare created_at: CreationOptional<Date>;
10
+ declare updated_at: CreationOptional<Date>;
11
+
12
+ public static readonly GENESIS_ATTRIBUTES = {
13
+ entitlement_id: {
14
+ type: DataTypes.STRING(30),
15
+ primaryKey: true,
16
+ allowNull: false,
17
+ },
18
+ product_id: {
19
+ type: DataTypes.STRING(30),
20
+ primaryKey: true,
21
+ allowNull: false,
22
+ },
23
+ created_at: {
24
+ type: DataTypes.DATE,
25
+ defaultValue: DataTypes.NOW,
26
+ allowNull: false,
27
+ },
28
+ updated_at: {
29
+ type: DataTypes.DATE,
30
+ defaultValue: DataTypes.NOW,
31
+ allowNull: false,
32
+ },
33
+ };
34
+
35
+ public static initialize(sequelize: any) {
36
+ this.init(EntitlementProduct.GENESIS_ATTRIBUTES, {
37
+ sequelize,
38
+ modelName: 'EntitlementProduct',
39
+ tableName: 'entitlement_products',
40
+ createdAt: 'created_at',
41
+ updatedAt: 'updated_at',
42
+ });
43
+ }
44
+
45
+ public static associate() {}
46
+ }
47
+
48
+ export type TEntitlementProduct = InferAttributes<EntitlementProduct>;
@@ -0,0 +1,86 @@
1
+ /* eslint-disable @typescript-eslint/lines-between-class-members */
2
+ import { CreationOptional, DataTypes, InferAttributes, InferCreationAttributes, Model } from 'sequelize';
3
+
4
+ import { createIdGenerator } from '../../libs/util';
5
+
6
+ export const nextEntitlementId = createIdGenerator('ent', 24);
7
+
8
+ // eslint-disable-next-line prettier/prettier
9
+ export class Entitlement extends Model<InferAttributes<Entitlement>, InferCreationAttributes<Entitlement>> {
10
+ declare id: CreationOptional<string>;
11
+ declare livemode: boolean;
12
+
13
+ declare key: string;
14
+ declare name?: string;
15
+ declare description?: string;
16
+
17
+ declare metadata?: Record<string, any>;
18
+
19
+ declare created_at: CreationOptional<Date>;
20
+ declare updated_at: CreationOptional<Date>;
21
+
22
+ public static readonly GENESIS_ATTRIBUTES = {
23
+ id: {
24
+ type: DataTypes.STRING(30),
25
+ primaryKey: true,
26
+ allowNull: false,
27
+ defaultValue: nextEntitlementId,
28
+ },
29
+ livemode: {
30
+ type: DataTypes.BOOLEAN,
31
+ allowNull: false,
32
+ },
33
+ key: {
34
+ type: DataTypes.STRING(64),
35
+ allowNull: false,
36
+ unique: true,
37
+ },
38
+ name: {
39
+ type: DataTypes.STRING(255),
40
+ allowNull: true,
41
+ },
42
+ description: {
43
+ type: DataTypes.TEXT,
44
+ allowNull: true,
45
+ },
46
+ metadata: {
47
+ type: DataTypes.JSON,
48
+ allowNull: true,
49
+ },
50
+ created_at: {
51
+ type: DataTypes.DATE,
52
+ defaultValue: DataTypes.NOW,
53
+ allowNull: false,
54
+ },
55
+ updated_at: {
56
+ type: DataTypes.DATE,
57
+ defaultValue: DataTypes.NOW,
58
+ allowNull: false,
59
+ },
60
+ };
61
+
62
+ public static initialize(sequelize: any) {
63
+ this.init(Entitlement.GENESIS_ATTRIBUTES, {
64
+ sequelize,
65
+ modelName: 'Entitlement',
66
+ tableName: 'entitlements',
67
+ createdAt: 'created_at',
68
+ updatedAt: 'updated_at',
69
+ });
70
+ }
71
+
72
+ public static associate(models: any) {
73
+ this.belongsToMany(models.Product, {
74
+ through: models.EntitlementProduct,
75
+ foreignKey: 'entitlement_id',
76
+ otherKey: 'product_id',
77
+ as: 'products',
78
+ });
79
+ this.hasMany(models.EntitlementGrant, {
80
+ foreignKey: 'entitlement_id',
81
+ as: 'grants',
82
+ });
83
+ }
84
+ }
85
+
86
+ export type TEntitlement = InferAttributes<Entitlement>;
@@ -39,6 +39,9 @@ import { PriceQuote } from './price-quote';
39
39
  import { ArchiveMetadata } from './archive-metadata';
40
40
  import { ArchiveLock } from './archive-lock';
41
41
  import { RevenueSnapshot } from './revenue-snapshot';
42
+ import { Entitlement } from './entitlement';
43
+ import { EntitlementGrant } from './entitlement-grant';
44
+ import { EntitlementProduct } from './entitlement-product';
42
45
 
43
46
  const models = {
44
47
  CheckoutSession,
@@ -81,6 +84,9 @@ const models = {
81
84
  ArchiveMetadata,
82
85
  ArchiveLock,
83
86
  RevenueSnapshot,
87
+ Entitlement,
88
+ EntitlementGrant,
89
+ EntitlementProduct,
84
90
  };
85
91
 
86
92
  export function initialize(sequelize: any) {
@@ -137,6 +143,9 @@ export * from './price-quote';
137
143
  export * from './archive-metadata';
138
144
  export * from './archive-lock';
139
145
  export * from './revenue-snapshot';
146
+ export * from './entitlement';
147
+ export * from './entitlement-grant';
148
+ export * from './entitlement-product';
140
149
 
141
150
  export type TPriceExpanded = TPrice & {
142
151
  object: 'price';
@@ -84,6 +84,14 @@ export class Invoice extends Model<InferAttributes<Invoice>, InferCreationAttrib
84
84
  declare tax: string;
85
85
  declare total: string;
86
86
 
87
+ // Three-segment amounts for cross-channel accounting (D-001 A).
88
+ // gross_amount = what the user actually paid (= total for non-IAP channels).
89
+ // platform_fee = amount Apple / Google withheld (0 for Stripe / on-chain).
90
+ // net_amount = what we actually received (= gross_amount - platform_fee).
91
+ declare gross_amount?: string;
92
+ declare platform_fee?: string;
93
+ declare net_amount?: string;
94
+
87
95
  declare amount_due: string; // total - amount_paid
88
96
  declare amount_paid: string;
89
97
  declare amount_remaining: string; // amount_due - amount_paid
@@ -250,6 +258,18 @@ export class Invoice extends Model<InferAttributes<Invoice>, InferCreationAttrib
250
258
  type: DataTypes.STRING(32),
251
259
  allowNull: false,
252
260
  },
261
+ gross_amount: {
262
+ type: DataTypes.STRING(32),
263
+ allowNull: true,
264
+ },
265
+ platform_fee: {
266
+ type: DataTypes.STRING(32),
267
+ defaultValue: '0',
268
+ },
269
+ net_amount: {
270
+ type: DataTypes.STRING(32),
271
+ allowNull: true,
272
+ },
253
273
  subtotal: {
254
274
  type: DataTypes.STRING(32),
255
275
  allowNull: false,
@@ -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<'stripe' | 'arcblock' | 'ethereum' | 'bitcoin' | 'base', string>;
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
  }
@@ -240,6 +265,42 @@ export class PaymentMethod extends Model<InferAttributes<PaymentMethod>, InferCr
240
265
  return client as JsonRpcProvider;
241
266
  }
242
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
+
243
304
  public static async supportAutoCharge(id: string) {
244
305
  const method = await PaymentMethod.findByPk(id);
245
306
  return method && CHARGE_SUPPORTED_CHAIN_TYPES.includes(method.type);
@@ -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 = {