@namiml/expo-sdk 3.4.7-dev.202607230540 → 3.4.7-dev.202607231604

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 CHANGED
@@ -3081,14 +3081,14 @@ const NamiContentContainer = ({ component, scaleFactor, onClose, }) => {
3081
3081
  }, [component.verticalAlignment, component.horizontalAlignment, flexDir]);
3082
3082
  const renderedChildren = require$$0.useMemo(() => {
3083
3083
  return component.components?.map((comp, i) => (jsxRuntimeExports.jsx(reactNative.View, { style: [
3084
- styles$5.fullWidth,
3084
+ styles$6.fullWidth,
3085
3085
  i === 0
3086
3086
  ? null
3087
3087
  : childSpacingStyle(i, { spacing: component.spacing, direction }, scaleFactor),
3088
3088
  ], children: jsxRuntimeExports.jsx(TemplateRenderer, { component: comp, scaleFactor: scaleFactor, onClose: onClose, parentDirection: direction }) }, comp.id ?? i)));
3089
3089
  }, [component.components, component.spacing, direction, scaleFactor, onClose]);
3090
3090
  const innerContentStyle = require$$0.useMemo(() => [
3091
- styles$5.inner,
3091
+ styles$6.inner,
3092
3092
  {
3093
3093
  flexDirection: flexDir,
3094
3094
  },
@@ -3096,11 +3096,11 @@ const NamiContentContainer = ({ component, scaleFactor, onClose, }) => {
3096
3096
  contentAlignment,
3097
3097
  ], [flexDir, inner, contentAlignment]);
3098
3098
  if (!scrollEnabled) {
3099
- return (jsxRuntimeExports.jsx(reactNative.View, { style: [styles$5.outer, outer], children: jsxRuntimeExports.jsx(reactNative.View, { style: [styles$5.scroll, innerContentStyle], children: renderedChildren }) }));
3099
+ return (jsxRuntimeExports.jsx(reactNative.View, { style: [styles$6.outer, outer], children: jsxRuntimeExports.jsx(reactNative.View, { style: [styles$6.scroll, innerContentStyle], children: renderedChildren }) }));
3100
3100
  }
3101
- return (jsxRuntimeExports.jsx(reactNative.View, { style: [styles$5.outer, outer], children: jsxRuntimeExports.jsx(reactNative.ScrollView, { style: styles$5.scroll, contentContainerStyle: innerContentStyle, showsVerticalScrollIndicator: false, scrollEnabled: true, children: renderedChildren }) }));
3101
+ return (jsxRuntimeExports.jsx(reactNative.View, { style: [styles$6.outer, outer], children: jsxRuntimeExports.jsx(reactNative.ScrollView, { style: styles$6.scroll, contentContainerStyle: innerContentStyle, showsVerticalScrollIndicator: false, scrollEnabled: true, children: renderedChildren }) }));
3102
3102
  };
3103
- const styles$5 = reactNative.StyleSheet.create({
3103
+ const styles$6 = reactNative.StyleSheet.create({
3104
3104
  outer: {
3105
3105
  flex: 1,
3106
3106
  minHeight: 0,
@@ -3142,9 +3142,9 @@ const NamiBackgroundContainer = ({ component, scaleFactor, onClose }) => {
3142
3142
  return (jsxRuntimeExports.jsx(TemplateRenderer, { component: spacedComponent, scaleFactor: scaleFactor, onClose: onClose, parentDirection: direction }, comp.id ?? i));
3143
3143
  }) }));
3144
3144
  if (fillImageUrl) {
3145
- return (jsxRuntimeExports.jsx(reactNative.ImageBackground, { source: { uri: fillImageUrl }, resizeMode: 'cover', style: [containerStyle, styles$4.background, clipStyle], children: content }));
3145
+ return (jsxRuntimeExports.jsx(reactNative.ImageBackground, { source: { uri: fillImageUrl }, resizeMode: 'cover', style: [containerStyle, styles$5.background, clipStyle], children: content }));
3146
3146
  }
3147
- return (jsxRuntimeExports.jsx(reactNative.View, { style: [containerStyle, bgColor ? { backgroundColor: bgColor } : null, styles$4.background, clipStyle], children: content }));
3147
+ return (jsxRuntimeExports.jsx(reactNative.View, { style: [containerStyle, bgColor ? { backgroundColor: bgColor } : null, styles$5.background, clipStyle], children: content }));
3148
3148
  };
3149
3149
  function mergeRawMarginValue(existing, spacing) {
3150
3150
  if (existing == null)
@@ -3170,7 +3170,7 @@ function toNumericMargin(value) {
3170
3170
  const parsed = Number(trimmed);
3171
3171
  return Number.isFinite(parsed) ? parsed : null;
3172
3172
  }
3173
- const styles$4 = reactNative.StyleSheet.create({
3173
+ const styles$5 = reactNative.StyleSheet.create({
3174
3174
  background: {
3175
3175
  ...reactNative.StyleSheet.absoluteFillObject,
3176
3176
  },
@@ -3185,9 +3185,9 @@ const NamiHeader = ({ components, scaleFactor, onClose }) => {
3185
3185
  ? components
3186
3186
  : components?.components ?? [components];
3187
3187
  const renderedList = require$$0.useMemo(() => list.map((comp, i) => (jsxRuntimeExports.jsx(TemplateRenderer, { component: comp, scaleFactor: scaleFactor, onClose: onClose }, comp.id ?? i))), [list, scaleFactor, onClose]);
3188
- return (jsxRuntimeExports.jsx(reactNative.View, { style: [styles$3.header, { marginTop }], children: renderedList }));
3188
+ return (jsxRuntimeExports.jsx(reactNative.View, { style: [styles$4.header, { marginTop }], children: renderedList }));
3189
3189
  };
3190
- const styles$3 = reactNative.StyleSheet.create({
3190
+ const styles$4 = reactNative.StyleSheet.create({
3191
3191
  header: {
3192
3192
  display: 'flex',
3193
3193
  position: 'relative',
@@ -3202,9 +3202,9 @@ const NamiFooter = ({ components, scaleFactor, onClose }) => {
3202
3202
  const list = Array.isArray(components)
3203
3203
  ? components
3204
3204
  : components?.components ?? [components];
3205
- return (jsxRuntimeExports.jsx(reactNative.View, { style: styles$2.footer, children: list.map((comp, i) => (jsxRuntimeExports.jsx(TemplateRenderer, { component: comp, scaleFactor: scaleFactor, onClose: onClose }, comp.id ?? i))) }));
3205
+ return (jsxRuntimeExports.jsx(reactNative.View, { style: styles$3.footer, children: list.map((comp, i) => (jsxRuntimeExports.jsx(TemplateRenderer, { component: comp, scaleFactor: scaleFactor, onClose: onClose }, comp.id ?? i))) }));
3206
3206
  };
3207
- const styles$2 = reactNative.StyleSheet.create({
3207
+ const styles$3 = reactNative.StyleSheet.create({
3208
3208
  footer: {
3209
3209
  position: 'absolute',
3210
3210
  bottom: 0,
@@ -3215,6 +3215,196 @@ const styles$2 = reactNative.StyleSheet.create({
3215
3215
  },
3216
3216
  });
3217
3217
 
3218
+ /**
3219
+ * NAM-403: full-page screenreader announcement helpers for the Vega/Kepler
3220
+ * (and shared Expo) renderer. RN-free so the jest node environment can unit-test
3221
+ * it (mirrors imageAccessibility.ts). See the TV Full-Page Announcement contract.
3222
+ */
3223
+ // Reserved action functions whose buttons never trigger a full-page announcement
3224
+ // and always speak only their own label. Source of truth: utils/actionHandler.ts
3225
+ // (duplicated here to keep this module RN-free and node-testable).
3226
+ const TEXT_TYPES = new Set(['text', 'text-list']);
3227
+ // Subtrees never descended into during the page-text walk. Button labels are
3228
+ // announced on their own focus; media/decoration carry no announce-worthy text.
3229
+ const SKIP_SUBTREE_TYPES = new Set(['button', 'image', 'videoUrl', 'spacer', 'symbol']);
3230
+ function childrenOf(node) {
3231
+ if (Array.isArray(node))
3232
+ return node;
3233
+ if (node && Array.isArray(node.components))
3234
+ return node.components;
3235
+ return [];
3236
+ }
3237
+ function collectNode(node, resolve, out) {
3238
+ if (!node || typeof node !== 'object')
3239
+ return;
3240
+ if (Array.isArray(node)) {
3241
+ for (const child of node)
3242
+ collectNode(child, resolve, out);
3243
+ return;
3244
+ }
3245
+ if (node.hidden === true)
3246
+ return;
3247
+ const type = node.component;
3248
+ if (type && TEXT_TYPES.has(type)) {
3249
+ if (type === 'text-list' && Array.isArray(node.texts)) {
3250
+ for (const item of node.texts) {
3251
+ const resolved = resolve(typeof item === 'string' ? item : String(item ?? '')).trim();
3252
+ if (resolved)
3253
+ out.push(resolved);
3254
+ }
3255
+ }
3256
+ else {
3257
+ const raw = node.text ?? node.title;
3258
+ const resolved = resolve(typeof raw === 'string' ? raw : raw == null ? '' : String(raw)).trim();
3259
+ if (resolved)
3260
+ out.push(resolved);
3261
+ }
3262
+ return; // text nodes have no announce-worthy descendants
3263
+ }
3264
+ if (type && SKIP_SUBTREE_TYPES.has(type))
3265
+ return;
3266
+ for (const child of childrenOf(node))
3267
+ collectNode(child, resolve, out);
3268
+ }
3269
+ /**
3270
+ * Walk a page (header -> contentContainer -> footer) in source order and join the
3271
+ * resolved text of every `text` / `text-list` node. Skips images, media, spacers,
3272
+ * symbols, button subtrees, and any node with `hidden: true` (and its children).
3273
+ */
3274
+ function collectPageAnnouncementText(page, resolve) {
3275
+ const out = [];
3276
+ collectNode(page?.header, resolve, out);
3277
+ collectNode(page?.contentContainer, resolve, out);
3278
+ collectNode(page?.footer, resolve, out);
3279
+ return out.join('. ');
3280
+ }
3281
+
3282
+ function createAnnouncementMachine() {
3283
+ let pageAnnounced = false;
3284
+ let deferred = null;
3285
+ return {
3286
+ onFocus({ kind, ownLabel, videoPlaying, buildComposite }) {
3287
+ if (videoPlaying) {
3288
+ // Defer: the button is hidden from a11y during playback, so nothing is
3289
+ // spoken now; the flush on video end says the composite (first primary
3290
+ // focus) or the focused button's own label.
3291
+ deferred = kind === 'primary' && !pageAnnounced ? buildComposite() : ownLabel;
3292
+ if (kind === 'primary')
3293
+ pageAnnounced = true;
3294
+ return null;
3295
+ }
3296
+ if (kind === 'auxiliary')
3297
+ return null; // natural focus read handles it
3298
+ if (pageAnnounced)
3299
+ return null; // subsequent focus -> natural read
3300
+ pageAnnounced = true;
3301
+ return buildComposite();
3302
+ },
3303
+ onVideoStopped() {
3304
+ if (deferred == null)
3305
+ return null;
3306
+ const text = deferred;
3307
+ deferred = null;
3308
+ return text;
3309
+ },
3310
+ reset() {
3311
+ pageAnnounced = false;
3312
+ deferred = null;
3313
+ },
3314
+ };
3315
+ }
3316
+
3317
+ let state = {
3318
+ playing: false,
3319
+ muted: false,
3320
+ };
3321
+ const listeners = new Set();
3322
+ function getVideoControlState() {
3323
+ return state;
3324
+ }
3325
+ function subscribeVideoControls(listener) {
3326
+ listeners.add(listener);
3327
+ listener(state);
3328
+ return () => listeners.delete(listener);
3329
+ }
3330
+
3331
+ const AnnouncementCtx = require$$0.createContext(null);
3332
+ // Re-announcing identical text will not fire the live region again; append an
3333
+ // alternating zero-width space (U+200B) so each announce is a distinct text value.
3334
+ const ZERO_WIDTH_SPACE = "\u200B";
3335
+ let toggle = false;
3336
+ function distinct(text) {
3337
+ toggle = !toggle;
3338
+ return toggle ? text : `${text}${ZERO_WIDTH_SPACE}`;
3339
+ }
3340
+ /**
3341
+ * NAM-403: holds per-page announcement state and drives the assertive live region.
3342
+ * Pure decision logic lives in announcementMachine; this provider wires it to
3343
+ * React state, the video-controls subscription, and smart-text resolution.
3344
+ * Reset per page via the `page` dependency.
3345
+ */
3346
+ const AnnouncementProvider = ({ page, children, }) => {
3347
+ const ctx = usePaywallContext();
3348
+ const [liveRegionText, setLiveRegionText] = require$$0.useState('');
3349
+ const [isVideoPlaying, setIsVideoPlaying] = require$$0.useState(() => getVideoControlState().playing);
3350
+ const machineRef = require$$0.useRef(createAnnouncementMachine());
3351
+ const prevPlayingRef = require$$0.useRef(getVideoControlState().playing);
3352
+ const buildComposite = require$$0.useCallback((ownLabel) => {
3353
+ const replacements = buildSmartTextReplacements(ctx.state, ctx.flow, undefined);
3354
+ const resolve = (v) => {
3355
+ const out = interpolateSmartText(v ?? '', replacements);
3356
+ return out == null ? '' : String(out);
3357
+ };
3358
+ const pageText = collectPageAnnouncementText(page, resolve);
3359
+ return pageText ? `${pageText}. ${ownLabel}` : ownLabel;
3360
+ }, [ctx.state, ctx.flow, page]);
3361
+ const announceOnFocus = require$$0.useCallback((kind, ownLabel) => {
3362
+ const text = machineRef.current.onFocus({
3363
+ kind,
3364
+ ownLabel,
3365
+ videoPlaying: getVideoControlState().playing,
3366
+ buildComposite: () => buildComposite(ownLabel),
3367
+ });
3368
+ if (text != null)
3369
+ setLiveRegionText(distinct(text));
3370
+ }, [buildComposite]);
3371
+ // Track video playback; flush any deferred announcement when it ends.
3372
+ require$$0.useEffect(() => subscribeVideoControls((state) => {
3373
+ setIsVideoPlaying(state.playing);
3374
+ if (prevPlayingRef.current && !state.playing) {
3375
+ const text = machineRef.current.onVideoStopped();
3376
+ if (text != null)
3377
+ setLiveRegionText(distinct(text));
3378
+ }
3379
+ prevPlayingRef.current = state.playing;
3380
+ }), []);
3381
+ // Reset per page.
3382
+ require$$0.useEffect(() => {
3383
+ machineRef.current.reset();
3384
+ setLiveRegionText('');
3385
+ }, [page]);
3386
+ const value = require$$0.useMemo(() => ({ isVideoPlaying, liveRegionText, announceOnFocus }), [isVideoPlaying, liveRegionText, announceOnFocus]);
3387
+ return jsxRuntimeExports.jsx(AnnouncementCtx.Provider, { value: value, children: children });
3388
+ };
3389
+ function useAnnouncer() {
3390
+ const ctx = require$$0.useContext(AnnouncementCtx);
3391
+ if (!ctx)
3392
+ throw new Error('useAnnouncer must be used within an AnnouncementProvider');
3393
+ return ctx;
3394
+ }
3395
+
3396
+ /**
3397
+ * NAM-403: visually-hidden assertive live region used to speak the full-page
3398
+ * composite announcement. Default (RN) path uses accessibilityLiveRegion, which
3399
+ * TalkBack/VoiceView honor on focus-independent text changes. The Kepler override
3400
+ * (A11yLiveRegion.kepler.tsx) uses Amazon's preferred aria-live prop. Empty text
3401
+ * renders nothing to speak.
3402
+ */
3403
+ const A11yLiveRegion = ({ text }) => (jsxRuntimeExports.jsx(reactNative.Text, { accessible: !!text, accessibilityLiveRegion: "assertive", style: styles$2.hidden, allowFontScaling: false, children: text }));
3404
+ const styles$2 = reactNative.StyleSheet.create({
3405
+ hidden: { position: 'absolute', width: 1, height: 1, overflow: 'hidden', left: -9999, opacity: 0 },
3406
+ });
3407
+
3218
3408
  function buildImpressionPayload(impression) {
3219
3409
  const payload = { ...impression };
3220
3410
  const sessionId = sdkCore.storageService.getSessionId();
@@ -3387,8 +3577,16 @@ const PaywallScreen = ({ paywall, onClose, onCommitted, holdInteractionUntilFocu
3387
3577
  // its onPress fires only for taps on non-interactive/empty areas. TV has no
3388
3578
  // keyboard, so it renders the content without the Pressable.
3389
3579
  const isTelevision = ctx.state.formFactor === 'television';
3390
- const pageContent = (jsxRuntimeExports.jsxs(PageFocusWrapper, { ...pageFocusWrapperProps, children: [backgroundContainer && (jsxRuntimeExports.jsx(NamiBackgroundContainer, { component: backgroundContainer, scaleFactor: scaleFactor, onClose: onClose })), hasHeader && (jsxRuntimeExports.jsx(NamiHeader, { components: header, scaleFactor: scaleFactor, onClose: onClose })), contentContainer && (jsxRuntimeExports.jsx(NamiContentContainer, { component: contentContainer, scaleFactor: scaleFactor, onClose: onClose })), hasFooter && (jsxRuntimeExports.jsx(NamiFooter, { components: footer, scaleFactor: scaleFactor, onClose: onClose })), shouldHoldForFocus && !focusReady && (jsxRuntimeExports.jsx(reactNative.View, { pointerEvents: "none", style: [styles$1.waitingOverlay, { backgroundColor: waitingOverlayColor }] }))] }, `${paywall.id}:${page.name ?? currentPageName}`));
3391
- return (jsxRuntimeExports.jsx(reactNative.View, { style: [styles$1.screen, { backgroundColor: bgColor }], pointerEvents: interactionEnabled ? 'auto' : 'none', children: isTelevision ? (pageContent) : (jsxRuntimeExports.jsx(reactNative.Pressable, { style: styles$1.screen, onPress: () => reactNative.Keyboard.dismiss(), accessible: false, children: pageContent })) }));
3580
+ const pageContent = (jsxRuntimeExports.jsxs(PageFocusWrapper, { ...pageFocusWrapperProps, children: [jsxRuntimeExports.jsx(A11yLiveRegionMount, {}), backgroundContainer && (jsxRuntimeExports.jsx(NamiBackgroundContainer, { component: backgroundContainer, scaleFactor: scaleFactor, onClose: onClose })), hasHeader && (jsxRuntimeExports.jsx(NamiHeader, { components: header, scaleFactor: scaleFactor, onClose: onClose })), contentContainer && (jsxRuntimeExports.jsx(NamiContentContainer, { component: contentContainer, scaleFactor: scaleFactor, onClose: onClose })), hasFooter && (jsxRuntimeExports.jsx(NamiFooter, { components: footer, scaleFactor: scaleFactor, onClose: onClose })), shouldHoldForFocus && !focusReady && (jsxRuntimeExports.jsx(reactNative.View, { pointerEvents: "none", style: [styles$1.waitingOverlay, { backgroundColor: waitingOverlayColor }] }))] }, `${paywall.id}:${page.name ?? currentPageName}`));
3581
+ return (jsxRuntimeExports.jsx(AnnouncementProvider, { page: page, children: jsxRuntimeExports.jsx(reactNative.View, { style: [styles$1.screen, { backgroundColor: bgColor }], pointerEvents: interactionEnabled ? 'auto' : 'none', children: isTelevision ? (pageContent) : (jsxRuntimeExports.jsx(reactNative.Pressable, { style: styles$1.screen, onPress: () => reactNative.Keyboard.dismiss(), accessible: false, children: pageContent })) }) }, `ann:${paywall.id}:${page.name ?? currentPageName}`));
3582
+ };
3583
+ /**
3584
+ * NAM-403: renders the assertive live region using text from the announcer.
3585
+ * Must live under AnnouncementProvider so it can read useAnnouncer().
3586
+ */
3587
+ const A11yLiveRegionMount = () => {
3588
+ const { liveRegionText } = useAnnouncer();
3589
+ return jsxRuntimeExports.jsx(A11yLiveRegion, { text: liveRegionText });
3392
3590
  };
3393
3591
  const styles$1 = reactNative.StyleSheet.create({
3394
3592
  screen: { flex: 1 },