@namiml/expo-sdk 3.4.5-dev.202607100416 → 3.4.5-dev.202607101515

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@namiml/expo-sdk",
3
- "version": "3.4.5-dev.202607100416",
3
+ "version": "3.4.5-dev.202607101515",
4
4
  "type": "module",
5
5
  "description": "Nami Expo SDK — paywall and subscription management for Expo apps",
6
6
  "main": "./dist/index.cjs",
@@ -29,8 +29,8 @@
29
29
  "prepublishOnly": "yarn clean && yarn build:prod"
30
30
  },
31
31
  "dependencies": {
32
- "@namiml/expo-nami-iap": "3.4.5-dev.202607100416",
33
- "@namiml/sdk-core": "3.4.5-dev.202607100416",
32
+ "@namiml/expo-nami-iap": "3.4.5-dev.202607101515",
33
+ "@namiml/sdk-core": "3.4.5-dev.202607101515",
34
34
  "react-native-qrcode-svg": "^6.3.21",
35
35
  "react-native-safe-area-context": "^5.6.0",
36
36
  "react-native-svg": "^15.15.4"
@@ -5,6 +5,7 @@ import { ExpoUIAdapter } from './expo-ui.adapter';
5
5
  import { ExpoPurchaseAdapter } from './expo-purchase.adapter';
6
6
 
7
7
  export const expoUIAdapter = new ExpoUIAdapter();
8
+ export const expoPurchaseAdapter = new ExpoPurchaseAdapter();
8
9
  let adaptersRegistered = false;
9
10
 
10
11
  export function registerExpoAdapters(): void {
@@ -18,7 +19,7 @@ export function registerExpoAdapters(): void {
18
19
  device: new ExpoDeviceAdapter(),
19
20
  ui: expoUIAdapter,
20
21
  });
21
- registerPurchaseAdapter(new ExpoPurchaseAdapter());
22
+ registerPurchaseAdapter(expoPurchaseAdapter);
22
23
  adaptersRegistered = true;
23
24
  } catch (error) {
24
25
  logger.warn('[NamiExpo] Failed to register Expo adapters.', error);
@@ -32,6 +32,7 @@ import { expoUIAdapter } from '../adapters';
32
32
  import { FocusProvider } from '../context/FocusContext';
33
33
  import { prewarmPaywallFonts, prepareAndLoadFontsWithTimeout } from '../utils/fonts';
34
34
  import { parseColor } from '../utils/styles';
35
+ import { backfill as backfillProducts, collectSkuRefIds } from '../products/productStore';
35
36
 
36
37
  export interface NamiViewProps {
37
38
  /** Campaign placement label */
@@ -541,6 +542,11 @@ export const NamiView: React.FC<NamiViewProps> = ({
541
542
 
542
543
  prewarmPaywallFonts([paywall, ...flowPaywalls]);
543
544
 
545
+ const launchSkuRefIds = collectSkuRefIds(
546
+ [paywall, ...flowPaywalls].filter((item): item is IPaywall => item != null),
547
+ );
548
+ if (launchSkuRefIds.length) void backfillProducts(launchSkuRefIds);
549
+
544
550
  if (initialPaywall?.fonts) {
545
551
  void prepareAndLoadFontsWithTimeout(initialPaywall.fonts);
546
552
  }
@@ -3,6 +3,8 @@ import { ScrollView, StyleSheet, View } from 'react-native';
3
3
  import { applyStyles, focusedStyleOverrides, parseSize } from '../../utils/styles';
4
4
  import { NamiSegmentPickerItem } from './NamiSegmentPickerItem';
5
5
  import { FocusScope, useFocusEnabled } from '../../context/FocusContext';
6
+ import { usePaywallContext } from '../../context/PaywallContext';
7
+ import { resolveComponentSmartText, withOverrides } from '../../utils/rendering';
6
8
 
7
9
  interface Props {
8
10
  component: any;
@@ -18,6 +20,7 @@ export const NamiSegmentPicker: React.FC<Props> = ({
18
20
  parentDirection,
19
21
  }) => {
20
22
  const focusEnabled = useFocusEnabled();
23
+ const ctx = usePaywallContext();
21
24
 
22
25
  // NAM-1213 CTV: track whether any segment item currently holds remote focus so the
23
26
  // whole container can apply its focused visual treatment (focusedFillColor etc.).
@@ -35,7 +38,13 @@ export const NamiSegmentPicker: React.FC<Props> = ({
35
38
 
36
39
  const direction = component.direction ?? 'horizontal';
37
40
  const parentFormId = component.formId ?? component.id ?? '';
38
- const segments = component.components ?? [];
41
+ const segments = (component.components ?? []).map((segment: any) => {
42
+ const smartTextSku = segment?.smartTextSku ?? segment?.sku;
43
+ const withConditions = segment?.conditionAttributes?.length
44
+ ? withOverrides(ctx, segment, smartTextSku)
45
+ : segment;
46
+ return resolveComponentSmartText(withConditions, ctx);
47
+ });
39
48
  const spacing = parseSize(component.spacing, scaleFactor) ?? 0;
40
49
 
41
50
  const inner = (
package/src/nami.ts CHANGED
@@ -1,10 +1,25 @@
1
1
  import './adapters';
2
- import { Nami as CoreNami } from '@namiml/sdk-core';
2
+ import { Nami as CoreNami, NamiPaywallManager, logger } from '@namiml/sdk-core';
3
+ import { backfill, collectSkuRefIds, reset as resetProductStore } from './products/productStore';
3
4
 
4
5
  type ResettableCoreNami = typeof CoreNami & {
5
6
  reset?: () => Promise<void>;
6
7
  };
7
8
 
9
+ /**
10
+ * Prefetch StoreKit/Play product details for every configured paywall's SKUs so that
11
+ * prices are available before any paywall mounts (mirrors the Apple SDK startup load).
12
+ * Fire-and-forget and error-safe: a failure here never blocks configure.
13
+ */
14
+ export async function prefetchProducts(): Promise<void> {
15
+ try {
16
+ const ids = collectSkuRefIds(NamiPaywallManager.allPaywalls());
17
+ if (ids.length) await backfill(ids);
18
+ } catch (error) {
19
+ logger.warn('[NamiExpo] Product prefetch skipped.', error);
20
+ }
21
+ }
22
+
8
23
  export class Nami {
9
24
  static get sdkVersion(): string {
10
25
  return CoreNami.sdkVersion();
@@ -15,10 +30,13 @@ export class Nami {
15
30
  }
16
31
 
17
32
  static async configure(config: Parameters<typeof CoreNami.configure>[0]) {
18
- return CoreNami.configure(config);
33
+ const result = await CoreNami.configure(config);
34
+ void prefetchProducts();
35
+ return result;
19
36
  }
20
37
 
21
38
  static async reset(): Promise<void> {
39
+ resetProductStore();
22
40
  await (CoreNami as ResettableCoreNami).reset?.();
23
41
  }
24
42
  }
@@ -0,0 +1,63 @@
1
+ import type { IPaywall, NamiProductDetails } from '@namiml/sdk-core';
2
+ import { NamiPaywallManager, logger } from '@namiml/sdk-core';
3
+ import { expoPurchaseAdapter } from '../adapters';
4
+
5
+ // Accumulated store product details, keyed by product_ref_id (== sku_ref_id).
6
+ const productsById = new Map<string, NamiProductDetails>();
7
+
8
+ // Serializes backfill runs so concurrent launches/prefetches don't double-fetch
9
+ // or race the map (parity with the Apple SDK's backfillTaskLock).
10
+ let pending: Promise<boolean> = Promise.resolve(false);
11
+
12
+ /** Collect unique store product ids (sku_ref_id) from the given paywalls' sku menus. */
13
+ export function collectSkuRefIds(paywalls: IPaywall[]): string[] {
14
+ const ids = new Set<string>();
15
+ for (const paywall of paywalls ?? []) {
16
+ for (const menu of paywall?.sku_menus ?? []) {
17
+ for (const sku of menu?.products ?? []) {
18
+ if (sku?.sku_ref_id) ids.add(sku.sku_ref_id);
19
+ }
20
+ }
21
+ }
22
+ return [...ids];
23
+ }
24
+
25
+ /**
26
+ * Fetch store product details for any of `skuRefIds` not already loaded, merge them into
27
+ * the accumulated set, and push the union to core via NamiPaywallManager.setProductDetails.
28
+ * Serialized; returns whether new products were added. Never throws.
29
+ */
30
+ export function backfill(skuRefIds: string[]): Promise<boolean> {
31
+ pending = pending.then(() => backfillOnce(skuRefIds));
32
+ return pending;
33
+ }
34
+
35
+ async function backfillOnce(skuRefIds: string[]): Promise<boolean> {
36
+ const missing = [...new Set(skuRefIds)].filter((id) => id && !productsById.has(id));
37
+ if (!missing.length) return false;
38
+ try {
39
+ const products = await expoPurchaseAdapter.getProducts(missing);
40
+ let added = false;
41
+ for (const product of products ?? []) {
42
+ if (product?.product_ref_id) {
43
+ productsById.set(product.product_ref_id, product);
44
+ added = true;
45
+ }
46
+ }
47
+ if (added) {
48
+ NamiPaywallManager.setProductDetails([...productsById.values()]);
49
+ }
50
+ return added;
51
+ } catch (error) {
52
+ logger.warn('[NamiExpo] Failed to load store products.', error);
53
+ return false;
54
+ }
55
+ }
56
+
57
+ /** Clear accumulated products. Called on Nami.reset so reconfigure doesn't serve stale data. */
58
+ export function reset(): void {
59
+ pending = pending.then(() => {
60
+ productsById.clear();
61
+ return false;
62
+ });
63
+ }