@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 +216 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.mjs +216 -15
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/components/PaywallScreen.tsx +15 -7
- package/src/components/containers/BackgroundOverlay.tsx +94 -0
- package/src/components/containers/NamiBackgroundContainer.tsx +13 -3
- package/src/context/PaywallContext.tsx +8 -0
- package/src/utils/rendering.ts +9 -0
- package/src/utils/styles.ts +68 -5
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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.
|
|
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.
|
|
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
|
|
3476
|
-
//
|
|
3477
|
-
//
|
|
3478
|
-
//
|
|
3479
|
-
//
|
|
3480
|
-
//
|
|
3481
|
-
//
|
|
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
|