react-native-revnix 1.4.0
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/LICENSE +21 -0
- package/README.md +766 -0
- package/dist/blocks/background.d.ts +101 -0
- package/dist/blocks/background.js +165 -0
- package/dist/blocks/color.d.ts +88 -0
- package/dist/blocks/color.js +329 -0
- package/dist/blocks/locale.d.ts +42 -0
- package/dist/blocks/locale.js +263 -0
- package/dist/blocks/selection.d.ts +49 -0
- package/dist/blocks/selection.js +109 -0
- package/dist/blocks/style.d.ts +93 -0
- package/dist/blocks/style.js +365 -0
- package/dist/blocks/tags.d.ts +12 -0
- package/dist/blocks/tags.js +121 -0
- package/dist/blocks/types.d.ts +301 -0
- package/dist/blocks/types.js +44 -0
- package/dist/client.d.ts +402 -0
- package/dist/client.js +1653 -0
- package/dist/device.d.ts +29 -0
- package/dist/device.js +137 -0
- package/dist/errors.d.ts +39 -0
- package/dist/errors.js +68 -0
- package/dist/esm/blocks/background.js +160 -0
- package/dist/esm/blocks/color.js +318 -0
- package/dist/esm/blocks/locale.js +255 -0
- package/dist/esm/blocks/selection.js +100 -0
- package/dist/esm/blocks/style.js +356 -0
- package/dist/esm/blocks/tags.js +117 -0
- package/dist/esm/blocks/types.js +40 -0
- package/dist/esm/client.js +1649 -0
- package/dist/esm/device.js +130 -0
- package/dist/esm/errors.js +59 -0
- package/dist/esm/iap.js +101 -0
- package/dist/esm/implicit.js +63 -0
- package/dist/esm/index.js +29 -0
- package/dist/esm/package.json +1 -0
- package/dist/esm/storage.js +78 -0
- package/dist/esm/types.js +4 -0
- package/dist/esm/ui/RevnixBlockPaywall.js +706 -0
- package/dist/esm/ui/RevnixPaywall.js +890 -0
- package/dist/esm/ui/index.js +6 -0
- package/dist/esm/version.js +4 -0
- package/dist/iap.d.ts +20 -0
- package/dist/iap.js +104 -0
- package/dist/implicit.d.ts +69 -0
- package/dist/implicit.js +67 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +50 -0
- package/dist/storage.d.ts +14 -0
- package/dist/storage.js +85 -0
- package/dist/types.d.ts +463 -0
- package/dist/types.js +5 -0
- package/dist/ui/RevnixBlockPaywall.d.ts +96 -0
- package/dist/ui/RevnixBlockPaywall.js +742 -0
- package/dist/ui/RevnixPaywall.d.ts +148 -0
- package/dist/ui/RevnixPaywall.js +927 -0
- package/dist/ui/index.d.ts +7 -0
- package/dist/ui/index.js +25 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +7 -0
- package/package.json +79 -0
- package/src/blocks/background.ts +232 -0
- package/src/blocks/color.ts +352 -0
- package/src/blocks/locale.ts +271 -0
- package/src/blocks/paywall-background-wire.json +12 -0
- package/src/blocks/paywall-selection-wire.json +149 -0
- package/src/blocks/selection.ts +161 -0
- package/src/blocks/style.ts +411 -0
- package/src/blocks/tags.ts +125 -0
- package/src/blocks/types.ts +352 -0
- package/src/client.ts +1993 -0
- package/src/device.ts +146 -0
- package/src/errors.ts +69 -0
- package/src/iap.ts +137 -0
- package/src/implicit.ts +129 -0
- package/src/index.ts +82 -0
- package/src/storage.ts +92 -0
- package/src/types.ts +528 -0
- package/src/ui/RevnixBlockPaywall.tsx +1423 -0
- package/src/ui/RevnixPaywall.tsx +1542 -0
- package/src/ui/index.ts +43 -0
- package/src/version.ts +4 -0
- package/ui.d.ts +1 -0
- package/ui.js +5 -0
|
@@ -0,0 +1,742 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// RevnixBlockPaywall — renders a designed paywall (a block tree) natively.
|
|
3
|
+
//
|
|
4
|
+
// The dashboard's paywall builder publishes `config.blocks`: a tree of styled
|
|
5
|
+
// elements the builder previews with BlockScreen.tsx (revnix-app). This is the
|
|
6
|
+
// second interpreter of that same document, so the design a merchant approves
|
|
7
|
+
// in the dashboard is the design their customers see. Keep the two in
|
|
8
|
+
// lockstep — a block type only one side understands ships as a surprise.
|
|
9
|
+
//
|
|
10
|
+
// What React Native cannot reproduce is listed on BlockStyle and handled by
|
|
11
|
+
// degrading rather than failing:
|
|
12
|
+
// * CSS gradients need a gradient component from the host app (`gradient`
|
|
13
|
+
// prop). Without one the screen paints the design's ground color.
|
|
14
|
+
// * backdrop blur has no RN equivalent; the translucent fill carries it.
|
|
15
|
+
// * Design fonts render only through `fontFamilyMap` — RN cannot load a
|
|
16
|
+
// family by name at runtime.
|
|
17
|
+
//
|
|
18
|
+
// RevnixPaywall delegates here whenever a config carries `blocks`, so apps
|
|
19
|
+
// get designed paywalls without changing a line of their integration.
|
|
20
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
21
|
+
if (k2 === undefined) k2 = k;
|
|
22
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
23
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
24
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
25
|
+
}
|
|
26
|
+
Object.defineProperty(o, k2, desc);
|
|
27
|
+
}) : (function(o, m, k, k2) {
|
|
28
|
+
if (k2 === undefined) k2 = k;
|
|
29
|
+
o[k2] = m[k];
|
|
30
|
+
}));
|
|
31
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
32
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
33
|
+
}) : function(o, v) {
|
|
34
|
+
o["default"] = v;
|
|
35
|
+
});
|
|
36
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
37
|
+
var ownKeys = function(o) {
|
|
38
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
39
|
+
var ar = [];
|
|
40
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
41
|
+
return ar;
|
|
42
|
+
};
|
|
43
|
+
return ownKeys(o);
|
|
44
|
+
};
|
|
45
|
+
return function (mod) {
|
|
46
|
+
if (mod && mod.__esModule) return mod;
|
|
47
|
+
var result = {};
|
|
48
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
49
|
+
__setModuleDefault(result, mod);
|
|
50
|
+
return result;
|
|
51
|
+
};
|
|
52
|
+
})();
|
|
53
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
54
|
+
exports.RevnixBlockPaywall = RevnixBlockPaywall;
|
|
55
|
+
const React = __importStar(require("react"));
|
|
56
|
+
const react_native_1 = require("react-native");
|
|
57
|
+
const color_js_1 = require("../blocks/color.js");
|
|
58
|
+
const style_js_1 = require("../blocks/style.js");
|
|
59
|
+
const background_js_1 = require("../blocks/background.js");
|
|
60
|
+
const locale_js_1 = require("../blocks/locale.js");
|
|
61
|
+
const tags_js_1 = require("../blocks/tags.js");
|
|
62
|
+
const selection_js_1 = require("../blocks/selection.js");
|
|
63
|
+
const types_js_1 = require("../blocks/types.js");
|
|
64
|
+
/** CSS angle (0 = up, 90 = right) → RN gradient start/end points. */
|
|
65
|
+
function angleToPoints(angle) {
|
|
66
|
+
const rad = (angle * Math.PI) / 180;
|
|
67
|
+
const dx = Math.sin(rad);
|
|
68
|
+
const dy = -Math.cos(rad);
|
|
69
|
+
return {
|
|
70
|
+
start: { x: 0.5 - dx / 2, y: 0.5 - dy / 2 },
|
|
71
|
+
end: { x: 0.5 + dx / 2, y: 0.5 + dy / 2 },
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* A radial gradient has no React Native equivalent, so it is stood in for by a
|
|
76
|
+
* linear one. Two things make that stand-in read like the original rather than
|
|
77
|
+
* a coloured band:
|
|
78
|
+
*
|
|
79
|
+
* * DIRECTION comes from the radial's own centre — a glow anchored at the top
|
|
80
|
+
* of the screen has to fade downwards, not upwards.
|
|
81
|
+
* * ALPHA is scaled down. A radial reaches its opening colour only at the
|
|
82
|
+
* centre point and falls off in every direction; a linear paints that
|
|
83
|
+
* colour across the entire width. Carrying the stated alpha across would
|
|
84
|
+
* turn a soft glow into a solid header.
|
|
85
|
+
*
|
|
86
|
+
* Linear layers translate exactly, and a solid is a flat fill.
|
|
87
|
+
*/
|
|
88
|
+
const RADIAL_ALPHA = 0.55;
|
|
89
|
+
function layerColors(layer) {
|
|
90
|
+
if (layer.kind === "linear" && layer.colors.length >= 2) {
|
|
91
|
+
const colors = layer.colors;
|
|
92
|
+
const locations = layer.locations && layer.locations.length >= 2
|
|
93
|
+
? layer.locations
|
|
94
|
+
: undefined;
|
|
95
|
+
return { colors, locations, angle: layer.angle };
|
|
96
|
+
}
|
|
97
|
+
const first = layer.colors[0];
|
|
98
|
+
if (layer.kind !== "radial") {
|
|
99
|
+
return { colors: [first, first], angle: layer.angle };
|
|
100
|
+
}
|
|
101
|
+
const last = layer.colors[layer.colors.length - 1];
|
|
102
|
+
// Fade away from the centre: a centre in the top third falls downwards, one
|
|
103
|
+
// in the bottom third upwards, anything else keeps the default.
|
|
104
|
+
const y = layer.center?.y ?? 0.5;
|
|
105
|
+
const angle = y >= 0.65 ? 0 : 180;
|
|
106
|
+
// Where the radial's outermost stop sits, so the glow stops where it did.
|
|
107
|
+
const extent = layer.locations?.[layer.locations.length - 1] ?? 1;
|
|
108
|
+
return {
|
|
109
|
+
colors: [(0, color_js_1.withAlpha)(first, RADIAL_ALPHA), (0, color_js_1.withAlpha)(last, 0)],
|
|
110
|
+
locations: [0, Math.max(0.05, Math.min(1, extent))],
|
|
111
|
+
angle,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
const FILL_ABSOLUTE = {
|
|
115
|
+
position: "absolute",
|
|
116
|
+
top: 0,
|
|
117
|
+
right: 0,
|
|
118
|
+
bottom: 0,
|
|
119
|
+
left: 0,
|
|
120
|
+
};
|
|
121
|
+
/** The gradient layers of a fill, as absolutely-positioned siblings to be
|
|
122
|
+
* rendered behind a box's content. Null whenever the fill is a plain color,
|
|
123
|
+
* or no gradient component was supplied — in both cases the caller's
|
|
124
|
+
* `backgroundColor` (the design's ground color) is the whole paint job. */
|
|
125
|
+
function GradientLayers({ fill, doc, gradient: Gradient, radius, onDiagnostic, }) {
|
|
126
|
+
const resolved = (0, color_js_1.resolveColor)(fill, doc);
|
|
127
|
+
if (!resolved || !(0, color_js_1.isGradient)(resolved))
|
|
128
|
+
return null;
|
|
129
|
+
if (!Gradient) {
|
|
130
|
+
// The fill still paints — `toViewStyle` degraded it to its ground stop —
|
|
131
|
+
// but the design asked for a gradient and this app passed no gradient
|
|
132
|
+
// component, so say so rather than letting it silently render flat.
|
|
133
|
+
onDiagnostic?.(`gradient flattened: pass RevnixPaywall a \`gradient\` component to render ${resolved}`);
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
const layers = (0, color_js_1.parseGradient)(resolved);
|
|
137
|
+
if (layers.length === 0)
|
|
138
|
+
return null;
|
|
139
|
+
return (React.createElement(React.Fragment, null, [...layers].reverse().map((layer, i) => {
|
|
140
|
+
const { colors, locations, angle } = layerColors(layer);
|
|
141
|
+
const { start, end } = angleToPoints(angle);
|
|
142
|
+
return (React.createElement(Gradient, { key: i, colors: colors, locations: locations, start: start, end: end, style: radius === undefined
|
|
143
|
+
? FILL_ABSOLUTE
|
|
144
|
+
: [FILL_ABSOLUTE, { borderRadius: radius }] }));
|
|
145
|
+
})));
|
|
146
|
+
}
|
|
147
|
+
/** A block's own gradient fill, ready to drop in as the first child of
|
|
148
|
+
* whatever box that block renders. `toViewStyle` has already painted the
|
|
149
|
+
* ground stop as the box's backgroundColor, so this adds the real gradient
|
|
150
|
+
* over it — and renders nothing at all for a plain colour. */
|
|
151
|
+
function BlockFill({ block, ctx, }) {
|
|
152
|
+
return (React.createElement(GradientLayers, { fill: block.style?.fill, doc: ctx.doc, gradient: ctx.gradient, radius: block.style?.radius, onDiagnostic: ctx.onDiagnostic }));
|
|
153
|
+
}
|
|
154
|
+
/** The solid a fill paints under any gradient layers. */
|
|
155
|
+
function fillColor(fill, doc) {
|
|
156
|
+
const resolved = (0, color_js_1.resolveColor)(fill, doc);
|
|
157
|
+
if (!resolved)
|
|
158
|
+
return undefined;
|
|
159
|
+
return (0, color_js_1.isGradient)(resolved) ? (0, color_js_1.gradientFallbackColor)(resolved) : resolved;
|
|
160
|
+
}
|
|
161
|
+
/** Paints a fill that may be a CSS gradient, behind `children`.
|
|
162
|
+
*
|
|
163
|
+
* The solid goes under the gradient only when the gradient itself cannot be
|
|
164
|
+
* drawn. A scrim is transparent at one end on purpose, and a solid under it
|
|
165
|
+
* was a black sheet over the background photo — see `surfaceSolid`. */
|
|
166
|
+
function Surface({ fill, doc, gradient, style, children, }) {
|
|
167
|
+
const solid = (0, color_js_1.surfaceSolid)((0, color_js_1.resolveColor)(fill, doc), gradient !== undefined);
|
|
168
|
+
return (React.createElement(react_native_1.View, { style: [style, solid ? { backgroundColor: solid } : null] },
|
|
169
|
+
React.createElement(GradientLayers, { fill: fill, doc: doc, gradient: gradient }),
|
|
170
|
+
children));
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* The screen background's photo layer.
|
|
174
|
+
*
|
|
175
|
+
* React Native's <Image> has no `object-position`, so a focal point other
|
|
176
|
+
* than the centre is drawn by hand: measure the box, ask the platform for
|
|
177
|
+
* the source dimensions, and place the scaled image with `coverPlacement` —
|
|
178
|
+
* the same rule CSS applies for `object-position: X% Y%`. Until both
|
|
179
|
+
* measurements land (and always for a centred focal point, and for
|
|
180
|
+
* `contain`) the plain resizeMode path is already correct, so there is no
|
|
181
|
+
* flash of a wrongly-cropped photo.
|
|
182
|
+
*/
|
|
183
|
+
function BackgroundPhoto({ image, blur: Blur, }) {
|
|
184
|
+
const centred = image.focalX === 50 && image.focalY === 50;
|
|
185
|
+
const needsPlacement = image.fit === "cover" && !centred;
|
|
186
|
+
const [box, setBox] = React.useState(null);
|
|
187
|
+
const [source, setSource] = React.useState(null);
|
|
188
|
+
React.useEffect(() => {
|
|
189
|
+
if (!needsPlacement)
|
|
190
|
+
return;
|
|
191
|
+
let live = true;
|
|
192
|
+
react_native_1.Image.getSize(image.url, (width, height) => live && setSource({ width, height }),
|
|
193
|
+
// A failed measure is not an error worth surfacing — the centred
|
|
194
|
+
// resizeMode path below stays in place.
|
|
195
|
+
() => undefined);
|
|
196
|
+
return () => {
|
|
197
|
+
live = false;
|
|
198
|
+
};
|
|
199
|
+
}, [image.url, needsPlacement]);
|
|
200
|
+
const placement = needsPlacement && box && source
|
|
201
|
+
? (0, background_js_1.coverPlacement)(box, source, image.focalX, image.focalY)
|
|
202
|
+
: null;
|
|
203
|
+
const photo = placement ? (React.createElement(react_native_1.Image, { source: { uri: image.url }, style: {
|
|
204
|
+
position: "absolute",
|
|
205
|
+
left: placement.left,
|
|
206
|
+
top: placement.top,
|
|
207
|
+
width: placement.width,
|
|
208
|
+
height: placement.height,
|
|
209
|
+
}, resizeMode: "cover" })) : (React.createElement(react_native_1.Image, { source: { uri: image.url }, style: FILL_ABSOLUTE, resizeMode: image.fit }));
|
|
210
|
+
return (React.createElement(react_native_1.View, { pointerEvents: "none", style: [FILL_ABSOLUTE, { opacity: image.opacity, overflow: "hidden" }], onLayout: needsPlacement
|
|
211
|
+
? (e) => setBox({
|
|
212
|
+
width: e.nativeEvent.layout.width,
|
|
213
|
+
height: e.nativeEvent.layout.height,
|
|
214
|
+
})
|
|
215
|
+
: undefined },
|
|
216
|
+
photo,
|
|
217
|
+
image.blur !== undefined && Blur ? (React.createElement(Blur, { intensity: Math.min(100, Math.round((image.blur / 30) * 100)), radius: image.blur, style: FILL_ABSOLUTE })) : null));
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* The whole screen background: ground, then photo, then scrim — the same
|
|
221
|
+
* three layers, in the same order, as the dashboard renderer paints. A
|
|
222
|
+
* document whose background is still a plain string resolves to a ground
|
|
223
|
+
* and nothing else, so this is byte-for-byte the old `Surface` for every
|
|
224
|
+
* paywall that has not had its background edited.
|
|
225
|
+
*/
|
|
226
|
+
function ScreenBackground({ doc, gradient, blur, style, onDiagnostic, children, }) {
|
|
227
|
+
const layers = (0, background_js_1.backgroundLayers)(doc.background, (value) => (0, color_js_1.resolveColor)(value, doc) ?? value);
|
|
228
|
+
const ground = fillColor(layers.ground, doc);
|
|
229
|
+
return (React.createElement(react_native_1.View, { style: [style, ground ? { backgroundColor: ground } : null] },
|
|
230
|
+
React.createElement(GradientLayers, { fill: layers.ground, doc: doc, gradient: gradient, onDiagnostic: onDiagnostic }),
|
|
231
|
+
layers.image ? (React.createElement(BackgroundPhoto, { image: layers.image, blur: blur })) : null,
|
|
232
|
+
layers.overlay ? (React.createElement(Surface, { fill: layers.overlay.fill, doc: doc, gradient: gradient, style: [FILL_ABSOLUTE, { opacity: layers.overlay.opacity }] })) : null,
|
|
233
|
+
children));
|
|
234
|
+
}
|
|
235
|
+
/** A container block's box. The style lands on the PRESSABLE itself when the
|
|
236
|
+
* card is a selection target — wrapping a styled View in a bare Pressable
|
|
237
|
+
* would leave absolute offsets resolving against a zero-sized parent, which
|
|
238
|
+
* makes an `inset` card vanish and a pinned badge fly off the edge. */
|
|
239
|
+
function CardBox({ style, fill, doc, gradient, radius, onPress, children, }) {
|
|
240
|
+
const layers = (React.createElement(GradientLayers, { fill: fill, doc: doc, gradient: gradient, radius: radius }));
|
|
241
|
+
if (onPress) {
|
|
242
|
+
return (React.createElement(react_native_1.Pressable, { onPress: onPress, style: style },
|
|
243
|
+
layers,
|
|
244
|
+
children));
|
|
245
|
+
}
|
|
246
|
+
return (React.createElement(react_native_1.View, { style: style },
|
|
247
|
+
layers,
|
|
248
|
+
children));
|
|
249
|
+
}
|
|
250
|
+
/** The dismiss affordance the renderer supplies itself (REV-252).
|
|
251
|
+
*
|
|
252
|
+
* Drawn only when the design authors none of its own and the host wired an
|
|
253
|
+
* onClose — which is what makes every paywall published before close existed
|
|
254
|
+
* dismissible without being re-authored, while a design that DOES carry a
|
|
255
|
+
* close chip never ends up showing two.
|
|
256
|
+
*
|
|
257
|
+
* Deliberately plain: it is a safety net, not a design element. It is tinted
|
|
258
|
+
* from the screen's own ink rather than a fixed white so it stays legible on
|
|
259
|
+
* a light design as well as a dark one, and it sits outside the canvas
|
|
260
|
+
* transform so it is never scaled away on a small screen. */
|
|
261
|
+
function FallbackClose({ doc, onPress, safeAreaTop, }) {
|
|
262
|
+
return (React.createElement(react_native_1.Pressable, { accessibilityRole: "button", accessibilityLabel: "Close", hitSlop: 10, onPress: onPress, style: {
|
|
263
|
+
position: "absolute",
|
|
264
|
+
top: 14 + safeAreaTop,
|
|
265
|
+
right: 14,
|
|
266
|
+
zIndex: 50,
|
|
267
|
+
width: 30,
|
|
268
|
+
height: 30,
|
|
269
|
+
borderRadius: 15,
|
|
270
|
+
alignItems: "center",
|
|
271
|
+
justifyContent: "center",
|
|
272
|
+
backgroundColor: (0, color_js_1.withAlpha)(doc.textColor, 0.14),
|
|
273
|
+
} },
|
|
274
|
+
React.createElement(react_native_1.Text, { style: { color: doc.textColor, fontSize: 17, lineHeight: 20 } }, "\u00D7")));
|
|
275
|
+
}
|
|
276
|
+
/** Text blocks always render inside a View: a style may carry both typography
|
|
277
|
+
* and box properties (a round chip is a 34pt circle with a centered glyph),
|
|
278
|
+
* and RN cannot centre a glyph inside <Text> the way flexbox does. */
|
|
279
|
+
function BlockText({ text, style, ctx, inherited, onPress, }) {
|
|
280
|
+
const styleCtx = { ...ctx.style, fontSize: inherited.fontSize };
|
|
281
|
+
// The block's own typography is folded into the inherited set first, so
|
|
282
|
+
// em letter-spacing and unitless line-height resolve against whichever font
|
|
283
|
+
// size actually applies — the block's, or the one it inherited.
|
|
284
|
+
const own = (0, style_js_1.inherit)(inherited, style, styleCtx);
|
|
285
|
+
const body = (React.createElement(React.Fragment, null,
|
|
286
|
+
React.createElement(GradientLayers, { fill: style?.fill, doc: ctx.doc, gradient: ctx.gradient, radius: style?.radius, onDiagnostic: ctx.onDiagnostic }),
|
|
287
|
+
React.createElement(react_native_1.Text, { numberOfLines: style?.nowrap ? 1 : undefined, style: [(0, style_js_1.inheritedTextStyle)(own, ctx.style), textOnly(style, styleCtx)] }, text)));
|
|
288
|
+
// The whole box is the target, not just the glyph: a close chip is mostly
|
|
289
|
+
// padding, and a 12pt × is under the 44pt tap minimum on its own. The style
|
|
290
|
+
// lands on the Pressable itself for the same reason CardBox does it —
|
|
291
|
+
// wrapping a styled View would leave a pinned chip resolving its offsets
|
|
292
|
+
// against a zero-sized parent.
|
|
293
|
+
if (onPress) {
|
|
294
|
+
return (React.createElement(react_native_1.Pressable, { accessibilityRole: "button", accessibilityLabel: "Close", hitSlop: 8, onPress: onPress, style: (0, style_js_1.toViewStyle)(style, styleCtx) }, body));
|
|
295
|
+
}
|
|
296
|
+
return React.createElement(react_native_1.View, { style: (0, style_js_1.toViewStyle)(style, styleCtx) }, body);
|
|
297
|
+
}
|
|
298
|
+
/** The press handler for a block the design marks as the close, or undefined
|
|
299
|
+
* when it is not one (or the host wired no dismissal). One helper so text,
|
|
300
|
+
* image and button blocks cannot drift on what "action: close" means. */
|
|
301
|
+
function closePress(block, ctx) {
|
|
302
|
+
return block.action === "close" && ctx.onClose ? ctx.onClose : undefined;
|
|
303
|
+
}
|
|
304
|
+
/** Typography that is NOT inherited (decoration) — everything else already
|
|
305
|
+
* arrives through the inherited set. */
|
|
306
|
+
function textOnly(s, ctx) {
|
|
307
|
+
const t = (0, style_js_1.toTextStyle)(s, ctx);
|
|
308
|
+
return t.textDecorationLine !== undefined
|
|
309
|
+
? { textDecorationLine: t.textDecorationLine }
|
|
310
|
+
: null;
|
|
311
|
+
}
|
|
312
|
+
function Products({ block, ctx, inherited, }) {
|
|
313
|
+
const { doc, packages } = ctx;
|
|
314
|
+
const base = (0, style_js_1.inheritedTextStyle)(inherited, ctx.style);
|
|
315
|
+
const row = block.direction === "row";
|
|
316
|
+
return (React.createElement(react_native_1.View, { style: [
|
|
317
|
+
{
|
|
318
|
+
flexDirection: row ? "row" : "column",
|
|
319
|
+
gap: block.style?.gap ?? 8,
|
|
320
|
+
},
|
|
321
|
+
(0, style_js_1.toViewStyle)(block.style, ctx.style),
|
|
322
|
+
] },
|
|
323
|
+
React.createElement(BlockFill, { block: block, ctx: ctx }),
|
|
324
|
+
packages.map((pkg) => {
|
|
325
|
+
const selected = pkg.packageId === ctx.selectedId;
|
|
326
|
+
const cardStyle = selected ? block.highlightStyle : block.cardStyle;
|
|
327
|
+
return (React.createElement(react_native_1.Pressable, { key: pkg.packageId, onPress: () => ctx.onSelect(pkg.packageId), style: [
|
|
328
|
+
{
|
|
329
|
+
position: "relative",
|
|
330
|
+
flexGrow: row ? 1 : undefined,
|
|
331
|
+
flexBasis: row ? 0 : undefined,
|
|
332
|
+
gap: 3,
|
|
333
|
+
borderRadius: 14,
|
|
334
|
+
paddingVertical: 12,
|
|
335
|
+
paddingHorizontal: 14,
|
|
336
|
+
borderWidth: 1.5,
|
|
337
|
+
borderColor: selected ? doc.accent : (0, color_js_1.withAlpha)("#808080", 0.35),
|
|
338
|
+
backgroundColor: selected
|
|
339
|
+
? (0, color_js_1.withAlpha)(doc.accent, 0.12)
|
|
340
|
+
: "transparent",
|
|
341
|
+
},
|
|
342
|
+
(0, style_js_1.toViewStyle)(cardStyle, ctx.style),
|
|
343
|
+
] },
|
|
344
|
+
React.createElement(GradientLayers, { fill: cardStyle?.fill, doc: doc, gradient: ctx.gradient, radius: cardStyle?.radius ?? 14, onDiagnostic: ctx.onDiagnostic }),
|
|
345
|
+
selected && block.badgeText ? (React.createElement(react_native_1.View, { style: {
|
|
346
|
+
position: "absolute",
|
|
347
|
+
top: -9,
|
|
348
|
+
right: 12,
|
|
349
|
+
backgroundColor: doc.accent,
|
|
350
|
+
paddingVertical: 2,
|
|
351
|
+
paddingHorizontal: 8,
|
|
352
|
+
borderRadius: 99,
|
|
353
|
+
} },
|
|
354
|
+
React.createElement(react_native_1.Text, { style: {
|
|
355
|
+
color: doc.accentInk,
|
|
356
|
+
fontSize: 10,
|
|
357
|
+
fontWeight: "800",
|
|
358
|
+
letterSpacing: 0.5,
|
|
359
|
+
} }, block.badgeText))) : null,
|
|
360
|
+
React.createElement(react_native_1.Text, { style: [
|
|
361
|
+
base,
|
|
362
|
+
{
|
|
363
|
+
fontWeight: "700",
|
|
364
|
+
fontSize: 14,
|
|
365
|
+
textAlign: row ? "center" : "left",
|
|
366
|
+
},
|
|
367
|
+
] }, (0, tags_js_1.resolveTags)(block.titleTpl ?? "{title}", pkg, packages)),
|
|
368
|
+
React.createElement(react_native_1.Text, { style: [
|
|
369
|
+
base,
|
|
370
|
+
{ fontSize: 13, textAlign: row ? "center" : "left" },
|
|
371
|
+
] }, (0, tags_js_1.resolveTags)(block.priceTpl ?? "{price}", pkg, packages)),
|
|
372
|
+
selected && block.highlightSub ? (React.createElement(react_native_1.Text, { style: [
|
|
373
|
+
base,
|
|
374
|
+
{
|
|
375
|
+
fontSize: 11.5,
|
|
376
|
+
opacity: 0.75,
|
|
377
|
+
textAlign: row ? "center" : "left",
|
|
378
|
+
},
|
|
379
|
+
] }, block.highlightSub)) : null));
|
|
380
|
+
})));
|
|
381
|
+
}
|
|
382
|
+
/** grid track list ("1fr 60px 66px") → per-column flex/width. */
|
|
383
|
+
function gridTracks(block) {
|
|
384
|
+
if (block.gridColumns) {
|
|
385
|
+
return block.gridColumns
|
|
386
|
+
.trim()
|
|
387
|
+
.split(/\s+/)
|
|
388
|
+
.map((track) => {
|
|
389
|
+
const fr = track.match(/^([\d.]*)fr$/);
|
|
390
|
+
if (fr)
|
|
391
|
+
return { flex: fr[1] ? parseFloat(fr[1]) : 1 };
|
|
392
|
+
const px = track.match(/^([\d.]+)px$/);
|
|
393
|
+
if (px)
|
|
394
|
+
return { width: parseFloat(px[1]) };
|
|
395
|
+
return { flex: 1 };
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
return Array.from({ length: block.columns ?? 2 }, () => ({ flex: 1 }));
|
|
399
|
+
}
|
|
400
|
+
function Block({ block, ctx, pkg, inherited, selection, }) {
|
|
401
|
+
const { doc } = ctx;
|
|
402
|
+
const styleCtx = { ...ctx.style, fontSize: inherited.fontSize };
|
|
403
|
+
// A pinned card judges its own context; every other block inherits the
|
|
404
|
+
// nearest plan card's. (A repeat card is judged per instance below.)
|
|
405
|
+
const context = block.type === "card" &&
|
|
406
|
+
block.packageIndex !== undefined &&
|
|
407
|
+
ctx.packages[block.packageIndex] !== undefined
|
|
408
|
+
? (0, selection_js_1.packageContext)(ctx.packages[block.packageIndex], ctx.selectedId)
|
|
409
|
+
: selection;
|
|
410
|
+
if (!(0, selection_js_1.isVisible)(block, context))
|
|
411
|
+
return null;
|
|
412
|
+
// The style this block draws with in its state: `selectedStyle` merged
|
|
413
|
+
// over `style` while selected. Every read below goes through it.
|
|
414
|
+
const style = (0, selection_js_1.effectiveStyle)(block, context);
|
|
415
|
+
// Web flex items shrink by default (`flex: 0 1 auto`); RN's do not. Without
|
|
416
|
+
// matching that, a row the browser fits by squeezing its children overflows
|
|
417
|
+
// the screen here instead.
|
|
418
|
+
const view = { flexShrink: 1, ...(0, style_js_1.toViewStyle)(style, styleCtx) };
|
|
419
|
+
const text = (0, style_js_1.inherit)(inherited, style, styleCtx);
|
|
420
|
+
const size = text.fontSize;
|
|
421
|
+
switch (block.type) {
|
|
422
|
+
case "text":
|
|
423
|
+
return (React.createElement(BlockText, { text: (0, tags_js_1.resolveTags)(block.text, pkg, ctx.packages), style: style, ctx: ctx, inherited: inherited, onPress: closePress(block, ctx) }));
|
|
424
|
+
case "image": {
|
|
425
|
+
const url = block.url || ctx.heroImageUrl;
|
|
426
|
+
const round = block.shape === "circle";
|
|
427
|
+
const s = style;
|
|
428
|
+
// A converted design sizes its own slot; the 160pt default is only for
|
|
429
|
+
// a slot dropped into a flow column, and must not fight it.
|
|
430
|
+
const sized = s !== undefined &&
|
|
431
|
+
(s.inset ||
|
|
432
|
+
s.height !== undefined ||
|
|
433
|
+
s.aspectRatio !== undefined ||
|
|
434
|
+
(s.top !== undefined && s.bottom !== undefined) ||
|
|
435
|
+
s.flex !== undefined);
|
|
436
|
+
// An icon the design marks as the close (REV-252). Same rule as text:
|
|
437
|
+
// the whole box is the target, and the style lands on the Pressable so
|
|
438
|
+
// a pinned icon keeps its offsets.
|
|
439
|
+
const closeImage = closePress(block, ctx);
|
|
440
|
+
const ImageBox = closeImage ? react_native_1.Pressable : react_native_1.View;
|
|
441
|
+
return (React.createElement(ImageBox, { accessibilityRole: closeImage ? "button" : undefined, accessibilityLabel: closeImage ? "Close" : undefined, hitSlop: closeImage ? 8 : undefined, onPress: closeImage, style: [
|
|
442
|
+
sized ? null : { height: 160 },
|
|
443
|
+
{ borderRadius: round ? 9999 : sized ? 0 : 16, overflow: "hidden", flexShrink: 0 },
|
|
444
|
+
view,
|
|
445
|
+
] },
|
|
446
|
+
React.createElement(BlockFill, { block: block, ctx: ctx }),
|
|
447
|
+
url ? (React.createElement(react_native_1.Image, { source: { uri: url }, resizeMode: block.fit ?? "cover", style: { width: "100%", height: "100%" } })) : (React.createElement(react_native_1.View, { style: {
|
|
448
|
+
width: "100%",
|
|
449
|
+
height: "100%",
|
|
450
|
+
backgroundColor: (0, color_js_1.withAlpha)("#7d879b", 0.2),
|
|
451
|
+
} }))));
|
|
452
|
+
}
|
|
453
|
+
case "list":
|
|
454
|
+
return (React.createElement(react_native_1.View, { style: [{ flexDirection: "column", gap: style?.gap ?? 8 }, view] },
|
|
455
|
+
React.createElement(BlockFill, { block: block, ctx: ctx }),
|
|
456
|
+
block.items.map((item, i) => (React.createElement(react_native_1.View, { key: i, style: { flexDirection: "row", gap: 9 } },
|
|
457
|
+
React.createElement(react_native_1.Text, { style: [
|
|
458
|
+
(0, style_js_1.inheritedTextStyle)(text, ctx.style),
|
|
459
|
+
{
|
|
460
|
+
color: (0, color_js_1.resolveColor)(block.iconColor, doc) ?? doc.accent,
|
|
461
|
+
fontWeight: "800",
|
|
462
|
+
},
|
|
463
|
+
] }, item.icon ?? "✓"),
|
|
464
|
+
React.createElement(react_native_1.View, { style: { flexShrink: 1 } },
|
|
465
|
+
React.createElement(react_native_1.Text, { style: [
|
|
466
|
+
(0, style_js_1.inheritedTextStyle)(text, ctx.style),
|
|
467
|
+
{ fontWeight: item.description ? "700" : "500" },
|
|
468
|
+
] }, item.title),
|
|
469
|
+
item.description ? (React.createElement(react_native_1.Text, { style: [
|
|
470
|
+
(0, style_js_1.inheritedTextStyle)(text, ctx.style),
|
|
471
|
+
{ fontSize: 12, opacity: 0.7 },
|
|
472
|
+
] }, item.description)) : null))))));
|
|
473
|
+
case "products":
|
|
474
|
+
return React.createElement(Products, { block: block, ctx: ctx, inherited: text });
|
|
475
|
+
case "button": {
|
|
476
|
+
// A converted CTA sizes itself and centres with flex; the padded default
|
|
477
|
+
// is only for buttons added by hand.
|
|
478
|
+
const sized = style?.height !== undefined;
|
|
479
|
+
// A button the design marks as the close dismisses instead of buying,
|
|
480
|
+
// and takes no accent fill: the CTA must stay the one accented thing on
|
|
481
|
+
// the screen, or a "Not now" competes with "Subscribe" for the eye.
|
|
482
|
+
const close = closePress(block, ctx);
|
|
483
|
+
// A purchase in flight: the button keeps its box and fill, loses its
|
|
484
|
+
// label to a spinner in the ink colour, and ignores taps. A close
|
|
485
|
+
// button is never blocked — the user can always leave.
|
|
486
|
+
const busy = !close && ctx.loading;
|
|
487
|
+
return (React.createElement(react_native_1.Pressable, { accessibilityRole: "button", accessibilityState: { disabled: busy, busy }, disabled: busy, onPress: close ?? ctx.onPurchase, style: ({ pressed }) => [
|
|
488
|
+
{
|
|
489
|
+
backgroundColor: close ? "transparent" : doc.accent,
|
|
490
|
+
borderRadius: 12,
|
|
491
|
+
paddingVertical: sized ? 0 : 15,
|
|
492
|
+
paddingHorizontal: 16,
|
|
493
|
+
alignItems: "center",
|
|
494
|
+
justifyContent: "center",
|
|
495
|
+
},
|
|
496
|
+
view,
|
|
497
|
+
// Pressed feedback (contract §4): 80% while the finger is down.
|
|
498
|
+
pressed && !busy
|
|
499
|
+
? {
|
|
500
|
+
opacity: (typeof view.opacity === "number" ? view.opacity : 1) * 0.8,
|
|
501
|
+
}
|
|
502
|
+
: null,
|
|
503
|
+
] },
|
|
504
|
+
React.createElement(BlockFill, { block: block, ctx: ctx }),
|
|
505
|
+
React.createElement(react_native_1.Text, { style: [
|
|
506
|
+
(0, style_js_1.inheritedTextStyle)(text, ctx.style),
|
|
507
|
+
{
|
|
508
|
+
color: close ? doc.textColor : doc.accentInk,
|
|
509
|
+
fontWeight: "800",
|
|
510
|
+
textAlign: "center",
|
|
511
|
+
},
|
|
512
|
+
// The CTA's own colour/size, when the design set one.
|
|
513
|
+
(0, style_js_1.toTextStyle)(style, styleCtx),
|
|
514
|
+
busy ? { opacity: 0 } : null,
|
|
515
|
+
] }, (0, tags_js_1.resolveTags)(block.label, pkg, ctx.packages)),
|
|
516
|
+
busy ? (React.createElement(react_native_1.ActivityIndicator, { color: doc.accentInk, style: { position: "absolute" } })) : null));
|
|
517
|
+
}
|
|
518
|
+
case "links": {
|
|
519
|
+
const open = (url) => url ? () => void react_native_1.Linking.openURL(url) : undefined;
|
|
520
|
+
const labels = (0, locale_js_1.linkLabels)(ctx.doc.defaultLocale);
|
|
521
|
+
const links = [];
|
|
522
|
+
if (block.showRestore ?? true)
|
|
523
|
+
links.push({ label: labels.restore, onPress: ctx.onRestore });
|
|
524
|
+
if (block.showTerms ?? true)
|
|
525
|
+
links.push({
|
|
526
|
+
label: labels.terms,
|
|
527
|
+
onPress: ctx.onTerms ?? open(block.termsUrl ?? ctx.footer?.termsUrl),
|
|
528
|
+
});
|
|
529
|
+
if (block.showPrivacy ?? true)
|
|
530
|
+
links.push({
|
|
531
|
+
label: labels.privacy,
|
|
532
|
+
onPress: ctx.onPrivacy ?? open(block.privacyUrl ?? ctx.footer?.privacyUrl),
|
|
533
|
+
});
|
|
534
|
+
if (links.length === 0)
|
|
535
|
+
return null;
|
|
536
|
+
return (React.createElement(react_native_1.View, { style: [
|
|
537
|
+
{ flexDirection: "row", justifyContent: "center", gap: 20 },
|
|
538
|
+
view,
|
|
539
|
+
] },
|
|
540
|
+
React.createElement(BlockFill, { block: block, ctx: ctx }),
|
|
541
|
+
links.map((link) => (React.createElement(react_native_1.Pressable, { key: link.label, onPress: link.onPress },
|
|
542
|
+
React.createElement(react_native_1.Text, { style: [
|
|
543
|
+
(0, style_js_1.inheritedTextStyle)(text, ctx.style),
|
|
544
|
+
{ fontSize: text.fontSize === inherited.fontSize ? 12 : text.fontSize },
|
|
545
|
+
{ opacity: 0.65 },
|
|
546
|
+
] }, link.label))))));
|
|
547
|
+
}
|
|
548
|
+
case "line":
|
|
549
|
+
return (
|
|
550
|
+
// The web rule paints in `currentColor` at low alpha — that is the
|
|
551
|
+
// INHERITED text colour, not the screen default.
|
|
552
|
+
React.createElement(react_native_1.View, { style: [{ height: 1, backgroundColor: (0, style_js_1.hairlineColor)(text.color) }, view] },
|
|
553
|
+
React.createElement(BlockFill, { block: block, ctx: ctx })));
|
|
554
|
+
case "spacer":
|
|
555
|
+
// A flexing spacer still honours the rest of its style (a minHeight
|
|
556
|
+
// floor, margins) — flex decides how it grows, not what it is.
|
|
557
|
+
return (React.createElement(react_native_1.View, { style: [block.flex ? { flexGrow: 1 } : { height: 16 }, view] }));
|
|
558
|
+
case "card": {
|
|
559
|
+
const layout = block.layout ?? "column";
|
|
560
|
+
const base = layout === "stack"
|
|
561
|
+
? { position: "relative" }
|
|
562
|
+
: layout === "grid"
|
|
563
|
+
? {
|
|
564
|
+
position: "relative",
|
|
565
|
+
flexDirection: "row",
|
|
566
|
+
flexWrap: "wrap",
|
|
567
|
+
gap: style?.gap ?? 10,
|
|
568
|
+
}
|
|
569
|
+
: {
|
|
570
|
+
position: "relative",
|
|
571
|
+
flexDirection: layout === "row" ? "row" : "column",
|
|
572
|
+
gap: style?.gap ?? 10,
|
|
573
|
+
};
|
|
574
|
+
const tracks = layout === "grid" ? gridTracks(block) : null;
|
|
575
|
+
const renderChildren = (context, childSelection, childText = text) => block.children.map((child, i) => {
|
|
576
|
+
const node = (React.createElement(Block, { key: child.id, block: child, ctx: ctx, pkg: context, inherited: childText, selection: childSelection }));
|
|
577
|
+
if (!tracks)
|
|
578
|
+
return node;
|
|
579
|
+
const track = tracks[i % tracks.length];
|
|
580
|
+
return (React.createElement(react_native_1.View, { key: child.id, style: track.width !== undefined
|
|
581
|
+
? { width: track.width }
|
|
582
|
+
: // A grid row must WRAP after `columns` items, so each cell
|
|
583
|
+
// claims its share of the width as a basis rather than
|
|
584
|
+
// flexing to fit everything on one line.
|
|
585
|
+
{ flexBasis: `${100 / tracks.length}%`, flexGrow: 0, flexShrink: 1 } }, node));
|
|
586
|
+
});
|
|
587
|
+
// A card that describes a package the offering does not reach is
|
|
588
|
+
// dropped rather than shown with unresolved tags.
|
|
589
|
+
if (block.packageIndex !== undefined &&
|
|
590
|
+
ctx.packages[block.packageIndex] === undefined) {
|
|
591
|
+
return null;
|
|
592
|
+
}
|
|
593
|
+
// A repeat card is one block drawn once per package, each instance in
|
|
594
|
+
// its own context, so its box, fill and inherited text are resolved
|
|
595
|
+
// per instance rather than from the `style` computed above.
|
|
596
|
+
if (block.repeat === "packages") {
|
|
597
|
+
const list = ctx.packages
|
|
598
|
+
.length
|
|
599
|
+
? ctx.packages
|
|
600
|
+
: [undefined];
|
|
601
|
+
return (React.createElement(React.Fragment, null, list.map((p, i) => {
|
|
602
|
+
const instance = (0, selection_js_1.packageContext)(p, ctx.selectedId) ?? selection;
|
|
603
|
+
if (!(0, selection_js_1.isVisible)(block, instance))
|
|
604
|
+
return null;
|
|
605
|
+
const es = (0, selection_js_1.effectiveStyle)(block, instance);
|
|
606
|
+
return (React.createElement(CardBox, { key: p?.packageId ?? i, style: [
|
|
607
|
+
base,
|
|
608
|
+
layout === "stack" ? null : { borderRadius: 16 },
|
|
609
|
+
{ flexShrink: 1, ...(0, style_js_1.toViewStyle)(es, styleCtx) },
|
|
610
|
+
], fill: es?.fill, doc: doc, gradient: ctx.gradient, radius: es?.radius ?? (layout === "stack" ? undefined : 16), onPress: p ? () => ctx.onSelect(p.packageId) : undefined }, renderChildren(p, instance, (0, style_js_1.inherit)(inherited, es, styleCtx))));
|
|
611
|
+
})));
|
|
612
|
+
}
|
|
613
|
+
const boxStyle = [
|
|
614
|
+
base,
|
|
615
|
+
layout === "stack" ? null : { borderRadius: 16 },
|
|
616
|
+
view,
|
|
617
|
+
];
|
|
618
|
+
const cardPackage = block.packageIndex !== undefined
|
|
619
|
+
? (ctx.packages[block.packageIndex] ?? pkg)
|
|
620
|
+
: pkg;
|
|
621
|
+
// A card pinned to a package doubles as its selection target — that is
|
|
622
|
+
// how hand-styled plan rows (a highlighted annual beside a plain
|
|
623
|
+
// monthly) become tappable without a products block.
|
|
624
|
+
const pinned = block.packageIndex !== undefined && cardPackage !== undefined;
|
|
625
|
+
return (React.createElement(CardBox, { style: boxStyle, fill: style?.fill, doc: doc, gradient: ctx.gradient, radius: style?.radius ?? (layout === "stack" ? undefined : 16), onPress: pinned ? () => ctx.onSelect(cardPackage.packageId) : undefined }, renderChildren(cardPackage, context)));
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
function RevnixBlockPaywall(props) {
|
|
630
|
+
const { packages } = props;
|
|
631
|
+
// REV-271: the language overlay is applied ONCE, here, so every renderer
|
|
632
|
+
// path below reads plain strings and cannot forget to localize one. Memoized
|
|
633
|
+
// on the doc and the locale because it rebuilds the tree, and the renderer
|
|
634
|
+
// re-runs on every selection tap.
|
|
635
|
+
const doc = React.useMemo(() => (0, locale_js_1.localizeBlockDoc)(props.doc, props.locale ?? (0, locale_js_1.detectLocale)()), [props.doc, props.locale]);
|
|
636
|
+
const [internalSelected, setInternalSelected] = React.useState(undefined);
|
|
637
|
+
// Contract §1: host → own tap → design highlight → first package, each
|
|
638
|
+
// only when the offering carries it.
|
|
639
|
+
const selectedId = (0, selection_js_1.selectedPackageId)(packages, props.selectedPackageId, internalSelected, props.highlightPackageId);
|
|
640
|
+
const select = (packageId) => {
|
|
641
|
+
setInternalSelected(packageId);
|
|
642
|
+
props.onSelectPackage?.(packageId);
|
|
643
|
+
};
|
|
644
|
+
const styleCtx = {
|
|
645
|
+
doc,
|
|
646
|
+
fontSize: style_js_1.DEFAULT_FONT_SIZE,
|
|
647
|
+
fontFamilyMap: props.fontFamilyMap,
|
|
648
|
+
onDiagnostic: props.onDiagnostic,
|
|
649
|
+
paintsGradients: props.gradient !== undefined,
|
|
650
|
+
};
|
|
651
|
+
// The screen's own typography is the root of the inheritance chain, exactly
|
|
652
|
+
// as the document's colors and font sit on the web renderer's screen div.
|
|
653
|
+
const rootText = {
|
|
654
|
+
color: doc.textColor,
|
|
655
|
+
fontSize: style_js_1.DEFAULT_FONT_SIZE,
|
|
656
|
+
fontFamilyName: doc.fontFamily,
|
|
657
|
+
};
|
|
658
|
+
const ctx = {
|
|
659
|
+
doc,
|
|
660
|
+
packages,
|
|
661
|
+
selectedId,
|
|
662
|
+
onSelect: select,
|
|
663
|
+
onPurchase: () => {
|
|
664
|
+
if (props.loading || !selectedId)
|
|
665
|
+
return;
|
|
666
|
+
props.onPurchase(selectedId);
|
|
667
|
+
},
|
|
668
|
+
onRestore: props.onRestore,
|
|
669
|
+
onTerms: props.onTerms,
|
|
670
|
+
onPrivacy: props.onPrivacy,
|
|
671
|
+
onClose: props.onClose,
|
|
672
|
+
footer: props.footer,
|
|
673
|
+
heroImageUrl: props.heroImageUrl,
|
|
674
|
+
gradient: props.gradient,
|
|
675
|
+
onDiagnostic: props.onDiagnostic,
|
|
676
|
+
style: styleCtx,
|
|
677
|
+
loading: props.loading === true,
|
|
678
|
+
};
|
|
679
|
+
const children = doc.blocks.map((block) => (React.createElement(Block, { key: block.id, block: block, ctx: ctx, pkg: packages.find((p) => p.packageId === selectedId) ?? packages[0], inherited: rootText })));
|
|
680
|
+
// REV-252: the renderer's own close, drawn only for a design that authors
|
|
681
|
+
// none. Rendered as a sibling of the tree — outside the canvas scale — so
|
|
682
|
+
// it keeps its tap size and its distance from the screen edge whatever the
|
|
683
|
+
// device width does to the design.
|
|
684
|
+
const safeAreaTop = props.safeAreaTop ??
|
|
685
|
+
(react_native_1.Platform.OS === "android" ? (react_native_1.StatusBar.currentHeight ?? 0) : 0);
|
|
686
|
+
const fallbackClose = props.onClose && !(0, types_js_1.hasCloseAction)(doc.blocks) ? (React.createElement(FallbackClose, { doc: doc, onPress: props.onClose, safeAreaTop: safeAreaTop })) : null;
|
|
687
|
+
const [width, setWidth] = React.useState(null);
|
|
688
|
+
const [viewportHeight, setViewportHeight] = React.useState(null);
|
|
689
|
+
if (doc.layout === "canvas") {
|
|
690
|
+
// Authored against the fixed device screen: lay the tree out at 1× and
|
|
691
|
+
// scale the whole thing, so absolute placement inside stack containers
|
|
692
|
+
// stays true at any width. RN scales about the centre, hence the
|
|
693
|
+
// compensating translate — the design must grow from the top-left.
|
|
694
|
+
// Contract §3. The canvas keeps the aspect ratio it was designed at,
|
|
695
|
+
// scaled by width up to the 480pt tablet cap and centred beyond it. On a
|
|
696
|
+
// screen SHORTER than 852:393 it overflows and scrolls — that is what
|
|
697
|
+
// keeps the CTA and the legal links reachable. On a TALLER screen the
|
|
698
|
+
// design is stretched to the viewport instead of leaving a band below
|
|
699
|
+
// it: the root card is "100%" tall and bottom-anchored groups follow it,
|
|
700
|
+
// which is exactly what the web preview does for a taller frame.
|
|
701
|
+
const metrics = width === null ? null : (0, selection_js_1.canvasMetrics)(width, viewportHeight);
|
|
702
|
+
const scale = metrics?.scale ?? 1;
|
|
703
|
+
const overflows = metrics?.overflows ?? false;
|
|
704
|
+
const canvasHeight = metrics?.scaledHeight ?? types_js_1.DESIGN_CANVAS.height;
|
|
705
|
+
const left = metrics?.left ?? 0;
|
|
706
|
+
return (React.createElement(ScreenBackground, { doc: doc, gradient: props.gradient, blur: props.blur, onDiagnostic: props.onDiagnostic, style: [{ flex: 1 }, props.style] },
|
|
707
|
+
React.createElement(react_native_1.ScrollView, { contentContainerStyle: { height: canvasHeight },
|
|
708
|
+
// A designed screen never shows a scrollbar — the web preview has
|
|
709
|
+
// none, and in an RTL app the indicator lands on the left edge of
|
|
710
|
+
// the artwork. When nothing overflows, scrolling is off entirely so
|
|
711
|
+
// a drag cannot bounce the design.
|
|
712
|
+
showsVerticalScrollIndicator: false, scrollEnabled: overflows, bounces: overflows, onLayout: (e) => setViewportHeight(e.nativeEvent.layout.height) },
|
|
713
|
+
React.createElement(react_native_1.View, { onLayout: (e) => setWidth(e.nativeEvent.layout.width), style: { flex: 1, overflow: "hidden" } },
|
|
714
|
+
React.createElement(react_native_1.View, { style: {
|
|
715
|
+
position: "absolute",
|
|
716
|
+
top: 0,
|
|
717
|
+
left: 0,
|
|
718
|
+
width: types_js_1.DESIGN_CANVAS.width,
|
|
719
|
+
// Laid out at design height, or taller when the viewport is:
|
|
720
|
+
// the extra height is in DESIGN units so the scale still
|
|
721
|
+
// maps it onto the screen exactly.
|
|
722
|
+
height: canvasHeight / scale,
|
|
723
|
+
opacity: width === null ? 0 : 1,
|
|
724
|
+
transform: [
|
|
725
|
+
{
|
|
726
|
+
translateX: -(types_js_1.DESIGN_CANVAS.width * (1 - scale)) / 2 + left,
|
|
727
|
+
},
|
|
728
|
+
{ translateY: -((canvasHeight / scale) * (1 - scale)) / 2 },
|
|
729
|
+
{ scale },
|
|
730
|
+
],
|
|
731
|
+
} }, children))),
|
|
732
|
+
fallbackClose));
|
|
733
|
+
}
|
|
734
|
+
return (React.createElement(ScreenBackground, { doc: doc, gradient: props.gradient, blur: props.blur, onDiagnostic: props.onDiagnostic, style: [{ flex: 1 }, props.style] },
|
|
735
|
+
React.createElement(react_native_1.ScrollView, { contentContainerStyle: {
|
|
736
|
+
gap: 12,
|
|
737
|
+
paddingHorizontal: 20,
|
|
738
|
+
paddingTop: 18,
|
|
739
|
+
paddingBottom: 30,
|
|
740
|
+
}, showsVerticalScrollIndicator: false, alwaysBounceVertical: false, overScrollMode: "never" }, children),
|
|
741
|
+
fallbackClose));
|
|
742
|
+
}
|