@onborn/billing 0.1.0-beta.4 → 0.1.0-beta.5

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 CHANGED
@@ -1,5 +1,11 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.0-beta.5
4
+
5
+ - Wait for the native store connection before loading localized products,
6
+ purchasing, or restoring. This prevents a cold-open paywall from showing an
7
+ unavailable state that only clears after a manual retry.
8
+
3
9
  ## 0.1.0-beta.4
4
10
 
5
11
  - Surface native store product-loading failures instead of silently falling
@@ -3,6 +3,8 @@ import type { OnbornBillingAdapter } from "./types";
3
3
  export type ExpoIapBillingAdapterOptions = {
4
4
  /** Additional subscription SKUs that should share the adapter product cache. */
5
5
  productIds?: string[];
6
+ /** Maximum time an operation waits for the native store connection. */
7
+ connectionTimeoutMs?: number;
6
8
  purchaseTimeoutMs?: number;
7
9
  purchaseRecoveryDelayMs?: number;
8
10
  productCacheMs?: number;
package/dist/expo-iap.js CHANGED
@@ -3,6 +3,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
3
3
  import { Platform } from "react-native";
4
4
  import { createNativeStoresBillingAdapter } from "./adapters/nativeStores";
5
5
  const DEFAULT_PURCHASE_TIMEOUT_MS = 2 * 60 * 1000;
6
+ const DEFAULT_CONNECTION_TIMEOUT_MS = 15_000;
6
7
  const DEFAULT_RECOVERY_DELAY_MS = 750;
7
8
  const DEFAULT_PRODUCT_CACHE_MS = 10_000;
8
9
  const DEFAULT_PRODUCT_RETRY_DELAY_MS = 500;
@@ -15,6 +16,8 @@ const DEFAULT_RESTORE_SETTLE_DELAY_MS = 300;
15
16
  */
16
17
  export function useExpoIapBillingAdapter(options = {}) {
17
18
  const pendingPurchaseRef = useRef(null);
19
+ const connectedRef = useRef(false);
20
+ const connectionWaitersRef = useRef(new Set());
18
21
  const productCacheRef = useRef(null);
19
22
  const productRequestRef = useRef(null);
20
23
  const knownProductIdsRef = useRef(new Set(normalizeIds(options.productIds)));
@@ -59,13 +62,48 @@ export function useExpoIapBillingAdapter(options = {}) {
59
62
  settlePendingPurchase((current) => current.reject(normalized));
60
63
  },
61
64
  });
65
+ useEffect(() => {
66
+ connectedRef.current = connected;
67
+ if (!connected)
68
+ return;
69
+ for (const waiter of connectionWaitersRef.current) {
70
+ clearTimeout(waiter.timeout);
71
+ waiter.resolve();
72
+ }
73
+ connectionWaitersRef.current.clear();
74
+ }, [connected]);
75
+ const waitForConnection = useCallback(async () => {
76
+ if (connectedRef.current)
77
+ return;
78
+ await new Promise((resolve, reject) => {
79
+ const waiter = {
80
+ resolve,
81
+ reject,
82
+ timeout: setTimeout(() => {
83
+ connectionWaitersRef.current.delete(waiter);
84
+ reject(new Error("Native store connection timed out."));
85
+ }, options.connectionTimeoutMs ?? DEFAULT_CONNECTION_TIMEOUT_MS),
86
+ };
87
+ connectionWaitersRef.current.add(waiter);
88
+ // The connection may have completed between the first check and
89
+ // registering this waiter.
90
+ if (connectedRef.current) {
91
+ connectionWaitersRef.current.delete(waiter);
92
+ clearTimeout(waiter.timeout);
93
+ resolve();
94
+ }
95
+ });
96
+ }, [options.connectionTimeoutMs]);
62
97
  useEffect(() => () => {
63
98
  settlePendingPurchase((item) => item.reject(new Error("Purchase interrupted.")));
99
+ for (const waiter of connectionWaitersRef.current) {
100
+ clearTimeout(waiter.timeout);
101
+ waiter.reject(new Error("Native store connection was interrupted."));
102
+ }
103
+ connectionWaitersRef.current.clear();
64
104
  }, [settlePendingPurchase]);
65
105
  const loadProducts = useCallback(async (requestedProductIds, force = false) => {
66
- if (!connected) {
67
- throw new Error("Native store is not connected.");
68
- }
106
+ await waitForConnection();
69
107
  const requestedIds = normalizeIds(requestedProductIds);
70
108
  for (const productId of requestedIds) {
71
109
  knownProductIdsRef.current.add(productId);
@@ -111,10 +149,10 @@ export function useExpoIapBillingAdapter(options = {}) {
111
149
  }
112
150
  return matches;
113
151
  }, [
114
- connected,
115
152
  options.productCacheMs,
116
153
  options.productRetryCount,
117
154
  options.productRetryDelayMs,
155
+ waitForConnection,
118
156
  ]);
119
157
  const reloadProducts = useCallback(async (productIds = options.productIds ?? []) => {
120
158
  const normalizedIds = normalizeIds(productIds);
@@ -141,9 +179,7 @@ export function useExpoIapBillingAdapter(options = {}) {
141
179
  return nativeProducts.map(normalizeStoreProduct);
142
180
  },
143
181
  async purchaseProduct({ storeProductId }) {
144
- if (!connected) {
145
- throw new Error("Native store is not connected.");
146
- }
182
+ await waitForConnection();
147
183
  if (pendingPurchaseRef.current) {
148
184
  throw new Error("Another purchase is already in progress.");
149
185
  }
@@ -212,9 +248,7 @@ export function useExpoIapBillingAdapter(options = {}) {
212
248
  return normalizeStorePurchase(purchase);
213
249
  },
214
250
  async restorePurchases({ products: configuredProducts }) {
215
- if (!connected) {
216
- throw new Error("Native store is not connected.");
217
- }
251
+ await waitForConnection();
218
252
  const subscriptionIds = normalizeIds([
219
253
  ...configuredProducts.map((product) => product.storeProductId),
220
254
  ...knownProductIdsRef.current,
@@ -285,7 +319,6 @@ export function useExpoIapBillingAdapter(options = {}) {
285
319
  },
286
320
  };
287
321
  }, [
288
- connected,
289
322
  finishTransaction,
290
323
  loadProducts,
291
324
  options.purchaseTimeoutMs,
@@ -293,6 +326,7 @@ export function useExpoIapBillingAdapter(options = {}) {
293
326
  recoverPurchase,
294
327
  requestPurchase,
295
328
  settlePendingPurchase,
329
+ waitForConnection,
296
330
  ]);
297
331
  return {
298
332
  billingAdapter,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onborn/billing",
3
- "version": "0.1.0-beta.4",
3
+ "version": "0.1.0-beta.5",
4
4
  "description": "Headless billing, offerings, purchases, restores, and entitlements for Onborn.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {