@namiml/expo-sdk 3.4.12-dev.202610051759 → 3.4.12-dev.202610060208

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
@@ -1939,6 +1939,12 @@ const PaywallProvider = ({ paywall, context, campaign, flow, onFirstFocusReady,
1939
1939
  setSafeAreaBottom: (bottom) => {
1940
1940
  providerRef.current?.setSafeAreaBottom(bottom);
1941
1941
  },
1942
+ setSafeAreaLeft: (left) => {
1943
+ providerRef.current?.setSafeAreaLeft(left);
1944
+ },
1945
+ setSafeAreaRight: (right) => {
1946
+ providerRef.current?.setSafeAreaRight(right);
1947
+ },
1942
1948
  setFullScreenPresentation: (full) => {
1943
1949
  providerRef.current?.setFullScreenPresentation(full);
1944
1950
  },
@@ -2329,6 +2335,117 @@ function isLinearGradient(value) {
2329
2335
  return false;
2330
2336
  return value.trim().toLowerCase().startsWith('linear-gradient(');
2331
2337
  }
2338
+ function splitGradientArgs(input) {
2339
+ const out = [];
2340
+ let current = '';
2341
+ let depth = 0;
2342
+ for (let i = 0; i < input.length; i++) {
2343
+ const ch = input[i];
2344
+ if (ch === '(')
2345
+ depth += 1;
2346
+ if (ch === ')')
2347
+ depth = Math.max(0, depth - 1);
2348
+ if (ch === ',' && depth === 0) {
2349
+ out.push(current.trim());
2350
+ current = '';
2351
+ continue;
2352
+ }
2353
+ current += ch;
2354
+ }
2355
+ if (current.trim())
2356
+ out.push(current.trim());
2357
+ return out;
2358
+ }
2359
+ function parseAngleToken(token) {
2360
+ const t = token.trim().toLowerCase();
2361
+ if (t.endsWith('deg')) {
2362
+ const num = parseFloat(t.replace('deg', '').trim());
2363
+ return Number.isFinite(num) ? num : undefined;
2364
+ }
2365
+ if (t.startsWith('to ')) {
2366
+ // CSS gradient directions
2367
+ if (t.includes('right') && t.includes('top'))
2368
+ return 45;
2369
+ if (t.includes('right') && t.includes('bottom'))
2370
+ return 135;
2371
+ if (t.includes('left') && t.includes('bottom'))
2372
+ return 225;
2373
+ if (t.includes('left') && t.includes('top'))
2374
+ return 315;
2375
+ if (t.includes('right'))
2376
+ return 90;
2377
+ if (t.includes('left'))
2378
+ return 270;
2379
+ if (t.includes('bottom'))
2380
+ return 180;
2381
+ if (t.includes('top'))
2382
+ return 0;
2383
+ }
2384
+ return undefined;
2385
+ }
2386
+ // NAM-3538: parse `linear-gradient(<angle|to side>?, <color> [pct%], ...)` for the
2387
+ // backgroundContainer overlay. Offsets are 0..1; stops without a % are spaced evenly.
2388
+ function parseLinearGradient(value) {
2389
+ if (!isLinearGradient(value))
2390
+ return undefined;
2391
+ const trimmed = value.trim();
2392
+ const open = trimmed.indexOf('(');
2393
+ const close = trimmed.lastIndexOf(')');
2394
+ if (open < 0 || close <= open)
2395
+ return undefined;
2396
+ const args = splitGradientArgs(trimmed.slice(open + 1, close));
2397
+ if (!args.length)
2398
+ return undefined;
2399
+ let angle = 180;
2400
+ const firstAngle = parseAngleToken(args[0]);
2401
+ if (firstAngle !== undefined) {
2402
+ angle = firstAngle;
2403
+ args.shift();
2404
+ }
2405
+ const colors = [];
2406
+ const offsets = [];
2407
+ for (const arg of args) {
2408
+ const match = arg.match(/^(.*?)(?:\s+(-?[\d.]+)%)?$/);
2409
+ const color = parseColor((match?.[1] ?? arg).trim());
2410
+ if (!color)
2411
+ return undefined;
2412
+ colors.push(color);
2413
+ const pct = match?.[2] !== undefined ? parseFloat(match[2]) : NaN;
2414
+ offsets.push(Number.isFinite(pct) ? Math.min(1, Math.max(0, pct / 100)) : undefined);
2415
+ }
2416
+ if (colors.length < 2)
2417
+ return undefined;
2418
+ if (offsets[0] === undefined)
2419
+ offsets[0] = 0;
2420
+ if (offsets[offsets.length - 1] === undefined)
2421
+ offsets[offsets.length - 1] = 1;
2422
+ let prev = 0;
2423
+ for (let i = 1; i < offsets.length; i++) {
2424
+ if (offsets[i] === undefined)
2425
+ continue;
2426
+ const gap = i - prev;
2427
+ for (let j = prev + 1; j < i; j++) {
2428
+ offsets[j] = offsets[prev] + ((offsets[i] - offsets[prev]) * (j - prev)) / gap;
2429
+ }
2430
+ prev = i;
2431
+ }
2432
+ return { angle, stops: colors.map((color, i) => ({ color, offset: offsets[i] })) };
2433
+ }
2434
+ // CSS gradient line for a w x h box: length |w*sin| + |h*cos|, centred on the box.
2435
+ function gradientLine(angleDeg, width, height) {
2436
+ const theta = (angleDeg * Math.PI) / 180;
2437
+ const length = Math.abs(width * Math.sin(theta)) + Math.abs(height * Math.cos(theta));
2438
+ const dx = (Math.sin(theta) * length) / 2;
2439
+ const dy = (-Math.cos(theta) * length) / 2;
2440
+ const cx = width / 2;
2441
+ const cy = height / 2;
2442
+ return { x1: cx - dx, y1: cy - dy, x2: cx + dx, y2: cy + dy };
2443
+ }
2444
+ // NAM-3538: solid stand-in when SVG is unavailable (VegaOS) — the last stop keeps a
2445
+ // "darken toward the bottom" scrim as a uniform darken rather than nothing.
2446
+ function pickOverlayFallbackColor(spec) {
2447
+ return spec.stops[spec.stops.length - 1]?.color;
2448
+ }
2332
2449
  function parseSize(value, scaleFactor = 1) {
2333
2450
  if (value === undefined || value === null)
2334
2451
  return undefined;
@@ -2788,6 +2905,15 @@ function valueFromSmartText(ctx, value, sku, block = null) {
2788
2905
  }
2789
2906
  return value;
2790
2907
  }
2908
+ // NAM-3538: the background container bypasses TemplateRenderer, so only its overlayColor
2909
+ // is run through conditionAttributes + smart text.
2910
+ function resolveOverlayColor(ctx, component) {
2911
+ if (!component)
2912
+ return undefined;
2913
+ const resolved = component.conditionAttributes ? withOverrides(ctx, component) : component;
2914
+ const value = valueFromSmartText(ctx, resolved.overlayColor);
2915
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
2916
+ }
2791
2917
  function resolveComponentSmartText(component, ctx) {
2792
2918
  const smartTextSku = componentSmartTextSku(component);
2793
2919
  const replacements = buildSmartTextReplacements(ctx.state, ctx.flow, smartTextSku);
@@ -3109,14 +3235,14 @@ const NamiContentContainer = ({ component, scaleFactor, onClose, }) => {
3109
3235
  }, [component.verticalAlignment, component.horizontalAlignment, flexDir]);
3110
3236
  const renderedChildren = require$$0.useMemo(() => {
3111
3237
  return component.components?.map((comp, i) => (jsxRuntimeExports.jsx(reactNative.View, { style: [
3112
- styles$6.fullWidth,
3238
+ styles$7.fullWidth,
3113
3239
  i === 0
3114
3240
  ? null
3115
3241
  : childSpacingStyle(i, { spacing: component.spacing, direction }, scaleFactor),
3116
3242
  ], children: jsxRuntimeExports.jsx(TemplateRenderer, { component: comp, scaleFactor: scaleFactor, onClose: onClose, parentDirection: direction }) }, comp.id ?? i)));
3117
3243
  }, [component.components, component.spacing, direction, scaleFactor, onClose]);
3118
3244
  const innerContentStyle = require$$0.useMemo(() => [
3119
- styles$6.inner,
3245
+ styles$7.inner,
3120
3246
  {
3121
3247
  flexDirection: flexDir,
3122
3248
  },
@@ -3124,11 +3250,11 @@ const NamiContentContainer = ({ component, scaleFactor, onClose, }) => {
3124
3250
  contentAlignment,
3125
3251
  ], [flexDir, inner, contentAlignment]);
3126
3252
  if (!scrollEnabled) {
3127
- return (jsxRuntimeExports.jsx(reactNative.View, { style: [styles$6.outer, outer], children: jsxRuntimeExports.jsx(reactNative.View, { style: [styles$6.scroll, innerContentStyle], children: renderedChildren }) }));
3253
+ return (jsxRuntimeExports.jsx(reactNative.View, { style: [styles$7.outer, outer], children: jsxRuntimeExports.jsx(reactNative.View, { style: [styles$7.scroll, innerContentStyle], children: renderedChildren }) }));
3128
3254
  }
3129
- 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 }) }));
3255
+ return (jsxRuntimeExports.jsx(reactNative.View, { style: [styles$7.outer, outer], children: jsxRuntimeExports.jsx(reactNative.ScrollView, { style: styles$7.scroll, contentContainerStyle: innerContentStyle, showsVerticalScrollIndicator: false, scrollEnabled: true, children: renderedChildren }) }));
3130
3256
  };
3131
- const styles$6 = reactNative.StyleSheet.create({
3257
+ const styles$7 = reactNative.StyleSheet.create({
3132
3258
  outer: {
3133
3259
  flex: 1,
3134
3260
  minHeight: 0,
@@ -3146,10 +3272,76 @@ const styles$6 = reactNative.StyleSheet.create({
3146
3272
  },
3147
3273
  });
3148
3274
 
3275
+ // react-native-svg may be missing on some runtimes (e.g. VegaOS); fall back to a solid color.
3276
+ let SvgComponent = null;
3277
+ let DefsComponent = null;
3278
+ let LinearGradientComponent = null;
3279
+ let StopComponent = null;
3280
+ let RectComponent = null;
3281
+ try {
3282
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
3283
+ const svg = require('react-native-svg');
3284
+ SvgComponent = svg.default ?? svg.Svg ?? null;
3285
+ DefsComponent = svg.Defs ?? null;
3286
+ LinearGradientComponent = svg.LinearGradient ?? null;
3287
+ StopComponent = svg.Stop ?? null;
3288
+ RectComponent = svg.Rect ?? null;
3289
+ }
3290
+ catch {
3291
+ SvgComponent = null;
3292
+ }
3293
+ const RADIUS_KEYS = [
3294
+ 'borderRadius',
3295
+ 'borderTopLeftRadius',
3296
+ 'borderTopRightRadius',
3297
+ 'borderBottomLeftRadius',
3298
+ 'borderBottomRightRadius',
3299
+ ];
3300
+ // NAM-3538: scrim painted above the background container's children, below the page content.
3301
+ const BackgroundOverlay = ({ color, radiusStyle }) => {
3302
+ const [size, setSize] = require$$0.useState({ w: 0, h: 0 });
3303
+ const radii = {};
3304
+ RADIUS_KEYS.forEach((key) => {
3305
+ if (radiusStyle[key] !== undefined)
3306
+ radii[key] = radiusStyle[key];
3307
+ });
3308
+ const base = [styles$6.overlay, radii];
3309
+ const solid = parseColor(color);
3310
+ if (solid) {
3311
+ return jsxRuntimeExports.jsx(reactNative.View, { pointerEvents: 'none', style: [...base, { backgroundColor: solid }] });
3312
+ }
3313
+ const spec = parseLinearGradient(color);
3314
+ if (!spec)
3315
+ return null;
3316
+ const canRenderSvg = Boolean(SvgComponent && DefsComponent && LinearGradientComponent && StopComponent && RectComponent);
3317
+ if (!canRenderSvg) {
3318
+ return jsxRuntimeExports.jsx(reactNative.View, { pointerEvents: 'none', style: [...base, { backgroundColor: pickOverlayFallbackColor(spec) }] });
3319
+ }
3320
+ const Svg = SvgComponent;
3321
+ const Defs = DefsComponent;
3322
+ const Gradient = LinearGradientComponent;
3323
+ const Stop = StopComponent;
3324
+ const Rect = RectComponent;
3325
+ const onLayout = (e) => {
3326
+ const { width, height } = e.nativeEvent.layout;
3327
+ setSize((prev) => (prev.w === width && prev.h === height ? prev : { w: width, h: height }));
3328
+ };
3329
+ return (jsxRuntimeExports.jsx(reactNative.View, { pointerEvents: 'none', style: base, onLayout: onLayout, children: size.w > 0 && size.h > 0 ? (jsxRuntimeExports.jsxs(Svg, { width: size.w, height: size.h, children: [jsxRuntimeExports.jsx(Defs, { children: jsxRuntimeExports.jsx(Gradient, { id: 'namiOverlay', gradientUnits: 'userSpaceOnUse', ...gradientLine(spec.angle, size.w, size.h), children: spec.stops.map((stop, i) => (jsxRuntimeExports.jsx(Stop, { offset: stop.offset, stopColor: stop.color }, i))) }) }), jsxRuntimeExports.jsx(Rect, { width: size.w, height: size.h, fill: 'url(#namiOverlay)' })] })) : null }));
3330
+ };
3331
+ const styles$6 = reactNative.StyleSheet.create({
3332
+ overlay: {
3333
+ ...reactNative.StyleSheet.absoluteFillObject,
3334
+ overflow: 'hidden',
3335
+ },
3336
+ });
3337
+
3149
3338
  const NamiBackgroundContainer = ({ component, scaleFactor, onClose }) => {
3339
+ const ctx = usePaywallContext();
3340
+ const containerStyle = require$$0.useMemo(() => (component ? applyStyles(component, scaleFactor) : {}), [component, scaleFactor]);
3341
+ // NAM-3538: only overlayColor is resolved here; the container itself skips TemplateRenderer.
3342
+ const overlayColor = require$$0.useMemo(() => resolveOverlayColor(ctx, component), [ctx, component]);
3150
3343
  if (!component || component.hidden)
3151
3344
  return null;
3152
- const containerStyle = require$$0.useMemo(() => applyStyles(component, scaleFactor), [component, scaleFactor]);
3153
3345
  const fillImageUrl = resolveFillImageUrl(component.fillImage);
3154
3346
  const bgColor = parseColor(component.fillColor) ?? parseColor(component.fillColorFallback);
3155
3347
  // Only clip when the container is a filled "tile" with specific (non-all-corners)
@@ -3169,10 +3361,11 @@ const NamiBackgroundContainer = ({ component, scaleFactor, onClose }) => {
3169
3361
  };
3170
3362
  return (jsxRuntimeExports.jsx(TemplateRenderer, { component: spacedComponent, scaleFactor: scaleFactor, onClose: onClose, parentDirection: direction }, comp.id ?? i));
3171
3363
  }) }));
3364
+ const overlay = overlayColor ? jsxRuntimeExports.jsx(BackgroundOverlay, { color: overlayColor, radiusStyle: containerStyle }) : null;
3172
3365
  if (fillImageUrl) {
3173
- return (jsxRuntimeExports.jsx(reactNative.ImageBackground, { source: { uri: fillImageUrl }, resizeMode: 'cover', style: [containerStyle, styles$5.background, clipStyle], children: content }));
3366
+ return (jsxRuntimeExports.jsxs(reactNative.ImageBackground, { source: { uri: fillImageUrl }, resizeMode: 'cover', style: [containerStyle, styles$5.background, clipStyle], children: [content, overlay] }));
3174
3367
  }
3175
- return (jsxRuntimeExports.jsx(reactNative.View, { style: [containerStyle, bgColor ? { backgroundColor: bgColor } : null, styles$5.background, clipStyle], children: content }));
3368
+ return (jsxRuntimeExports.jsxs(reactNative.View, { style: [containerStyle, bgColor ? { backgroundColor: bgColor } : null, styles$5.background, clipStyle], children: [content, overlay] }));
3176
3369
  };
3177
3370
  function mergeRawMarginValue(existing, spacing) {
3178
3371
  if (existing == null)
@@ -3472,13 +3665,15 @@ const PaywallScreen = ({ paywall, onClose, onCommitted, holdInteractionUntilFocu
3472
3665
  const ctx = usePaywallContext();
3473
3666
  const insets = reactNativeSafeAreaContext.useSafeAreaInsets();
3474
3667
  const focusReadyCtx = useFirstFocusReadyContext();
3475
- // Push the measured top and bottom safe-area insets into paywall state so
3476
- // templates can position chrome via ${state.safeAreaTop} (e.g. header
3477
- // topPadding, background topMargin) and ${state.safeAreaBottom} (e.g. a
3478
- // footer clear of the home indicator / gesture bar). Mirrors the native
3479
- // renderers, which measure the insets and set the same state. The full-bleed
3480
- // root lets the background extend under the system bars and nothing pads the
3481
- // footer, so the token is the only bottom offset — no double-padding.
3668
+ // Push the measured safe-area insets into paywall state so templates can
3669
+ // position chrome via ${state.safeAreaTop} (e.g. header topPadding,
3670
+ // background topMargin), ${state.safeAreaBottom} (e.g. a footer clear of the
3671
+ // home indicator / gesture bar) and ${state.safeAreaLeft} /
3672
+ // ${state.safeAreaRight} (content clear of a landscape notch, cutout or
3673
+ // side navigation bar). Mirrors the native renderers, which measure the
3674
+ // insets and set the same state. The full-bleed root lets the background
3675
+ // extend under the system bars and nothing pads the footer or the sides, so
3676
+ // each token is the only offset on its edge — no double-padding.
3482
3677
  // Depend only on the inset values (stable per device/orientation) —
3483
3678
  // depending on ctx would re-run every render and loop through setState.
3484
3679
  require$$0.useEffect(() => {
@@ -3487,6 +3682,12 @@ const PaywallScreen = ({ paywall, onClose, onCommitted, holdInteractionUntilFocu
3487
3682
  require$$0.useEffect(() => {
3488
3683
  ctx.setSafeAreaBottom(insets.bottom);
3489
3684
  }, [insets.bottom]);
3685
+ require$$0.useEffect(() => {
3686
+ ctx.setSafeAreaLeft(insets.left);
3687
+ }, [insets.left]);
3688
+ require$$0.useEffect(() => {
3689
+ ctx.setSafeAreaRight(insets.right);
3690
+ }, [insets.right]);
3490
3691
  const scaleFactor = sdkCore.getDeviceScaleFactor(ctx.state.formFactor);
3491
3692
  const userInteractionEnabled = ctx.state.userInteractionEnabled !== false;
3492
3693
  const currentPageName = ctx.state.selectedPaywall === paywall