@chipmobilesdk/rn-payment 0.4.0 → 0.5.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/CHANGELOG.md +482 -334
- package/README.md +925 -897
- package/package.json +52 -54
- package/skills/sdk-rn-payment/SKILL.md +175 -174
- package/src/adapter/createStoreProvider.ts +116 -118
- package/src/adapter/iap-adapter.ts +627 -660
- package/src/adapter/port.ts +209 -215
- package/src/config/types.ts +298 -310
- package/src/config/validate.ts +224 -232
- package/src/core/mutex.ts +25 -128
- package/src/core/retry.ts +95 -133
- package/src/core/timers.ts +272 -267
- package/src/diagnostics/errors.ts +124 -117
- package/src/diagnostics/events.ts +282 -266
- package/src/diagnostics/messageKeys.ts +93 -95
- package/src/engine/engine.ts +921 -999
- package/src/engine/finalize.ts +343 -346
- package/src/engine/foreground.ts +29 -0
- package/src/engine/products.ts +259 -259
- package/src/engine/purchase.ts +589 -614
- package/src/engine/subscriptions.ts +146 -157
- package/src/entitlements/aggregate.ts +261 -242
- package/src/entitlements/mapping.ts +67 -67
- package/src/entitlements/policy.ts +121 -129
- package/src/index.ts +5 -9
- package/src/persistence/mmkv-store.ts +158 -158
- package/src/persistence/snapshot.ts +220 -247
- package/src/react/PaymentProvider.tsx +90 -79
- package/src/react/hooks.ts +236 -274
- package/src/testing/conformance.ts +169 -0
- package/src/testing/fakeStore.ts +360 -393
- package/src/testing/index.ts +19 -10
|
@@ -1,157 +1,146 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Subscription plan changes and the route to the store's management screen (US12).
|
|
3
|
-
*
|
|
4
|
-
* Two platform rules drive this module:
|
|
5
|
-
*
|
|
6
|
-
* - **Proration is the app's decision, inside what the store permits** (FR-065).
|
|
7
|
-
* Whether a user who upgrades gets charged immediately, keeps their remaining
|
|
8
|
-
* time, or starts a fresh period is a product and pricing choice, so the
|
|
9
|
-
* package forwards it rather than picking.
|
|
10
|
-
* - **Cancellation belongs to the store.** Platforms forbid an app presenting its
|
|
11
|
-
* own cancellation screen, so this module offers only a route to the store's
|
|
12
|
-
* (FR-066) — there is deliberately no self-built alternative anywhere in the
|
|
13
|
-
* package.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import type {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
} from '../
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
*/
|
|
32
|
-
|
|
33
|
-
/** Switch now;
|
|
34
|
-
| '
|
|
35
|
-
/**
|
|
36
|
-
| '
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
| { kind: '
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
.
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
return { kind: 'failed', code: 'PAYMENT_UNKNOWN_PRODUCT' };
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const outcome = await deps.provider.confirmPriceChange(storeProductId);
|
|
151
|
-
if (!outcome.ok) {
|
|
152
|
-
return { kind: 'failed', code: 'PAYMENT_STORE_ERROR' };
|
|
153
|
-
}
|
|
154
|
-
return outcome.confirmed ? { kind: 'confirmed' } : { kind: 'dismissed' };
|
|
155
|
-
},
|
|
156
|
-
};
|
|
157
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Subscription plan changes and the route to the store's management screen (US12).
|
|
3
|
+
*
|
|
4
|
+
* Two platform rules drive this module:
|
|
5
|
+
*
|
|
6
|
+
* - **Proration is the app's decision, inside what the store permits** (FR-065).
|
|
7
|
+
* Whether a user who upgrades gets charged immediately, keeps their remaining
|
|
8
|
+
* time, or starts a fresh period is a product and pricing choice, so the
|
|
9
|
+
* package forwards it rather than picking.
|
|
10
|
+
* - **Cancellation belongs to the store.** Platforms forbid an app presenting its
|
|
11
|
+
* own cancellation screen, so this module offers only a route to the store's
|
|
12
|
+
* (FR-066) — there is deliberately no self-built alternative anywhere in the
|
|
13
|
+
* package.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { ProductKey, PurchaseOptions, PurchaseResult } from '../config/types';
|
|
17
|
+
import type { ResolvedConfig } from '../config/validate';
|
|
18
|
+
import type { ProviderPort } from '../adapter/port';
|
|
19
|
+
import { storeIdForProductKey } from '../entitlements/mapping';
|
|
20
|
+
import type { PersistedSnapshot } from '../persistence/snapshot';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* How already-paid time is handled when a plan changes.
|
|
24
|
+
*
|
|
25
|
+
* Named for the user-visible effect rather than mirroring a store's enum, so the
|
|
26
|
+
* same vocabulary will map onto StoreKit later (FR-004).
|
|
27
|
+
*/
|
|
28
|
+
export type ProrationMode =
|
|
29
|
+
/** Switch now; credit unused time against the new plan. The usual upgrade. */
|
|
30
|
+
| 'immediate_with_time_proration'
|
|
31
|
+
/** Switch now; charge the full new price immediately and restart the period. */
|
|
32
|
+
| 'immediate_and_charge_full_price'
|
|
33
|
+
/** Switch now; charge the prorated difference for the current period. */
|
|
34
|
+
| 'immediate_and_charge_prorated_price'
|
|
35
|
+
/** Keep the current plan until it expires, then start the new one. Usual downgrade. */
|
|
36
|
+
| 'deferred';
|
|
37
|
+
|
|
38
|
+
/** Store-specific replacement codes stay here, behind the neutral vocabulary above. */
|
|
39
|
+
const PRORATION_CODES: Record<ProrationMode, string> = {
|
|
40
|
+
immediate_with_time_proration: 'WITH_TIME_PRORATION',
|
|
41
|
+
immediate_and_charge_full_price: 'CHARGE_FULL_PRICE',
|
|
42
|
+
immediate_and_charge_prorated_price: 'CHARGE_PRORATED_PRICE',
|
|
43
|
+
deferred: 'DEFERRED',
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type PriceChangeResult =
|
|
47
|
+
| { kind: 'confirmed' }
|
|
48
|
+
| { kind: 'dismissed' }
|
|
49
|
+
/** The platform has no client-side flow; it notifies the user itself (FR-067). */
|
|
50
|
+
| { kind: 'unsupported' }
|
|
51
|
+
| { kind: 'failed'; code: string };
|
|
52
|
+
|
|
53
|
+
export interface SubscriptionsDeps {
|
|
54
|
+
resolved: ResolvedConfig;
|
|
55
|
+
provider: ProviderPort;
|
|
56
|
+
snapshot: () => PersistedSnapshot;
|
|
57
|
+
purchase: (
|
|
58
|
+
productKey: ProductKey,
|
|
59
|
+
options?: PurchaseOptions & { replaceProductKey?: ProductKey; prorationMode?: string },
|
|
60
|
+
) => Promise<PurchaseResult>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface Subscriptions {
|
|
64
|
+
changePlan(
|
|
65
|
+
fromProductKey: ProductKey,
|
|
66
|
+
toProductKey: ProductKey,
|
|
67
|
+
mode: ProrationMode,
|
|
68
|
+
options?: PurchaseOptions,
|
|
69
|
+
): Promise<PurchaseResult>;
|
|
70
|
+
openManagement(productKey?: ProductKey): Promise<void>;
|
|
71
|
+
confirmPriceChange(productKey: ProductKey): Promise<PriceChangeResult>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function createSubscriptions(deps: SubscriptionsDeps): Subscriptions {
|
|
75
|
+
const { resolved } = deps;
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
async changePlan(fromProductKey, toProductKey, mode, options): Promise<PurchaseResult> {
|
|
79
|
+
if (!deps.provider.capabilities().supportsPlanChange) {
|
|
80
|
+
return { kind: 'unsupported' };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const target = resolved.entries.get(toProductKey);
|
|
84
|
+
if (!target) {
|
|
85
|
+
return { kind: 'unknown_product', productKey: toProductKey };
|
|
86
|
+
}
|
|
87
|
+
if (!resolved.entries.has(fromProductKey)) {
|
|
88
|
+
return { kind: 'unknown_product', productKey: fromProductKey };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// The store needs the *token* of the subscription being replaced, not its
|
|
92
|
+
// product id — that is how it knows which active purchase to migrate, and
|
|
93
|
+
// it is why a plan change needs a live local record rather than just two
|
|
94
|
+
// product keys.
|
|
95
|
+
const active = deps
|
|
96
|
+
.snapshot()
|
|
97
|
+
.purchases.find(
|
|
98
|
+
p =>
|
|
99
|
+
p.productKey === fromProductKey &&
|
|
100
|
+
p.productType === 'subscription' &&
|
|
101
|
+
p.state === 'purchased',
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
if (!active) {
|
|
105
|
+
// No subscription to replace. Treated as a plain purchase would be
|
|
106
|
+
// surprising — the caller asked to *change* something that is not there,
|
|
107
|
+
// which almost always means its own state is stale.
|
|
108
|
+
return { kind: 'already_owned', productKey: fromProductKey };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Routed through the normal purchase flow on purpose: a plan change is a
|
|
112
|
+
// purchase, so it must inherit the single-flight lock, the environment
|
|
113
|
+
// guard, validation, and the finalization queue rather than reimplementing
|
|
114
|
+
// any of them.
|
|
115
|
+
return deps.purchase(toProductKey, {
|
|
116
|
+
...options,
|
|
117
|
+
replaceProductKey: fromProductKey,
|
|
118
|
+
prorationMode: PRORATION_CODES[mode],
|
|
119
|
+
});
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
async openManagement(productKey?: ProductKey): Promise<void> {
|
|
123
|
+
const storeProductId = productKey ? storeIdForProductKey(resolved, productKey) : undefined;
|
|
124
|
+
// No-ops rather than throwing where the platform has no such screen: this
|
|
125
|
+
// is wired to a button, and a button that crashes is worse than one that
|
|
126
|
+
// does nothing on a platform that cannot honour it.
|
|
127
|
+
await deps.provider.openSubscriptionManagement(storeProductId);
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
async confirmPriceChange(productKey): Promise<PriceChangeResult> {
|
|
131
|
+
if (!deps.provider.capabilities().supportsPriceChangeConfirmation) {
|
|
132
|
+
return { kind: 'unsupported' };
|
|
133
|
+
}
|
|
134
|
+
const storeProductId = storeIdForProductKey(resolved, productKey);
|
|
135
|
+
if (!storeProductId) {
|
|
136
|
+
return { kind: 'failed', code: 'PAYMENT_UNKNOWN_PRODUCT' };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const outcome = await deps.provider.confirmPriceChange(storeProductId);
|
|
140
|
+
if (!outcome.ok) {
|
|
141
|
+
return { kind: 'failed', code: 'PAYMENT_STORE_ERROR' };
|
|
142
|
+
}
|
|
143
|
+
return outcome.confirmed ? { kind: 'confirmed' } : { kind: 'dismissed' };
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|