@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.
@@ -1,215 +1,209 @@
1
- /**
2
- * The provider port — **private**, never exported from `src/index.ts`.
3
- *
4
- * This is the boundary between the platform-neutral engine and a store-specific
5
- * adapter. It exists to make three things true:
6
- *
7
- * 1. No store type reaches the public surface, because only the adapter ever
8
- * sees one (FR-002).
9
- * 2. An iOS adapter can be added by implementing this interface, changing no
10
- * consumer call (FR-007).
11
- * 3. The fake store is a drop-in replacement, so every outcome and ordering is
12
- * testable with no device, network, or store account (NFR-007).
13
- *
14
- * `Provider*` types are the port's **own** normalized shapes, not the binding's.
15
- * The adapter translates; nothing above it sees a `react-native-iap` value.
16
- */
17
-
18
- import type { ProductType, Unsubscribe } from '../config/types';
19
-
20
- /** A store's own product identifier. Confined to configuration and this layer. */
21
- export type StoreProductId = string;
22
-
23
- /** Opaque handle the store uses to identify a transaction for finalization. */
24
- export type ProviderToken = string;
25
-
26
- export type ProviderPurchaseState = 'purchased' | 'pending' | 'unspecified';
27
-
28
- export interface ProviderOffer {
29
- offerKey: string;
30
- billingPeriod: string;
31
- formattedPrice: string;
32
- priceAmountMicros: number;
33
- currencyCode: string;
34
- trialPeriod?: string;
35
- introductoryPrice?: {
36
- formattedPrice: string;
37
- period: string;
38
- cycles: number;
39
- };
40
- /** As the **store** determined it. Never computed locally (FR-026). */
41
- isEligible: boolean;
42
- }
43
-
44
- export interface ProviderProduct {
45
- storeProductId: StoreProductId;
46
- productType: ProductType;
47
- title: string;
48
- description: string;
49
- formattedPrice: string;
50
- currencyCode: string;
51
- priceAmountMicros: number;
52
- offers: ProviderOffer[];
53
- }
54
-
55
- export interface ProviderPurchase {
56
- storeProductId: StoreProductId;
57
- transactionId: string;
58
- token: ProviderToken;
59
- state: ProviderPurchaseState;
60
- purchasedAt: number;
61
- isAcknowledged: boolean;
62
- /** Subscription-only, and only where the platform supplies it. */
63
- expiresAt?: number;
64
- willAutoRenew?: boolean;
65
- /**
66
- * Opaque material for the app's backend. Passed to the validation hook and
67
- * **never** logged or put in an event (FR-074).
68
- */
69
- verificationPayload?: string;
70
- }
71
-
72
- export type ProviderFailureReason =
73
- | 'user_cancelled'
74
- | 'already_owned'
75
- | 'ineligible'
76
- | 'unknown_product'
77
- | 'product_unavailable_in_region'
78
- | 'not_ready'
79
- | 'network_error'
80
- | 'timeout'
81
- | 'store_error';
82
-
83
- export type ProviderConnectOutcome =
84
- | { ok: true }
85
- | {
86
- ok: false;
87
- reason: 'no_billing_service' | 'billing_service_outdated' | 'platform_unsupported' | 'store_error';
88
- retryable: boolean;
89
- };
90
-
91
- export type ProviderPurchaseOutcome =
92
- | { ok: true; purchase: ProviderPurchase }
93
- | { ok: false; reason: ProviderFailureReason; detail?: string };
94
-
95
- export type ProviderPriceChangeOutcome =
96
- | { ok: true; confirmed: boolean }
97
- | { ok: false; reason: ProviderFailureReason };
98
-
99
- export interface ProviderPurchaseRequest {
100
- storeProductId: StoreProductId;
101
- productType: ProductType;
102
- offerKey?: string;
103
- /** App-supplied, passed straight through. Never generated or stored (FR-034). */
104
- obfuscatedAccountId?: string;
105
- /**
106
- * Plan change: the token of the active subscription being replaced. The store
107
- * migrates *that* purchase rather than creating a second one (FR-065).
108
- */
109
- replacePurchaseToken?: ProviderToken;
110
- /** Plan change: the store product id of the subscription being replaced. */
111
- replaceStoreProductId?: StoreProductId;
112
- prorationMode?: string;
113
- }
114
-
115
- /**
116
- * What the platform can actually do.
117
- *
118
- * This is how honest degradation is enforced rather than left to good intentions.
119
- * `reportsGraceAndHoldDistinctly` is `false` on Android, because client-side Play
120
- * Billing genuinely cannot separate the two: a grace-period subscription is still
121
- * returned as active, and one in account hold stops being returned at all, making
122
- * it indistinguishable from expired, cancelled, or refunded.
123
- *
124
- * The entitlement layer reads this flag and reports `indeterminate` instead of
125
- * fabricating a distinction from `willAutoRenew` — which cannot separate grace
126
- * from cancelled-but-valid either. Claiming otherwise would breach FR-058.
127
- */
128
- export interface ProviderCapabilities {
129
- supportsSubscriptions: boolean;
130
- supportsPlanChange: boolean;
131
- supportsPriceChangeConfirmation: boolean;
132
- reportsGraceAndHoldDistinctly: boolean;
133
- /** Externally-initiated purchases: false on Android, true for a future iOS adapter. */
134
- supportsExternalTransactions: boolean;
135
- /**
136
- * Whether this adapter can move real money.
137
- *
138
- * `true` for the store adapter, `false` for the fake store. This is what lets
139
- * FR-014's block be precise: the engine refuses a purchase when the
140
- * environment may not create a production transaction **and** the provider is
141
- * one that actually would. Without it the guard would either block the test
142
- * suite — which must be able to buy things — or wave real purchases through in
143
- * a development build.
144
- */
145
- createsRealTransactions: boolean;
146
- /**
147
- * Whether `launchPurchase` settles only once the user has left the store's
148
- * purchase surface.
149
- *
150
- * `true` for the Play adapter: the binding gives no "surface opened" signal and
151
- * reports the outcome only when the flow ends, so the time the user spends in
152
- * Play is inside the call. The engine then does not apply its launch timeout,
153
- * which would otherwise cancel a user still choosing a payment method (FR-035).
154
- * The binding always ends the flow with a result or an error, so the call
155
- * cannot hang. `false` for the fake store, whose launch returns at once.
156
- */
157
- purchaseOutcomeIncludesStoreTime: boolean;
158
- }
159
-
160
- export interface ProviderPort {
161
- connect(): Promise<ProviderConnectOutcome>;
162
- disconnect(): Promise<void>;
163
-
164
- /**
165
- * Reports a **real** disconnect.
166
- *
167
- * The engine must never infer connectivity from an unrelated call failing.
168
- * The upstream binding historically left its `connected` flag true after a
169
- * disconnect and had a `reconnect()` that could report success without
170
- * rebuilding anything (openiap#408), so detection belongs here, behind a
171
- * contract test.
172
- */
173
- observeConnection(
174
- listener: (connected: boolean, reason?: string) => void,
175
- ): Unsubscribe;
176
-
177
- queryProducts(
178
- storeProductIds: StoreProductId[],
179
- productType: ProductType,
180
- ): Promise<ProviderProduct[]>;
181
-
182
- /** Owned and pending purchases. Pending ones are **flagged, never filtered** (FR-037). */
183
- queryOwned(): Promise<ProviderPurchase[]>;
184
-
185
- launchPurchase(
186
- request: ProviderPurchaseRequest,
187
- ): Promise<ProviderPurchaseOutcome>;
188
-
189
- /** Store-initiated updates, including a pending purchase settling later (FR-039). */
190
- observePurchases(listener: (purchase: ProviderPurchase) => void): Unsubscribe;
191
-
192
- /**
193
- * Why a pending purchase the store has stopped reporting ended, if the store
194
- * says (FR-102). Optional, and synchronous because it is asked about a
195
- * transaction the store already dropped, not fetched.
196
- *
197
- * Play gives no reason — a declined or abandoned pending purchase simply
198
- * vanishes from `queryOwned` — so the Android adapter leaves this out and the
199
- * engine reports `cancelled`. It is never inferred from age: guessing the
200
- * store's pending window would be inventing a fact.
201
- */
202
- pendingEndReason?(transactionId: string): 'cancelled' | 'expired' | undefined;
203
-
204
- /** Must be safe to call for an already-finalized transaction (FR-044). */
205
- acknowledge(token: ProviderToken): Promise<void>;
206
- /** Must be safe to call twice; the item is granted once (FR-044). */
207
- consume(token: ProviderToken): Promise<void>;
208
-
209
- openSubscriptionManagement(storeProductId?: StoreProductId): Promise<void>;
210
- confirmPriceChange(
211
- storeProductId: StoreProductId,
212
- ): Promise<ProviderPriceChangeOutcome>;
213
-
214
- capabilities(): ProviderCapabilities;
215
- }
1
+ /**
2
+ * The provider port — **private**, never exported from `src/index.ts`.
3
+ *
4
+ * This is the boundary between the platform-neutral engine and a store-specific
5
+ * adapter. It exists to make three things true:
6
+ *
7
+ * 1. No store type reaches the public surface, because only the adapter ever
8
+ * sees one (FR-002).
9
+ * 2. An iOS adapter can be added by implementing this interface, changing no
10
+ * consumer call (FR-007).
11
+ * 3. The fake store is a drop-in replacement, so every outcome and ordering is
12
+ * testable with no device, network, or store account (NFR-007).
13
+ *
14
+ * `Provider*` types are the port's **own** normalized shapes, not the binding's.
15
+ * The adapter translates; nothing above it sees a `react-native-iap` value.
16
+ */
17
+
18
+ import type { ProductType, Unsubscribe } from '../config/types';
19
+
20
+ /** A store's own product identifier. Confined to configuration and this layer. */
21
+ export type StoreProductId = string;
22
+
23
+ /** Opaque handle the store uses to identify a transaction for finalization. */
24
+ export type ProviderToken = string;
25
+
26
+ export type ProviderPurchaseState = 'purchased' | 'pending' | 'unspecified';
27
+
28
+ export interface ProviderOffer {
29
+ offerKey: string;
30
+ billingPeriod: string;
31
+ formattedPrice: string;
32
+ priceAmountMicros: number;
33
+ currencyCode: string;
34
+ trialPeriod?: string;
35
+ introductoryPrice?: {
36
+ formattedPrice: string;
37
+ period: string;
38
+ cycles: number;
39
+ };
40
+ /** As the **store** determined it. Never computed locally (FR-026). */
41
+ isEligible: boolean;
42
+ }
43
+
44
+ export interface ProviderProduct {
45
+ storeProductId: StoreProductId;
46
+ productType: ProductType;
47
+ title: string;
48
+ description: string;
49
+ formattedPrice: string;
50
+ currencyCode: string;
51
+ priceAmountMicros: number;
52
+ offers: ProviderOffer[];
53
+ }
54
+
55
+ export interface ProviderPurchase {
56
+ storeProductId: StoreProductId;
57
+ transactionId: string;
58
+ token: ProviderToken;
59
+ state: ProviderPurchaseState;
60
+ purchasedAt: number;
61
+ isAcknowledged: boolean;
62
+ /** Subscription-only, and only where the platform supplies it. */
63
+ expiresAt?: number;
64
+ willAutoRenew?: boolean;
65
+ /**
66
+ * Opaque material for the app's backend. Passed to the validation hook and
67
+ * **never** logged or put in an event (FR-074).
68
+ */
69
+ verificationPayload?: string;
70
+ }
71
+
72
+ export type ProviderFailureReason =
73
+ | 'user_cancelled'
74
+ | 'already_owned'
75
+ | 'ineligible'
76
+ | 'unknown_product'
77
+ | 'product_unavailable_in_region'
78
+ | 'not_ready'
79
+ | 'network_error'
80
+ | 'timeout'
81
+ | 'store_error';
82
+
83
+ export type ProviderConnectOutcome =
84
+ | { ok: true }
85
+ | {
86
+ ok: false;
87
+ reason:
88
+ 'no_billing_service' | 'billing_service_outdated' | 'platform_unsupported' | 'store_error';
89
+ retryable: boolean;
90
+ };
91
+
92
+ export type ProviderPurchaseOutcome =
93
+ | { ok: true; purchase: ProviderPurchase }
94
+ | { ok: false; reason: ProviderFailureReason; detail?: string };
95
+
96
+ export type ProviderPriceChangeOutcome =
97
+ { ok: true; confirmed: boolean } | { ok: false; reason: ProviderFailureReason };
98
+
99
+ export interface ProviderPurchaseRequest {
100
+ storeProductId: StoreProductId;
101
+ productType: ProductType;
102
+ offerKey?: string;
103
+ /** App-supplied, passed straight through. Never generated or stored (FR-034). */
104
+ obfuscatedAccountId?: string;
105
+ /**
106
+ * Plan change: the token of the active subscription being replaced. The store
107
+ * migrates *that* purchase rather than creating a second one (FR-065).
108
+ */
109
+ replacePurchaseToken?: ProviderToken;
110
+ /** Plan change: the store product id of the subscription being replaced. */
111
+ replaceStoreProductId?: StoreProductId;
112
+ prorationMode?: string;
113
+ }
114
+
115
+ /**
116
+ * What the platform can actually do.
117
+ *
118
+ * This is how honest degradation is enforced rather than left to good intentions.
119
+ * `reportsGraceAndHoldDistinctly` is `false` on Android, because client-side Play
120
+ * Billing genuinely cannot separate the two: a grace-period subscription is still
121
+ * returned as active, and one in account hold stops being returned at all, making
122
+ * it indistinguishable from expired, cancelled, or refunded.
123
+ *
124
+ * The entitlement layer reads this flag and reports `indeterminate` instead of
125
+ * fabricating a distinction from `willAutoRenew` — which cannot separate grace
126
+ * from cancelled-but-valid either. Claiming otherwise would breach FR-058.
127
+ */
128
+ export interface ProviderCapabilities {
129
+ supportsSubscriptions: boolean;
130
+ supportsPlanChange: boolean;
131
+ supportsPriceChangeConfirmation: boolean;
132
+ reportsGraceAndHoldDistinctly: boolean;
133
+ /** Externally-initiated purchases: false on Android, true for a future iOS adapter. */
134
+ supportsExternalTransactions: boolean;
135
+ /**
136
+ * Whether this adapter can move real money.
137
+ *
138
+ * `true` for the store adapter, `false` for the fake store. This is what lets
139
+ * FR-014's block be precise: the engine refuses a purchase when the
140
+ * environment may not create a production transaction **and** the provider is
141
+ * one that actually would. Without it the guard would either block the test
142
+ * suite — which must be able to buy things — or wave real purchases through in
143
+ * a development build.
144
+ */
145
+ createsRealTransactions: boolean;
146
+ /**
147
+ * Whether `launchPurchase` settles only once the user has left the store's
148
+ * purchase surface.
149
+ *
150
+ * `true` for the Play adapter: the binding gives no "surface opened" signal and
151
+ * reports the outcome only when the flow ends, so the time the user spends in
152
+ * Play is inside the call. The engine then does not apply its launch timeout,
153
+ * which would otherwise cancel a user still choosing a payment method (FR-035).
154
+ * The binding always ends the flow with a result or an error, so the call
155
+ * cannot hang. `false` for the fake store, whose launch returns at once.
156
+ */
157
+ purchaseOutcomeIncludesStoreTime: boolean;
158
+ }
159
+
160
+ export interface ProviderPort {
161
+ connect(): Promise<ProviderConnectOutcome>;
162
+ disconnect(): Promise<void>;
163
+
164
+ /**
165
+ * Reports a **real** disconnect.
166
+ *
167
+ * The engine must never infer connectivity from an unrelated call failing.
168
+ * The upstream binding historically left its `connected` flag true after a
169
+ * disconnect and had a `reconnect()` that could report success without
170
+ * rebuilding anything (openiap#408), so detection belongs here, behind a
171
+ * contract test.
172
+ */
173
+ observeConnection(listener: (connected: boolean, reason?: string) => void): Unsubscribe;
174
+
175
+ queryProducts(
176
+ storeProductIds: StoreProductId[],
177
+ productType: ProductType,
178
+ ): Promise<ProviderProduct[]>;
179
+
180
+ /** Owned and pending purchases. Pending ones are **flagged, never filtered** (FR-037). */
181
+ queryOwned(): Promise<ProviderPurchase[]>;
182
+
183
+ launchPurchase(request: ProviderPurchaseRequest): Promise<ProviderPurchaseOutcome>;
184
+
185
+ /** Store-initiated updates, including a pending purchase settling later (FR-039). */
186
+ observePurchases(listener: (purchase: ProviderPurchase) => void): Unsubscribe;
187
+
188
+ /**
189
+ * Why a pending purchase the store has stopped reporting ended, if the store
190
+ * says (FR-102). Optional, and synchronous because it is asked about a
191
+ * transaction the store already dropped, not fetched.
192
+ *
193
+ * Play gives no reason — a declined or abandoned pending purchase simply
194
+ * vanishes from `queryOwned` — so the Android adapter leaves this out and the
195
+ * engine reports `cancelled`. It is never inferred from age: guessing the
196
+ * store's pending window would be inventing a fact.
197
+ */
198
+ pendingEndReason?(transactionId: string): 'cancelled' | 'expired' | undefined;
199
+
200
+ /** Must be safe to call for an already-finalized transaction (FR-044). */
201
+ acknowledge(token: ProviderToken): Promise<void>;
202
+ /** Must be safe to call twice; the item is granted once (FR-044). */
203
+ consume(token: ProviderToken): Promise<void>;
204
+
205
+ openSubscriptionManagement(storeProductId?: StoreProductId): Promise<void>;
206
+ confirmPriceChange(storeProductId: StoreProductId): Promise<ProviderPriceChangeOutcome>;
207
+
208
+ capabilities(): ProviderCapabilities;
209
+ }