@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,638 @@
1
+ import React, {
2
+ useCallback,
3
+ useEffect,
4
+ useMemo,
5
+ useRef,
6
+ useState,
7
+ } from "react";
8
+ import { useKeplerAppStateManager } from "@amazon-devices/react-native-kepler";
9
+ import { View, Text, StyleSheet, Image, useWindowDimensions } from "react-native";
10
+ import {
11
+ logger,
12
+ NamiEventEmitter,
13
+ NamiPaywallAction,
14
+ PAYWALL_ACTION_EVENT,
15
+ } from "@namiml/sdk-core";
16
+ import { applyStyles, parseSizeOrPercent } from "../../utils/styles";
17
+ import { usePaywallContext } from "../../context/PaywallContext";
18
+ import {
19
+ buildSmartTextReplacements,
20
+ interpolateSmartText,
21
+ } from "../../utils/smartText";
22
+ import {
23
+ initializeVideoControls,
24
+ setVideoMuted,
25
+ setVideoPlaying,
26
+ subscribeVideoControls,
27
+ } from "../../utils/videoControls";
28
+
29
+ interface Props {
30
+ component: any;
31
+ scaleFactor: number;
32
+ parentDirection?: string;
33
+ }
34
+
35
+ type W3CMediaModule = {
36
+ VideoPlayer: new () => any;
37
+ KeplerVideoSurfaceView: React.ComponentType<any>;
38
+ KeplerMediaControlHandler: new () => any;
39
+ };
40
+
41
+ function serializePlaybackError(reason: unknown): Record<string, unknown> {
42
+ if (reason instanceof Error) {
43
+ return {
44
+ name: reason.name,
45
+ message: reason.message,
46
+ stack: reason.stack,
47
+ };
48
+ }
49
+
50
+ if (reason && typeof reason === "object") {
51
+ const output: Record<string, unknown> = {};
52
+ for (const [key, value] of Object.entries(reason)) {
53
+ output[key] = value;
54
+ }
55
+ return output;
56
+ }
57
+
58
+ return {
59
+ value: reason == null ? "" : String(reason),
60
+ };
61
+ }
62
+
63
+ export const NamiVideo: React.FC<Props> = ({
64
+ component,
65
+ scaleFactor,
66
+ parentDirection,
67
+ }) => {
68
+ const ctx = usePaywallContext();
69
+ const keplerAppStateManager = useKeplerAppStateManager();
70
+ const { width: deviceWidth, height: deviceHeight } = useWindowDimensions();
71
+
72
+ const [playing, setPlaying] = useState(!!component.autoplayVideo);
73
+ const [muted, setMuted] = useState(!!(component.mute ?? component.muted));
74
+ const [playbackFailed, setPlaybackFailed] = useState(false);
75
+ const [activeUrl, setActiveUrl] = useState("");
76
+ const [metadataReady, setMetadataReady] = useState(false);
77
+ const [surfaceReady, setSurfaceReady] = useState(false);
78
+
79
+ const surfaceHandleRef = useRef<string | null>(null);
80
+ const playerRef = useRef<any>(null);
81
+ const disposedRef = useRef(false);
82
+
83
+ const smartTextSku = component?.smartTextSku ?? component?.sku;
84
+
85
+ const mediaLib = useMemo<W3CMediaModule | null>(() => {
86
+ try {
87
+ return require("@amazon-devices/react-native-w3cmedia");
88
+ } catch {
89
+ return null;
90
+ }
91
+ }, []);
92
+
93
+ const containerStyle = applyStyles(
94
+ component,
95
+ scaleFactor,
96
+ false,
97
+ parentDirection,
98
+ );
99
+
100
+ const width =
101
+ parseSizeOrPercent(component.width ?? component.fixedWidth, scaleFactor) ??
102
+ "100%";
103
+
104
+ const height =
105
+ parseSizeOrPercent(
106
+ component.height ?? component.fixedHeight,
107
+ scaleFactor,
108
+ ) ?? 200;
109
+
110
+ const resolvedWidth =
111
+ typeof width === "string" && width.endsWith("%")
112
+ ? (deviceWidth * Number.parseFloat(width)) / 100
113
+ : width;
114
+
115
+ const resolvedHeight =
116
+ typeof height === "string" && height.endsWith("%")
117
+ ? (deviceHeight * Number.parseFloat(height)) / 100
118
+ : height;
119
+
120
+ const poster = component.fallbackImage ?? "";
121
+ const scalingmode = component.imageCropping === "fit" ? "fit" : "fill";
122
+ const loop = !!(component.loopVideo ?? component.loop);
123
+ const shouldAutoplay =
124
+ component.paused != null ? !component.paused : !!component.autoplayVideo;
125
+
126
+ const resolvedUrl = useMemo(() => {
127
+ const raw =
128
+ component.url ??
129
+ component.videoUrl ??
130
+ (ctx.state as any).appSuppliedVideoUrl ??
131
+ "";
132
+
133
+ const replacements = buildSmartTextReplacements(
134
+ ctx.state,
135
+ ctx.flow,
136
+ smartTextSku,
137
+ );
138
+
139
+ const resolved = interpolateSmartText(raw, replacements);
140
+ return resolved == null ? "" : String(resolved);
141
+ }, [component, ctx.state, ctx.flow, smartTextSku]);
142
+
143
+ useEffect(() => {
144
+ initializeVideoControls({
145
+ playing:
146
+ component.paused != null
147
+ ? !component.paused
148
+ : !!component.autoplayVideo,
149
+ muted: !!(component.mute ?? component.muted),
150
+ });
151
+ }, [
152
+ component.paused,
153
+ component.autoplayVideo,
154
+ component.mute,
155
+ component.muted,
156
+ ]);
157
+
158
+ useEffect(() => {
159
+ return subscribeVideoControls((state) => {
160
+ setPlaying(state.playing);
161
+ setMuted(state.muted);
162
+ });
163
+ }, []);
164
+
165
+ useEffect(() => {
166
+ setPlaybackFailed(false);
167
+ setMetadataReady(false);
168
+ setSurfaceReady(false);
169
+ setActiveUrl(resolvedUrl);
170
+ }, [resolvedUrl, component?.id]);
171
+
172
+ const emitVideoEvent = useCallback(
173
+ (action: NamiPaywallAction) => {
174
+ const player = playerRef.current;
175
+
176
+ NamiEventEmitter.getInstance().emit(PAYWALL_ACTION_EVENT, {
177
+ ...ctx.getPaywallActionEventData(),
178
+ action,
179
+ videoMetaData: {
180
+ id: component?.id,
181
+ name: component?.title,
182
+ url: activeUrl,
183
+ loopVideo: loop,
184
+ muteByDefault: !!(component.mute ?? component.muted),
185
+ autoplayVideo: shouldAutoplay,
186
+ contentDuration:
187
+ typeof player?.duration === "number" ? player.duration : undefined,
188
+ contentTimecode:
189
+ typeof player?.currentTime === "number"
190
+ ? player.currentTime
191
+ : undefined,
192
+ },
193
+ });
194
+ },
195
+ [
196
+ ctx,
197
+ component?.id,
198
+ component?.title,
199
+ component?.mute,
200
+ component?.muted,
201
+ activeUrl,
202
+ loop,
203
+ shouldAutoplay,
204
+ ],
205
+ );
206
+
207
+ const handlePlaybackFailure = useCallback(
208
+ (reason?: unknown) => {
209
+ logger.warn(
210
+ "[NamiVideo][vega] Video playback failed; falling back to poster image.",
211
+ {
212
+ id: component?.id,
213
+ title: component?.title,
214
+ url: activeUrl,
215
+ error: serializePlaybackError(reason),
216
+ metadataReady,
217
+ surfaceReady,
218
+ playing,
219
+ muted,
220
+ },
221
+ );
222
+
223
+ setPlaybackFailed(true);
224
+ setMetadataReady(false);
225
+ setVideoPlaying(false);
226
+
227
+ emitVideoEvent(NamiPaywallAction.VIDEO_PAUSED);
228
+ },
229
+ [
230
+ component?.id,
231
+ component?.title,
232
+ activeUrl,
233
+ shouldAutoplay,
234
+ emitVideoEvent,
235
+ ],
236
+ );
237
+
238
+ const attachSurfaceAndMaybePlay = useCallback(async () => {
239
+ const player = playerRef.current;
240
+ if (!player || !surfaceHandleRef.current) return;
241
+
242
+ try {
243
+ player.setSurfaceHandle?.(surfaceHandleRef.current);
244
+
245
+ if (metadataReady && shouldAutoplay && playing && !playbackFailed) {
246
+ await player.play?.();
247
+ }
248
+ } catch (error) {
249
+ handlePlaybackFailure(error);
250
+ }
251
+ }, [
252
+ handlePlaybackFailure,
253
+ metadataReady,
254
+ playbackFailed,
255
+ playing,
256
+ shouldAutoplay,
257
+ ]);
258
+
259
+ useEffect(() => {
260
+ if (!mediaLib || !activeUrl) return;
261
+
262
+ const { VideoPlayer, KeplerMediaControlHandler } = mediaLib;
263
+ disposedRef.current = false;
264
+
265
+ let localPlayer: any = null;
266
+ const listeners: Array<[string, (event?: any) => void]> = [];
267
+
268
+ const addListener = (type: string, handler: (event?: any) => void) => {
269
+ localPlayer?.addEventListener?.(type, handler);
270
+ listeners.push([type, handler]);
271
+ };
272
+
273
+ const cleanup = async () => {
274
+ try {
275
+ localPlayer?.pause?.();
276
+ } catch {
277
+ // no-op
278
+ }
279
+
280
+ for (const [type, handler] of listeners) {
281
+ try {
282
+ localPlayer?.removeEventListener?.(type, handler);
283
+ } catch {
284
+ // no-op
285
+ }
286
+ }
287
+
288
+ try {
289
+ if (surfaceHandleRef.current) {
290
+ localPlayer?.clearSurfaceHandle?.(surfaceHandleRef.current);
291
+ }
292
+ } catch {
293
+ // no-op
294
+ }
295
+
296
+ try {
297
+ if (typeof localPlayer?.deinitializeSync === "function") {
298
+ localPlayer.deinitializeSync(1500);
299
+ } else {
300
+ await localPlayer?.deinitialize?.();
301
+ }
302
+ } catch {
303
+ // no-op
304
+ }
305
+
306
+ surfaceHandleRef.current = null;
307
+
308
+ if (playerRef.current === localPlayer) {
309
+ playerRef.current = null;
310
+ }
311
+ };
312
+
313
+ const initializePlayer = async () => {
314
+ try {
315
+ localPlayer = new VideoPlayer();
316
+ playerRef.current = localPlayer;
317
+
318
+ try {
319
+ const componentInstance =
320
+ keplerAppStateManager?.getComponentInstance?.();
321
+ if (componentInstance) {
322
+ await localPlayer.setMediaControlFocus?.(
323
+ componentInstance,
324
+ new KeplerMediaControlHandler(),
325
+ );
326
+ }
327
+ } catch {
328
+ // no-op
329
+ }
330
+
331
+ await localPlayer.initialize?.();
332
+
333
+ if (disposedRef.current) {
334
+ await cleanup();
335
+ return;
336
+ }
337
+
338
+ addListener("loadedmetadata", async () => {
339
+ setMetadataReady(true);
340
+ await attachSurfaceAndMaybePlay();
341
+ });
342
+
343
+ addListener("waiting", () => {
344
+ logger.debug?.("[NamiVideo][vega] VideoPlayer waiting for data.", {
345
+ id: component?.id,
346
+ title: component?.title,
347
+ url: activeUrl,
348
+ });
349
+ });
350
+
351
+ addListener("playing", () => {
352
+ setPlaybackFailed(false);
353
+ setVideoPlaying(true);
354
+
355
+ const currentTime = Number(localPlayer?.currentTime ?? 0);
356
+ emitVideoEvent(
357
+ currentTime > 0.05
358
+ ? NamiPaywallAction.VIDEO_RESUMED
359
+ : NamiPaywallAction.VIDEO_STARTED,
360
+ );
361
+ });
362
+
363
+ addListener("pause", () => {
364
+ setVideoPlaying(false);
365
+ if (!playbackFailed) {
366
+ emitVideoEvent(NamiPaywallAction.VIDEO_PAUSED);
367
+ }
368
+ });
369
+
370
+ addListener("ended", () => {
371
+ setVideoPlaying(false);
372
+ emitVideoEvent(NamiPaywallAction.VIDEO_ENDED);
373
+ });
374
+
375
+ addListener("volumechange", () => {
376
+ const nextMuted = !!localPlayer?.muted;
377
+ setVideoMuted(nextMuted);
378
+ emitVideoEvent(
379
+ nextMuted
380
+ ? NamiPaywallAction.VIDEO_MUTED
381
+ : NamiPaywallAction.VIDEO_UNMUTED,
382
+ );
383
+ });
384
+
385
+ addListener("error", (event?: any) => {
386
+ const mediaError = localPlayer?.error;
387
+ const targetError = event?.target?.mediaControlStateUtil?.mError;
388
+ logger.warn("[NamiVideo][vega] VideoPlayer emitted error.", {
389
+ id: component?.id,
390
+ title: component?.title,
391
+ url: activeUrl,
392
+ eventType: event?.type,
393
+ mediaErrorCode:
394
+ typeof mediaError?.code === "number"
395
+ ? mediaError.code
396
+ : undefined,
397
+ mediaErrorMessage:
398
+ typeof mediaError?.message === "string"
399
+ ? mediaError.message
400
+ : undefined,
401
+ readyState:
402
+ typeof localPlayer?.readyState === "number"
403
+ ? localPlayer.readyState
404
+ : undefined,
405
+ networkState:
406
+ typeof localPlayer?.networkState === "number"
407
+ ? localPlayer.networkState
408
+ : undefined,
409
+ currentSrc:
410
+ typeof localPlayer?.currentSrc === "string"
411
+ ? localPlayer.currentSrc
412
+ : undefined,
413
+ targetErrorCode:
414
+ typeof targetError?.code_ === "number"
415
+ ? targetError.code_
416
+ : undefined,
417
+ targetErrorMessage:
418
+ typeof targetError?.message_ === "string"
419
+ ? targetError.message_
420
+ : undefined,
421
+ });
422
+ handlePlaybackFailure(targetError ?? mediaError ?? event);
423
+ });
424
+
425
+ try {
426
+ localPlayer.autoplay = false;
427
+ } catch {
428
+ // no-op
429
+ }
430
+
431
+ try {
432
+ localPlayer.muted = muted;
433
+ } catch {
434
+ // no-op
435
+ }
436
+
437
+ try {
438
+ localPlayer.loop = loop;
439
+ } catch {
440
+ // no-op
441
+ }
442
+
443
+ try {
444
+ if ("defaultSeekIntervalInSec" in localPlayer) {
445
+ localPlayer.defaultSeekIntervalInSec = 10;
446
+ }
447
+ } catch {
448
+ // no-op
449
+ }
450
+
451
+ localPlayer.src = activeUrl;
452
+ try {
453
+ localPlayer.pause?.();
454
+ } catch {
455
+ // no-op
456
+ }
457
+ try {
458
+ localPlayer.load?.();
459
+ } catch {
460
+ // no-op
461
+ }
462
+ } catch (error) {
463
+ handlePlaybackFailure(error);
464
+ }
465
+ };
466
+
467
+ initializePlayer();
468
+
469
+ return () => {
470
+ disposedRef.current = true;
471
+ setMetadataReady(false);
472
+ setSurfaceReady(false);
473
+ cleanup();
474
+ };
475
+ }, [
476
+ mediaLib,
477
+ activeUrl,
478
+ keplerAppStateManager,
479
+ emitVideoEvent,
480
+ handlePlaybackFailure,
481
+ ]);
482
+
483
+ useEffect(() => {
484
+ const player = playerRef.current;
485
+ if (!player || playbackFailed || !surfaceHandleRef.current) return;
486
+
487
+ try {
488
+ player.muted = muted;
489
+ } catch {
490
+ // no-op
491
+ }
492
+
493
+ try {
494
+ player.loop = loop;
495
+ } catch {
496
+ // no-op
497
+ }
498
+
499
+ const syncPlayback = async () => {
500
+ try {
501
+ if (playing && metadataReady) {
502
+ await player.play?.();
503
+ } else {
504
+ await player.pause?.();
505
+ }
506
+ } catch (error) {
507
+ handlePlaybackFailure(error);
508
+ }
509
+ };
510
+
511
+ syncPlayback();
512
+ }, [
513
+ muted,
514
+ loop,
515
+ playing,
516
+ metadataReady,
517
+ playbackFailed,
518
+ handlePlaybackFailure,
519
+ ]);
520
+
521
+ const handleSurfaceViewCreated = useCallback(
522
+ async (surfaceHandle: string) => {
523
+ surfaceHandleRef.current = surfaceHandle;
524
+ setSurfaceReady(true);
525
+ await attachSurfaceAndMaybePlay();
526
+ },
527
+ [attachSurfaceAndMaybePlay],
528
+ );
529
+
530
+ const handleSurfaceViewDestroyed = useCallback((surfaceHandle: string) => {
531
+ try {
532
+ playerRef.current?.clearSurfaceHandle?.(surfaceHandle);
533
+ } catch {
534
+ // no-op
535
+ }
536
+
537
+ if (surfaceHandleRef.current === surfaceHandle) {
538
+ surfaceHandleRef.current = null;
539
+ }
540
+ setSurfaceReady(false);
541
+ }, []);
542
+
543
+ if (!mediaLib) {
544
+ return (
545
+ <View
546
+ style={[
547
+ containerStyle,
548
+ styles.placeholder,
549
+ { width: resolvedWidth as any, height: resolvedHeight as any },
550
+ ]}
551
+ >
552
+ <Text style={styles.placeholderText} allowFontScaling={false}>
553
+ Video
554
+ </Text>
555
+ </View>
556
+ );
557
+ }
558
+
559
+ if (!activeUrl) {
560
+ return (
561
+ <View
562
+ style={[
563
+ containerStyle,
564
+ styles.placeholder,
565
+ { width: resolvedWidth as any, height: resolvedHeight as any },
566
+ ]}
567
+ >
568
+ <Text style={styles.placeholderText} allowFontScaling={false}>
569
+ Missing video URL
570
+ </Text>
571
+ </View>
572
+ );
573
+ }
574
+
575
+ if (playbackFailed && poster) {
576
+ return (
577
+ <View style={containerStyle}>
578
+ <Image
579
+ source={{ uri: poster }}
580
+ style={{ width: resolvedWidth as any, height: resolvedHeight as any }}
581
+ resizeMode={scalingmode === "fit" ? "contain" : "cover"}
582
+ />
583
+ </View>
584
+ );
585
+ }
586
+
587
+ const { KeplerVideoSurfaceView } = mediaLib;
588
+
589
+ return (
590
+ <View style={containerStyle}>
591
+ <View
592
+ style={[
593
+ styles.surfaceContainer,
594
+ { width: resolvedWidth as any, height: resolvedHeight as any },
595
+ ]}
596
+ >
597
+ {!surfaceReady && poster ? (
598
+ <Image
599
+ source={{ uri: poster }}
600
+ style={styles.poster}
601
+ resizeMode={scalingmode === "fit" ? "contain" : "cover"}
602
+ />
603
+ ) : null}
604
+
605
+ {metadataReady && !playbackFailed ? (
606
+ <KeplerVideoSurfaceView
607
+ style={styles.videoSurface}
608
+ onSurfaceViewCreated={handleSurfaceViewCreated}
609
+ onSurfaceViewDestroyed={handleSurfaceViewDestroyed}
610
+ />
611
+ ) : null}
612
+ </View>
613
+ </View>
614
+ );
615
+ };
616
+
617
+ const styles = StyleSheet.create({
618
+ poster: {
619
+ ...StyleSheet.absoluteFillObject,
620
+ },
621
+ surfaceContainer: {
622
+ backgroundColor: "#000",
623
+ overflow: "hidden",
624
+ alignItems: "stretch",
625
+ },
626
+ videoSurface: {
627
+ zIndex: 0,
628
+ },
629
+ placeholder: {
630
+ backgroundColor: "#000",
631
+ justifyContent: "center",
632
+ alignItems: "center",
633
+ },
634
+ placeholderText: {
635
+ color: "#fff",
636
+ fontSize: 14,
637
+ },
638
+ });