@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/dist/index.cjs +82 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +7 -7
- package/dist/index.mjs +83 -3
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/adapters/index.ts +2 -1
- package/src/components/NamiView.tsx +6 -0
- package/src/components/elements/NamiSegmentPicker.tsx +10 -1
- package/src/nami.ts +20 -2
- package/src/products/productStore.ts +63 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@namiml/expo-sdk",
|
|
3
|
-
"version": "3.4.5-dev.
|
|
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.
|
|
33
|
-
"@namiml/sdk-core": "3.4.5-dev.
|
|
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"
|
package/src/adapters/index.ts
CHANGED
|
@@ -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(
|
|
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
|
-
|
|
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
|
+
}
|