@namiml/expo-sdk 3.4.0-dev.202605060437

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.
Files changed (60) hide show
  1. package/dist/index.cjs +4000 -0
  2. package/dist/index.cjs.map +1 -0
  3. package/dist/index.d.ts +151 -0
  4. package/dist/index.mjs +3966 -0
  5. package/dist/index.mjs.map +1 -0
  6. package/nami-expo-nami-iap.tgz +0 -0
  7. package/package.json +92 -0
  8. package/src/adapters/expo-device.adapter.ts +106 -0
  9. package/src/adapters/expo-purchase.adapter.ts +79 -0
  10. package/src/adapters/expo-storage.adapter.ts +92 -0
  11. package/src/adapters/expo-ui.adapter.ts +57 -0
  12. package/src/adapters/index.ts +33 -0
  13. package/src/amazon-kepler.d.ts +7 -0
  14. package/src/components/NamiView.tsx +1006 -0
  15. package/src/components/PaywallScreen.tsx +245 -0
  16. package/src/components/TemplateRenderer.tsx +243 -0
  17. package/src/components/containers/NamiBackgroundContainer.tsx +103 -0
  18. package/src/components/containers/NamiCarousel.tsx +217 -0
  19. package/src/components/containers/NamiCollapseContainer.tsx +116 -0
  20. package/src/components/containers/NamiContainer.tsx +315 -0
  21. package/src/components/containers/NamiContentContainer.tsx +140 -0
  22. package/src/components/containers/NamiFooter.tsx +35 -0
  23. package/src/components/containers/NamiHeader.tsx +45 -0
  24. package/src/components/containers/NamiProductContainer.tsx +248 -0
  25. package/src/components/containers/NamiRepeatingGrid.tsx +81 -0
  26. package/src/components/containers/NamiResponsiveGrid.tsx +75 -0
  27. package/src/components/containers/NamiStack.tsx +69 -0
  28. package/src/components/elements/NamiButton.tsx +285 -0
  29. package/src/components/elements/NamiCountdownTimer.tsx +123 -0
  30. package/src/components/elements/NamiImage.tsx +177 -0
  31. package/src/components/elements/NamiPlayPauseButton.tsx +93 -0
  32. package/src/components/elements/NamiProgressBar.tsx +90 -0
  33. package/src/components/elements/NamiProgressIndicator.tsx +41 -0
  34. package/src/components/elements/NamiQRCode.tsx +51 -0
  35. package/src/components/elements/NamiRadioButton.tsx +62 -0
  36. package/src/components/elements/NamiSegmentPicker.tsx +67 -0
  37. package/src/components/elements/NamiSegmentPickerItem.tsx +184 -0
  38. package/src/components/elements/NamiSpacer.tsx +23 -0
  39. package/src/components/elements/NamiSymbol.tsx +104 -0
  40. package/src/components/elements/NamiText.tsx +311 -0
  41. package/src/components/elements/NamiToggleButton.tsx +102 -0
  42. package/src/components/elements/NamiToggleSwitch.tsx +64 -0
  43. package/src/components/elements/NamiVideo.kepler.tsx +638 -0
  44. package/src/components/elements/NamiVideo.tsx +133 -0
  45. package/src/components/elements/NamiVolumeButton.tsx +93 -0
  46. package/src/context/FocusContext.tsx +169 -0
  47. package/src/context/PaywallContext.tsx +343 -0
  48. package/src/global.d.ts +5 -0
  49. package/src/index.ts +62 -0
  50. package/src/nami.ts +24 -0
  51. package/src/react-native-qrcode-svg.d.ts +4 -0
  52. package/src/utils/actionHandler.ts +281 -0
  53. package/src/utils/fonts.ts +359 -0
  54. package/src/utils/iconMap.ts +67 -0
  55. package/src/utils/impression.ts +39 -0
  56. package/src/utils/rendering.ts +197 -0
  57. package/src/utils/smartText.ts +148 -0
  58. package/src/utils/styles.ts +668 -0
  59. package/src/utils/tvFocus.ts +31 -0
  60. package/src/utils/videoControls.ts +49 -0
@@ -0,0 +1,281 @@
1
+ import { Linking } from 'react-native';
2
+ import {
3
+ NamiEventEmitter, PAYWALL_ACTION_EVENT, NamiPaywallAction,
4
+ namiBuySKU,
5
+ PaywallManagerEvents,
6
+ hasCapability,
7
+ Capabilities,
8
+ logger,
9
+ storageService,
10
+ } from '@namiml/sdk-core';
11
+ import type {
12
+ NamiSKU, NamiPaywallComponentChange,
13
+ } from '@namiml/sdk-core';
14
+ import type { PaywallContextValue } from '../context/PaywallContext';
15
+ import { setVideoMuted, setVideoPlaying } from './videoControls';
16
+
17
+ export const ACTION_CLOSE_PAYWALL = 'namiClosePaywall';
18
+ export const ACTION_RESTORE_PURCHASES = 'namiRestorePurchases';
19
+ export const ACTION_SIGN_IN = 'namiSignIn';
20
+ export const ACTION_DEEP_LINK = 'namiDeeplink';
21
+ export const ACTION_BUY_SKU = 'namiBuySKU';
22
+ export const ACTION_NAVIGATE_TO_SCREEN = 'namiNavigateToScreen';
23
+ export const ACTION_SET_STATE = 'setState';
24
+ export const ACTION_SELECT_SKU = 'namiSelectSKU';
25
+ export const ACTION_PURCHASE_SELECTED = 'namiPurchaseSelectedSKU';
26
+ export const ACTION_RELOAD_PRODUCTS = 'namiReloadProducts';
27
+ export const ACTION_PLAY_VIDEO = 'namiPlayVideo';
28
+ export const ACTION_PAUSE_VIDEO = 'namiPauseVideo';
29
+ export const ACTION_MUTE_VIDEO = 'namiMuteVideo';
30
+ export const ACTION_UNMUTE_VIDEO = 'namiUnmuteVideo';
31
+
32
+ type NamiTapAction = {
33
+ function: string;
34
+ parameters?: Record<string, unknown>;
35
+ };
36
+
37
+ type ProductOffer = {
38
+ offer_ref_id?: string;
39
+ offer_type?: string;
40
+ price?: number;
41
+ };
42
+
43
+ type ProductDetailsWithOffers = {
44
+ offers?: ProductOffer[];
45
+ };
46
+
47
+ type ActionSku = NamiSKU & {
48
+ id?: string;
49
+ skuId?: string;
50
+ sku_ref_id?: string;
51
+ promoId?: string;
52
+ productDetails?: ProductDetailsWithOffers;
53
+ product_details?: ProductDetailsWithOffers;
54
+ };
55
+
56
+ type SetStateActionParams = {
57
+ formId?: string;
58
+ value?: string;
59
+ };
60
+
61
+ interface HandleActionParams {
62
+ onTap?: NamiTapAction;
63
+ sku?: NamiSKU;
64
+ componentChange?: NamiPaywallComponentChange;
65
+ ctx: PaywallContextValue;
66
+ onClose?: () => void;
67
+ }
68
+
69
+ export function handleAction({ onTap, sku, componentChange, ctx, onClose }: HandleActionParams): void {
70
+ if (!onTap?.function) return;
71
+
72
+ const eventData = ctx.getPaywallActionEventData();
73
+
74
+ switch (onTap.function) {
75
+ case ACTION_CLOSE_PAYWALL:
76
+ emitAction(NamiPaywallAction.CLOSE_PAYWALL, { ...eventData, componentChange });
77
+ closePaywallHandler(onClose);
78
+ break;
79
+
80
+ case ACTION_RESTORE_PURCHASES:
81
+ emitAction(NamiPaywallAction.RESTORE_PURCHASES, { ...eventData, componentChange });
82
+ restorePurchase();
83
+ break;
84
+
85
+ case ACTION_SIGN_IN:
86
+ emitAction(NamiPaywallAction.SIGN_IN, { ...eventData, componentChange });
87
+ signIn();
88
+ break;
89
+
90
+ case ACTION_DEEP_LINK: {
91
+ const url = asOptionalString(onTap.parameters?.url);
92
+ emitAction(NamiPaywallAction.DEEPLINK, { ...eventData, deeplinkUrl: url, componentChange });
93
+ deepLink(url);
94
+ break;
95
+ }
96
+
97
+ case ACTION_BUY_SKU: {
98
+ if (!sku) break;
99
+ ctx.setPurchaseInProgress(true);
100
+ const nextSku = buySKU(ctx, onTap, sku);
101
+ emitAction(NamiPaywallAction.BUY_SKU, { ...eventData, sku: nextSku, componentChange });
102
+ break;
103
+ }
104
+
105
+ case ACTION_SELECT_SKU: {
106
+ if (!sku) break;
107
+ const skuId = getSkuIdentifier(sku as ActionSku);
108
+ if (!skuId) break;
109
+ const products = { ...ctx.state.selectedProducts, [ctx.state.currentGroupId]: skuId };
110
+ ctx.setSelectedProducts(products);
111
+ emitAction(NamiPaywallAction.SELECT_SKU, { ...eventData, sku, componentChange });
112
+ break;
113
+ }
114
+
115
+ case ACTION_NAVIGATE_TO_SCREEN: {
116
+ const screen = asOptionalString(onTap.parameters?.screen);
117
+ if (screen) {
118
+ ctx.setCurrentPage(screen);
119
+ }
120
+ emitAction(NamiPaywallAction.PAGE_CHANGE, { ...eventData, componentChange });
121
+ break;
122
+ }
123
+
124
+ case ACTION_SET_STATE: {
125
+ const params = onTap.parameters as SetStateActionParams | undefined;
126
+ if (params?.formId) {
127
+ ctx.setCurrentFormId(params.formId, params.value);
128
+ } else {
129
+ ctx.setCurrentGroupData(ctx.state.currentGroupId, (ctx.state as any).currentGroupName ?? '');
130
+ }
131
+ emitAction(NamiPaywallAction.TOGGLE_CHANGE, { ...eventData, componentChange });
132
+ break;
133
+ }
134
+
135
+ case ACTION_PURCHASE_SELECTED: {
136
+ if (sku) {
137
+ ctx.setPurchase(true, sku);
138
+ }
139
+ emitAction(NamiPaywallAction.PURCHASE_SELECTED_SKU, { ...eventData, sku, componentChange });
140
+ break;
141
+ }
142
+
143
+ case ACTION_RELOAD_PRODUCTS:
144
+ emitAction(NamiPaywallAction.UNKNOWN, eventData);
145
+ break;
146
+
147
+ case ACTION_PLAY_VIDEO:
148
+ setVideoPlaying(true);
149
+ emitAction(NamiPaywallAction.UNKNOWN, { ...eventData, componentChange });
150
+ break;
151
+
152
+ case ACTION_PAUSE_VIDEO:
153
+ setVideoPlaying(false);
154
+ emitAction(NamiPaywallAction.UNKNOWN, { ...eventData, componentChange });
155
+ break;
156
+
157
+ case ACTION_MUTE_VIDEO:
158
+ setVideoMuted(true);
159
+ emitAction(NamiPaywallAction.UNKNOWN, { ...eventData, componentChange });
160
+ break;
161
+
162
+ case ACTION_UNMUTE_VIDEO:
163
+ setVideoMuted(false);
164
+ emitAction(NamiPaywallAction.UNKNOWN, { ...eventData, componentChange });
165
+ break;
166
+
167
+ default:
168
+ break;
169
+ }
170
+ }
171
+
172
+ function emitAction(action: NamiPaywallAction, data: Record<string, any>): void {
173
+ NamiEventEmitter.getInstance().emit(PAYWALL_ACTION_EVENT, { ...data, action });
174
+ }
175
+
176
+ export function shouldHoist(onTap?: Pick<NamiTapAction, 'function'>): boolean {
177
+ if (!onTap) return false;
178
+ return [
179
+ ACTION_SIGN_IN, ACTION_DEEP_LINK, ACTION_RESTORE_PURCHASES,
180
+ ACTION_CLOSE_PAYWALL, ACTION_BUY_SKU, ACTION_SELECT_SKU,
181
+ ].includes(onTap.function);
182
+ }
183
+
184
+ function closePaywallHandler(onClose?: () => void): void {
185
+ storageService.clearLaunchId();
186
+ if (!invokeListener(PaywallManagerEvents.Close)) {
187
+ onClose?.();
188
+ }
189
+ }
190
+
191
+ function restorePurchase(): void {
192
+ const thirdPartyTransCap = hasCapability(Capabilities.THIRD_PARTY_TRANSACTIONS);
193
+ if (!invokeListener(PaywallManagerEvents.Restore) && !thirdPartyTransCap) {
194
+ logger.warn('NamiPaywallManager.registerRestoreHandler is not registered, so restore uses the default Expo behavior.');
195
+ }
196
+ }
197
+
198
+ function signIn(): void {
199
+ invokeListener(PaywallManagerEvents.SignIn);
200
+ }
201
+
202
+ function deepLink(url?: string): void {
203
+ if (!url) {
204
+ logger.debug('Paywall deep link action invoked, but no url present.');
205
+ return;
206
+ }
207
+
208
+ if (!invokeListener(PaywallManagerEvents.DeeplinkAction, url)) {
209
+ Linking.canOpenURL(url).then((canOpen: boolean) => {
210
+ if (canOpen) {
211
+ Linking.openURL(url);
212
+ }
213
+ }).catch(() => {});
214
+ }
215
+ }
216
+
217
+ function buySKU(
218
+ ctx: PaywallContextValue,
219
+ onTap: NamiTapAction,
220
+ sku: NamiSKU,
221
+ ): NamiSKU {
222
+ const nextSku = { ...sku } as ActionSku;
223
+ const promoId = onTap.parameters?.promo;
224
+
225
+ if (promoId) {
226
+ nextSku.promoId = String(promoId);
227
+ } else {
228
+ const product = nextSku.productDetails ?? nextSku.product_details;
229
+ const trialOffer = getFreeTrialOffer(product);
230
+ if (trialOffer?.offer_ref_id) {
231
+ nextSku.promoId = String(trialOffer.offer_ref_id);
232
+ } else {
233
+ const firstOffer = product?.offers?.[0];
234
+ if (firstOffer?.offer_ref_id) {
235
+ nextSku.promoId = String(firstOffer.offer_ref_id);
236
+ }
237
+ }
238
+ }
239
+
240
+ handleClickedSKU(ctx, nextSku);
241
+ return nextSku;
242
+ }
243
+
244
+ function handleClickedSKU(ctx: PaywallContextValue, sku: NamiSKU): void {
245
+ if (hasCapability(Capabilities.THIRD_PARTY_TRANSACTIONS)) {
246
+ storageService.setPurchaseImpression();
247
+
248
+ if (!invokeListener(PaywallManagerEvents.BuySku, sku)) {
249
+ logger.warn(
250
+ 'In order for Nami to hand off the purchase, you must register a NamiBuySkuHandler with NamiPaywallManager.registerBuySkuHandler.',
251
+ );
252
+ }
253
+ return;
254
+ }
255
+
256
+ namiBuySKU(getSkuIdentifier(sku as ActionSku) ?? '');
257
+ }
258
+
259
+ function invokeListener(eventName: string, ...args: any[]): boolean {
260
+ const emitter = NamiEventEmitter.getInstance();
261
+ const hasListeners = emitter.listenerCount(eventName) > 0;
262
+ if (hasListeners) {
263
+ emitter.emit(eventName, ...args);
264
+ }
265
+ return hasListeners;
266
+ }
267
+
268
+ function getFreeTrialOffer(product?: ProductDetailsWithOffers): ProductOffer | undefined {
269
+ if (!product?.offers?.length) {
270
+ return undefined;
271
+ }
272
+ return product.offers.find((offer) => offer.offer_type === 'promo' && offer.price === 0);
273
+ }
274
+
275
+ function getSkuIdentifier(sku?: ActionSku): string | undefined {
276
+ return sku?.skuId ?? sku?.sku_ref_id ?? sku?.id;
277
+ }
278
+
279
+ function asOptionalString(value: unknown): string | undefined {
280
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
281
+ }
@@ -0,0 +1,359 @@
1
+ import { Platform } from 'react-native';
2
+ import { logger, type FontDetails, type IPaywall } from '@namiml/sdk-core';
3
+
4
+ type FontModule = {
5
+ loadAsync(map: Record<string, string | { uri: string }>): Promise<void>;
6
+ };
7
+
8
+ type PreparedFont = FontDetails & {
9
+ alias: string;
10
+ };
11
+ type HostedFontsMap = Record<string, FontDetails | undefined>;
12
+
13
+ type FontVariantKey = 'regular' | 'bold' | 'italic' | 'boldItalic';
14
+ export type ResolvedFontDescriptor = {
15
+ family?: string;
16
+ isHosted: boolean;
17
+ variant?: FontVariantKey;
18
+ };
19
+
20
+ let fontModule: FontModule | null = null;
21
+ try {
22
+ fontModule = require('expo-font');
23
+ } catch {
24
+ try {
25
+ fontModule = require('@amazon-devices/expo-font');
26
+ } catch {
27
+ fontModule = null;
28
+ }
29
+ }
30
+
31
+ const knownFontFaces = new Set<string>();
32
+ const loadedFonts = new Set<string>();
33
+ const loadingFonts = new Map<string, Promise<void>>();
34
+ let warnedMissingFontModule = false;
35
+ const warnedFailedFontLoads = new Set<string>();
36
+ const fontFamilyVariants = new Map<string, Map<FontVariantKey, string>>();
37
+ const aliasToFamilyKey = new Map<string, string>();
38
+ const KEPLER_VARIANT_COLLISION_FAMILY_KEYS = new Set<string>(['All-ProDisplayC']);
39
+
40
+ export async function prepareAndLoadFonts(
41
+ fontsObject?: HostedFontsMap,
42
+ ): Promise<boolean> {
43
+ if (!fontsObject) return false;
44
+
45
+ if (!fontModule) {
46
+ if (!warnedMissingFontModule && Object.keys(fontsObject).length > 0) {
47
+ warnedMissingFontModule = true;
48
+ logger.warn(
49
+ '[NamiExpo][fonts] Paywall requested hosted fonts, but expo-font is unavailable in the current app runtime.',
50
+ );
51
+ }
52
+ return false;
53
+ }
54
+
55
+ const fonts = collectHostedFonts(fontsObject);
56
+
57
+ for (const font of fonts) {
58
+ registerFontMetadata(font);
59
+ }
60
+
61
+ const results = await Promise.all(fonts.map((font) => loadFont(font)));
62
+ return results.some(Boolean);
63
+ }
64
+
65
+ export function prewarmPaywallFonts(paywalls: Array<IPaywall | null | undefined>): void {
66
+ const uniqueCollections = new Set<HostedFontsMap>();
67
+
68
+ for (const paywall of paywalls) {
69
+ if (!paywall?.fonts) {
70
+ continue;
71
+ }
72
+ uniqueCollections.add(paywall.fonts);
73
+ }
74
+
75
+ for (const fonts of uniqueCollections) {
76
+ void prepareAndLoadFonts(fonts);
77
+ }
78
+ }
79
+
80
+ export async function prepareAndLoadFontsWithTimeout(
81
+ fontsObject?: HostedFontsMap,
82
+ timeoutMs = 1200,
83
+ ): Promise<'ready' | 'timeout' | 'failed' | 'none'> {
84
+ if (!fontsObject || Object.keys(fontsObject).length === 0) {
85
+ return 'none';
86
+ }
87
+
88
+ let timer: ReturnType<typeof setTimeout> | undefined;
89
+ const result: 'ready' | 'failed' | 'timeout' = await Promise.race([
90
+ ensureFontsReady(fontsObject).then((value) => (value === 'none' ? 'failed' : value)),
91
+ new Promise<'timeout'>((resolve) => {
92
+ timer = setTimeout(() => resolve('timeout'), timeoutMs);
93
+ }),
94
+ ]);
95
+
96
+ if (timer) {
97
+ clearTimeout(timer);
98
+ }
99
+
100
+ return result;
101
+ }
102
+
103
+ export async function ensureFontsReady(
104
+ fontsObject?: HostedFontsMap,
105
+ ): Promise<'ready' | 'failed' | 'none'> {
106
+ if (!fontsObject || Object.keys(fontsObject).length === 0) {
107
+ return 'none';
108
+ }
109
+
110
+ await prepareAndLoadFonts(fontsObject);
111
+
112
+ const fonts = collectHostedFonts(fontsObject);
113
+
114
+ const allReady = fonts.every((font) => loadedFonts.has(font.alias || buildFontFaceFamily(font)));
115
+ return allReady ? 'ready' : 'failed';
116
+ }
117
+
118
+ function collectHostedFonts(fontsObject?: HostedFontsMap): PreparedFont[] {
119
+ if (!fontsObject) {
120
+ return [];
121
+ }
122
+
123
+ return Object.entries(fontsObject)
124
+ .flatMap(([fontName, value]) => {
125
+ if (!value?.file) {
126
+ return [];
127
+ }
128
+
129
+ return [{
130
+ ...value,
131
+ alias: sanitizeFontName(fontName),
132
+ } satisfies PreparedFont];
133
+ });
134
+ }
135
+
136
+ async function loadFont(font: PreparedFont): Promise<boolean> {
137
+ if (!fontModule) return false;
138
+
139
+ if (shouldSkipHostedFontLoad(font)) {
140
+ return false;
141
+ }
142
+
143
+ const family = font.alias || buildFontFaceFamily(font);
144
+ knownFontFaces.add(family);
145
+ if (loadedFonts.has(family)) return false;
146
+
147
+ const existing = loadingFonts.get(family);
148
+ if (existing) {
149
+ await existing;
150
+ return false;
151
+ }
152
+
153
+ let loaded = false;
154
+ const promise = fontModule
155
+ .loadAsync({ [family]: { uri: font.file } })
156
+ .then(() => {
157
+ loadedFonts.add(family);
158
+ loaded = true;
159
+ })
160
+ .catch((error) => {
161
+ if (!warnedFailedFontLoads.has(family)) {
162
+ warnedFailedFontLoads.add(family);
163
+ logger.warn(
164
+ `[NamiExpo][fonts] Failed to load hosted font "${family}" from "${font.file}".`,
165
+ error,
166
+ );
167
+ }
168
+ // Keep the renderer resilient if a hosted font is unavailable.
169
+ })
170
+ .finally(() => {
171
+ loadingFonts.delete(family);
172
+ });
173
+
174
+ loadingFonts.set(family, promise);
175
+ await promise;
176
+ return loaded;
177
+ }
178
+
179
+ export function buildFontFaceFamily(font: FontDetails): string {
180
+ return sanitizeFontName(`${font.family}-${font.style}`);
181
+ }
182
+
183
+ export function sanitizeFontName(value: string): string {
184
+ return value.replace(/ /g, '');
185
+ }
186
+
187
+ export function resolveFontFamily(
188
+ fontName?: string,
189
+ options?: { italic?: boolean; bold?: boolean },
190
+ ): string | undefined {
191
+ return resolveFontDescriptor(fontName, options).family;
192
+ }
193
+
194
+ export function resolveFontDescriptor(
195
+ fontName?: string,
196
+ options?: { italic?: boolean; bold?: boolean },
197
+ ): ResolvedFontDescriptor {
198
+ if (!fontName) {
199
+ return {
200
+ family: undefined,
201
+ isHosted: false,
202
+ };
203
+ }
204
+
205
+ const sanitized = sanitizeFontName(fontName);
206
+ const familyKey = aliasToFamilyKey.get(sanitized) ?? sanitized;
207
+
208
+ const exact = getKnownFontFamily(sanitized);
209
+ if (exact) {
210
+ return {
211
+ family: exact,
212
+ isHosted: true,
213
+ variant: getRegisteredFontVariant(familyKey, exact),
214
+ };
215
+ }
216
+
217
+ if (options?.bold || options?.italic) {
218
+ const knownVariant = getKnownVariantFamily(familyKey, options);
219
+ if (knownVariant) {
220
+ return {
221
+ family: knownVariant,
222
+ isHosted: true,
223
+ variant: getRegisteredFontVariant(familyKey, knownVariant),
224
+ };
225
+ }
226
+ }
227
+
228
+ const knownBase = getKnownVariantFamily(familyKey);
229
+ if (knownBase) {
230
+ return {
231
+ family: knownBase,
232
+ isHosted: true,
233
+ variant: getRegisteredFontVariant(familyKey, knownBase),
234
+ };
235
+ }
236
+
237
+ return {
238
+ family: undefined,
239
+ isHosted: false,
240
+ };
241
+ }
242
+
243
+ function registerFontMetadata(font: PreparedFont): void {
244
+ const familyKey = sanitizeFontName(font.family);
245
+ aliasToFamilyKey.set(font.alias, familyKey);
246
+ const variant = normalizeFontVariant(font.style);
247
+ const variants = fontFamilyVariants.get(familyKey) ?? new Map<FontVariantKey, string>();
248
+ variants.set(variant, font.alias);
249
+ fontFamilyVariants.set(familyKey, variants);
250
+ }
251
+
252
+ function getKnownFontFamily(candidate: string): string | undefined {
253
+ if (loadedFonts.has(candidate)) {
254
+ return candidate;
255
+ }
256
+
257
+ return undefined;
258
+ }
259
+
260
+ function getKnownVariantFamily(
261
+ candidate: string,
262
+ options?: { italic?: boolean; bold?: boolean },
263
+ ): string | undefined {
264
+ const variants = fontFamilyVariants.get(candidate);
265
+ if (!variants) {
266
+ return undefined;
267
+ }
268
+
269
+ const desired = buildRequestedVariantKey(options);
270
+ const exact = variants.get(desired);
271
+ if (exact && getKnownFontFamily(exact)) {
272
+ return exact;
273
+ }
274
+
275
+ const regular = variants.get('regular');
276
+ if (regular && getKnownFontFamily(regular)) {
277
+ return regular;
278
+ }
279
+
280
+ if (!options?.bold && !options?.italic) {
281
+ const bold = variants.get('bold');
282
+ if (bold && getKnownFontFamily(bold)) {
283
+ return bold;
284
+ }
285
+
286
+ const italic = variants.get('italic');
287
+ if (italic && getKnownFontFamily(italic)) {
288
+ return italic;
289
+ }
290
+
291
+ const boldItalic = variants.get('boldItalic');
292
+ if (boldItalic && getKnownFontFamily(boldItalic)) {
293
+ return boldItalic;
294
+ }
295
+ }
296
+
297
+ return undefined;
298
+ }
299
+
300
+ function getRegisteredFontVariant(
301
+ familyKey: string,
302
+ alias: string,
303
+ ): FontVariantKey | undefined {
304
+ const variants = fontFamilyVariants.get(familyKey);
305
+ if (!variants) {
306
+ return undefined;
307
+ }
308
+
309
+ for (const [variant, family] of variants.entries()) {
310
+ if (family === alias) {
311
+ return variant;
312
+ }
313
+ }
314
+
315
+ return undefined;
316
+ }
317
+
318
+ function buildRequestedVariantKey(
319
+ options?: { italic?: boolean; bold?: boolean },
320
+ ): FontVariantKey {
321
+ if (options?.bold && options?.italic) return 'boldItalic';
322
+ if (options?.bold) return 'bold';
323
+ if (options?.italic) return 'italic';
324
+ return 'regular';
325
+ }
326
+
327
+ function normalizeFontVariant(style?: string): FontVariantKey {
328
+ const normalized = sanitizeFontName(style ?? '').toLowerCase();
329
+ const hasBold = normalized.includes('bold');
330
+ const hasItalic = normalized.includes('italic');
331
+
332
+ if (hasBold && hasItalic) return 'boldItalic';
333
+ if (hasBold) return 'bold';
334
+ if (hasItalic) return 'italic';
335
+ return 'regular';
336
+ }
337
+
338
+ function shouldSkipHostedFontLoad(font: PreparedFont): boolean {
339
+ const isTelevisionRuntime = Platform.isTV === true;
340
+ if (!isTelevisionRuntime) {
341
+ return false;
342
+ }
343
+
344
+ const familyKey = sanitizeFontName(font.family);
345
+ if (!KEPLER_VARIANT_COLLISION_FAMILY_KEYS.has(familyKey)) {
346
+ return false;
347
+ }
348
+
349
+ const variant = normalizeFontVariant(font.style);
350
+ return variant === 'italic' || variant === 'boldItalic';
351
+ }
352
+
353
+ export function inferFontNameVariant(fontName?: string): { italic: boolean; bold: boolean } {
354
+ const normalized = sanitizeFontName(fontName ?? '').toLowerCase();
355
+ return {
356
+ italic: normalized.includes('italic'),
357
+ bold: normalized.includes('bold'),
358
+ };
359
+ }
@@ -0,0 +1,67 @@
1
+ const DEFAULT_ICON = 'CheckCircleOutlined';
2
+
3
+ const iconPathMap: Record<string, string[]> = {
4
+ CheckCircleOutlined: [
5
+ 'M699 353h-46.9c-10.2 0-19.9 4.9-25.9 13.3L469 584.3l-71.2-98.8c-6-8.3-15.6-13.3-25.9-13.3H325c-6.5 0-10.3 7.4-6.5 12.7l124.6 172.8a31.8 31.8 0 0051.7 0l210.6-292c3.9-5.3.1-12.7-6.4-12.7z',
6
+ 'M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z',
7
+ ],
8
+ CheckOutlined: [
9
+ 'M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 00-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z',
10
+ ],
11
+ CloseOutlined: [
12
+ 'M799.86 166.31c.02 0 .04.02.08.06l57.69 57.7c.04.03.05.05.06.08a.12.12 0 010 .06c0 .03-.02.05-.06.09L569.93 512l287.7 287.7c.04.04.05.06.06.09a.12.12 0 010 .07c0 .02-.02.04-.06.08l-57.7 57.69c-.03.04-.05.05-.07.06a.12.12 0 01-.07 0c-.03 0-.05-.02-.09-.06L512 569.93l-287.7 287.7c-.04.04-.06.05-.09.06a.12.12 0 01-.07 0c-.02 0-.04-.02-.08-.06l-57.69-57.7c-.04-.03-.05-.05-.06-.07a.12.12 0 010-.07c0-.03.02-.05.06-.09L454.07 512l-287.7-287.7c-.04-.04-.05-.06-.06-.09a.12.12 0 010-.07c0-.02.02-.04.06-.08l57.7-57.69c.03-.04.05-.05.07-.06a.12.12 0 01.07 0c.03 0 .05.02.09.06L512 454.07l287.7-287.7c.04-.04.06-.05.09-.06a.12.12 0 01.07 0z',
13
+ ],
14
+ LeftOutlined: [
15
+ 'M724 218.3V141c0-6.7-7.7-10.4-12.9-6.3L260.3 486.8a31.86 31.86 0 000 50.3l450.8 352.1c5.3 4.1 12.9.4 12.9-6.3v-77.3c0-4.9-2.3-9.6-6.1-12.6l-360-281 360-281.1c3.8-3 6.1-7.7 6.1-12.6z',
16
+ ],
17
+ RightOutlined: [
18
+ 'M765.7 486.8L314.9 134.7A7.97 7.97 0 00302 141v77.3c0 4.9 2.3 9.6 6.1 12.6l360 281.1-360 281.1c-3.9 3-6.1 7.7-6.1 12.6V883c0 6.7 7.7 10.4 12.9 6.3l450.8-352.1a31.96 31.96 0 000-50.4z',
19
+ ],
20
+ UpOutlined: [
21
+ 'M890.5 755.3L537.9 269.2c-12.8-17.6-39-17.6-51.7 0L133.5 755.3A8 8 0 00140 768h75c5.1 0 9.9-2.5 12.9-6.6L512 369.8l284.1 391.6c3 4.1 7.8 6.6 12.9 6.6h75c6.5 0 10.3-7.4 6.5-12.7z',
22
+ ],
23
+ DownOutlined: [
24
+ 'M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z',
25
+ ],
26
+ PlusOutlined: [
27
+ 'M482 152h60q8 0 8 8v704q0 8-8 8h-60q-8 0-8-8V160q0-8 8-8z',
28
+ 'M192 474h672q8 0 8 8v60q0 8-8 8H160q-8 0-8-8v-60q0-8 8-8z',
29
+ ],
30
+ CaretDownOutlined: [
31
+ 'M840.4 300H183.6c-19.7 0-30.7 20.8-18.5 35l328.4 380.8c9.4 10.9 27.5 10.9 37 0L858.9 335c12.2-14.2 1.2-35-18.5-35z',
32
+ ],
33
+ CaretLeftOutlined: [
34
+ 'M689 165.1L308.2 493.5c-10.9 9.4-10.9 27.5 0 37L689 858.9c14.2 12.2 35 1.2 35-18.5V183.6c0-19.7-20.8-30.7-35-18.5z',
35
+ ],
36
+ CaretRightOutlined: [
37
+ 'M715.8 493.5L335 165.1c-14.2-12.2-35-1.2-35 18.5v656.8c0 19.7 20.8 30.7 35 18.5l380.8-328.4c10.9-9.4 10.9-27.6 0-37z',
38
+ ],
39
+ CaretUpOutlined: [
40
+ 'M858.9 689L530.5 308.2c-9.4-10.9-27.5-10.9-37 0L165.1 689c-12.2 14.2-1.2 35 18.5 35h656.8c19.7 0 30.7-20.8 18.5-35z',
41
+ ],
42
+ InfoCircleOutlined: [
43
+ 'M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z',
44
+ 'M464 336a48 48 0 1096 0 48 48 0 10-96 0zm72 112h-48c-4.4 0-8 3.6-8 8v272c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V456c0-4.4-3.6-8-8-8z',
45
+ ],
46
+ QuestionOutlined: [
47
+ 'M764 280.9c-14-30.6-33.9-58.1-59.3-81.6C653.1 151.4 584.6 125 512 125s-141.1 26.4-192.7 74.2c-25.4 23.6-45.3 51-59.3 81.7-14.6 32-22 65.9-22 100.9v27c0 6.2 5 11.2 11.2 11.2h54c6.2 0 11.2-5 11.2-11.2v-27c0-99.5 88.6-180.4 197.6-180.4s197.6 80.9 197.6 180.4c0 40.8-14.5 79.2-42 111.2-27.2 31.7-65.6 54.4-108.1 64-24.3 5.5-46.2 19.2-61.7 38.8a110.85 110.85 0 00-23.9 68.6v31.4c0 6.2 5 11.2 11.2 11.2h54c6.2 0 11.2-5 11.2-11.2v-31.4c0-15.7 10.9-29.5 26-32.9 58.4-13.2 111.4-44.7 149.3-88.7 19.1-22.3 34-47.1 44.3-74 10.7-27.9 16.1-57.2 16.1-87 0-35-7.4-69-22-100.9zM512 787c-30.9 0-56 25.1-56 56s25.1 56 56 56 56-25.1 56-56-25.1-56-56-56z',
48
+ ],
49
+ PauseOutlined: [
50
+ 'M304 176h80v672h-80zm408 0h-64c-4.4 0-8 3.6-8 8v656c0 4.4 3.6 8 8 8h64c4.4 0 8-3.6 8-8V184c0-4.4-3.6-8-8-8z',
51
+ ],
52
+ PlayCircleOutlined: [
53
+ 'M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm0 820c-205.4 0-372-166.6-372-372s166.6-372 372-372 372 166.6 372 372-166.6 372-372 372z',
54
+ 'M719.4 499.1l-296.1-215A15.9 15.9 0 0 0 398 297v430c0 13.1 14.8 20.5 25.3 12.9l296.1-215a15.9 15.9 0 0 0 0-25.8zm-257.6 134V390.9L628.5 512 461.8 633.1z',
55
+ ],
56
+ SoundOutlined: [
57
+ 'M625.9 115c-5.9 0-11.9 1.6-17.4 5.3L254 352H90c-8.8 0-16 7.2-16 16v288c0 8.8 7.2 16 16 16h164l354.5 231.7c5.5 3.6 11.6 5.3 17.4 5.3 16.7 0 32.1-13.3 32.1-32.1V147.1c0-18.8-15.4-32.1-32.1-32.1zM586 803L293.4 611.7l-18-11.7H146V424h129.4l17.9-11.7L586 221v582zm348-327H806c-8.8 0-16 7.2-16 16v40c0 8.8 7.2 16 16 16h128c8.8 0 16-7.2 16-16v-40c0-8.8-7.2-16-16-16zm-41.9 261.8l-110.3-63.7a15.9 15.9 0 0 0-21.7 5.9l-19.9 34.5c-4.4 7.6-1.8 17.4 5.8 21.8L856.3 800a15.9 15.9 0 0 0 21.7-5.9l19.9-34.5c4.4-7.6 1.7-17.4-5.8-21.8zM760 344a15.9 15.9 0 0 0 21.7 5.9L892 286.2c7.6-4.4 10.2-14.2 5.8-21.8L878 230a15.9 15.9 0 0 0-21.7-5.9L746 287.8a15.99 15.99 0 0 0-5.8 21.8L760 344z',
58
+ ],
59
+ MutedOutlined: [
60
+ 'M771.915 115c-5.863 0-11.877 1.644-17.42 5.267L400 351.966H236c-8.837 0-16 7.165-16 16.003V656.03c0 8.838 7.163 16.003 16 16.003h164l354.495 231.7c5.542 3.621 11.558 5.267 17.42 5.267C788.566 909 804 895.749 804 876.94V147.06c0-18.808-15.436-32.06-32.085-32.06M732 220.997v582.005L439.386 611.75l-17.948-11.73H292V423.98h129.438l17.948-11.73z',
61
+ ],
62
+ };
63
+
64
+ export function iconPathsByName(name?: string): string[] | undefined {
65
+ if (!name) return undefined;
66
+ return iconPathMap[name] ?? iconPathMap[DEFAULT_ICON];
67
+ }