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,356 @@
|
|
|
1
|
+
// BlockStyle → React Native styles.
|
|
2
|
+
//
|
|
3
|
+
// The dashboard renderer maps a BlockStyle onto CSS; this maps the same style
|
|
4
|
+
// onto RN's subset. Where RN has no equivalent the property is DROPPED rather
|
|
5
|
+
// than approximated badly, and the omissions are documented on BlockStyle so
|
|
6
|
+
// a designer can see what will not survive the trip:
|
|
7
|
+
//
|
|
8
|
+
// backdrop blur RN has no backdrop-filter. The translucent `fill` a glass
|
|
9
|
+
// card already carries reads as frosted on its own.
|
|
10
|
+
// filter no equivalent; ignored.
|
|
11
|
+
// textWrap no equivalent (RN breaks lines its own way); ignored.
|
|
12
|
+
// fillSize RN paints `fill` as a flat colour, so a tiled wash has
|
|
13
|
+
// no size to honour; ignored.
|
|
14
|
+
// clipPath no equivalent; the shape renders as its uncut box.
|
|
15
|
+
// % translate RN transforms are points, not percentages. A percentage
|
|
16
|
+
// translate is ignored (a px one is applied), so a badge
|
|
17
|
+
// centred by `left:50%` + `translate:"-50% 0"` sits a half
|
|
18
|
+
// width to the right of where the web shows it.
|
|
19
|
+
//
|
|
20
|
+
// Two CSS units need converting rather than copying: `letterSpacing` is em in
|
|
21
|
+
// the document and points in RN, and `lineHeight` is a unitless multiplier in
|
|
22
|
+
// the document and points in RN. Both are resolved against the block's own
|
|
23
|
+
// font size, falling back to the inherited size the caller passes in.
|
|
24
|
+
import { flatColor, gradientFallbackColor, isGradient, parseGradient, resolveColor, withAlpha, } from "./color.js";
|
|
25
|
+
const JUSTIFY = {
|
|
26
|
+
start: "flex-start",
|
|
27
|
+
center: "center",
|
|
28
|
+
end: "flex-end",
|
|
29
|
+
between: "space-between",
|
|
30
|
+
};
|
|
31
|
+
const ITEMS = {
|
|
32
|
+
start: "flex-start",
|
|
33
|
+
center: "center",
|
|
34
|
+
end: "flex-end",
|
|
35
|
+
baseline: "baseline",
|
|
36
|
+
stretch: "stretch",
|
|
37
|
+
};
|
|
38
|
+
/** The browser's default, which is what the dashboard preview inherits — a
|
|
39
|
+
* block that sets no size of its own must come out the same size in both. */
|
|
40
|
+
export const DEFAULT_FONT_SIZE = 16;
|
|
41
|
+
/** The inheritable half of a block's style, layered over what it inherited. */
|
|
42
|
+
export function inherit(base, s, ctx) {
|
|
43
|
+
if (!s)
|
|
44
|
+
return base;
|
|
45
|
+
const next = { ...base };
|
|
46
|
+
const color = resolveColor(s.textColor, ctx.doc);
|
|
47
|
+
if (color !== undefined)
|
|
48
|
+
next.color = color;
|
|
49
|
+
if (s.fontSize !== undefined)
|
|
50
|
+
next.fontSize = s.fontSize;
|
|
51
|
+
if (s.fontFamily !== undefined)
|
|
52
|
+
next.fontFamilyName = s.fontFamily;
|
|
53
|
+
if (s.fontWeight !== undefined) {
|
|
54
|
+
next.fontWeight = String(s.fontWeight);
|
|
55
|
+
}
|
|
56
|
+
if (s.fontStyle !== undefined)
|
|
57
|
+
next.fontStyle = s.fontStyle;
|
|
58
|
+
if (s.align !== undefined)
|
|
59
|
+
next.textAlign = s.align;
|
|
60
|
+
if (s.textTransform !== undefined)
|
|
61
|
+
next.textTransform = s.textTransform;
|
|
62
|
+
if (s.letterSpacing !== undefined)
|
|
63
|
+
next.letterSpacing = s.letterSpacing;
|
|
64
|
+
if (s.lineHeight !== undefined)
|
|
65
|
+
next.lineHeight = s.lineHeight;
|
|
66
|
+
return next;
|
|
67
|
+
}
|
|
68
|
+
/** Inherited typography as an RN style, with the em/multiplier units resolved
|
|
69
|
+
* against the size that actually applies here, and the family resolved
|
|
70
|
+
* against the weight in force. */
|
|
71
|
+
export function inheritedTextStyle(t, ctx) {
|
|
72
|
+
const out = { color: t.color, fontSize: t.fontSize };
|
|
73
|
+
const family = resolveFontFamily(t.fontFamilyName, ctx, t.fontWeight);
|
|
74
|
+
if (family !== undefined)
|
|
75
|
+
out.fontFamily = family;
|
|
76
|
+
if (t.fontWeight !== undefined)
|
|
77
|
+
out.fontWeight = t.fontWeight;
|
|
78
|
+
if (t.fontStyle !== undefined)
|
|
79
|
+
out.fontStyle = t.fontStyle;
|
|
80
|
+
if (t.textAlign !== undefined)
|
|
81
|
+
out.textAlign = t.textAlign;
|
|
82
|
+
if (t.textTransform !== undefined)
|
|
83
|
+
out.textTransform = t.textTransform;
|
|
84
|
+
if (t.letterSpacing !== undefined) {
|
|
85
|
+
out.letterSpacing = t.letterSpacing * t.fontSize;
|
|
86
|
+
}
|
|
87
|
+
if (t.lineHeight !== undefined)
|
|
88
|
+
out.lineHeight = t.lineHeight * t.fontSize;
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
/** Border shorthand ("1.5px solid @text/12") → width + color. */
|
|
92
|
+
function parseBorder(value, doc, onDiagnostic) {
|
|
93
|
+
const width = value.match(/(-?[\d.]+)px/);
|
|
94
|
+
// The color is whatever remains once the width and the style keyword go.
|
|
95
|
+
const color = value
|
|
96
|
+
.replace(/(-?[\d.]+)px/, "")
|
|
97
|
+
.replace(/\b(solid|dashed|dotted|none|hidden)\b/i, "")
|
|
98
|
+
.trim();
|
|
99
|
+
return {
|
|
100
|
+
width: width ? parseFloat(width[1]) : 1,
|
|
101
|
+
color: color ? flatColor(color, doc, onDiagnostic) : undefined,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function parseAspectRatio(value) {
|
|
105
|
+
if (typeof value === "number")
|
|
106
|
+
return value;
|
|
107
|
+
const ratio = value.match(/^\s*([\d.]+)\s*\/\s*([\d.]+)\s*$/);
|
|
108
|
+
if (ratio) {
|
|
109
|
+
const h = parseFloat(ratio[2]);
|
|
110
|
+
return h ? parseFloat(ratio[1]) / h : undefined;
|
|
111
|
+
}
|
|
112
|
+
const n = parseFloat(value);
|
|
113
|
+
return Number.isNaN(n) ? undefined : n;
|
|
114
|
+
}
|
|
115
|
+
/** A rough RN shadow for a CSS box-shadow. Only the presence and color of a
|
|
116
|
+
* shadow survive — RN cannot express layered glows, so one soft shadow is
|
|
117
|
+
* drawn rather than none (a card designed to lift should still lift). */
|
|
118
|
+
function parseShadow(value, doc, onDiagnostic) {
|
|
119
|
+
const lengths = [...value.matchAll(/(-?[\d.]+)px/g)].map((m) => parseFloat(m[1]));
|
|
120
|
+
const [offsetX = 0, offsetY = 0, blur = 0] = lengths;
|
|
121
|
+
const colorMatch = value.match(/(rgba?\([^)]*\)|#[0-9a-fA-F]{3,8}|@[\w/]+)/);
|
|
122
|
+
const color = colorMatch ? flatColor(colorMatch[1], doc, onDiagnostic) : undefined;
|
|
123
|
+
return {
|
|
124
|
+
shadowColor: color ?? "#000000",
|
|
125
|
+
shadowOffset: { width: offsetX, height: offsetY },
|
|
126
|
+
shadowOpacity: color ? 1 : 0.25,
|
|
127
|
+
shadowRadius: blur / 2,
|
|
128
|
+
// Android has no per-shadow control; elevation is the only lever.
|
|
129
|
+
elevation: Math.round(Math.max(0, blur) / 4),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
/** The layout/paint half of a BlockStyle — everything a <View> understands. */
|
|
133
|
+
export function toViewStyle(s, ctx) {
|
|
134
|
+
if (!s)
|
|
135
|
+
return {};
|
|
136
|
+
const { doc } = ctx;
|
|
137
|
+
const out = {};
|
|
138
|
+
if (s.fill !== undefined) {
|
|
139
|
+
// A gradient fill reaches RN as an unparseable string and paints nothing,
|
|
140
|
+
// so it degrades to its ground color here — UNLESS the renderer is about to
|
|
141
|
+
// paint the real layers over this box, in which case the ground color would
|
|
142
|
+
// sit under them as an opaque backing and flatten every translucent stop.
|
|
143
|
+
const fill = resolveColor(s.fill, doc);
|
|
144
|
+
if (fill && isGradient(fill)) {
|
|
145
|
+
out.backgroundColor =
|
|
146
|
+
ctx.paintsGradients && parseGradient(fill).length > 0
|
|
147
|
+
? "transparent"
|
|
148
|
+
: gradientFallbackColor(fill);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
out.backgroundColor = fill;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (s.opacity !== undefined)
|
|
155
|
+
out.opacity = s.opacity / 100;
|
|
156
|
+
if (s.borderColor !== undefined || s.borderWidth !== undefined) {
|
|
157
|
+
out.borderWidth = s.borderWidth ?? 1;
|
|
158
|
+
// A gradient here reached RN as a color string it cannot parse; it is
|
|
159
|
+
// collapsed to the color it stands for so the stroke still draws.
|
|
160
|
+
out.borderColor = flatColor(s.borderColor, doc, ctx.onDiagnostic) ?? doc.textColor;
|
|
161
|
+
}
|
|
162
|
+
for (const [prop, side] of [
|
|
163
|
+
["borderTop", "Top"],
|
|
164
|
+
["borderRight", "Right"],
|
|
165
|
+
["borderBottom", "Bottom"],
|
|
166
|
+
["borderLeft", "Left"],
|
|
167
|
+
]) {
|
|
168
|
+
const raw = s[prop];
|
|
169
|
+
if (raw === undefined)
|
|
170
|
+
continue;
|
|
171
|
+
const { width, color } = parseBorder(raw, doc, ctx.onDiagnostic);
|
|
172
|
+
out[`border${side}Width`] = width;
|
|
173
|
+
if (color)
|
|
174
|
+
out[`border${side}Color`] = color;
|
|
175
|
+
}
|
|
176
|
+
if (s.radius !== undefined)
|
|
177
|
+
out.borderRadius = s.radius;
|
|
178
|
+
if (s.padding !== undefined)
|
|
179
|
+
out.padding = s.padding;
|
|
180
|
+
if (s.paddingX !== undefined)
|
|
181
|
+
out.paddingHorizontal = s.paddingX;
|
|
182
|
+
if (s.paddingY !== undefined)
|
|
183
|
+
out.paddingVertical = s.paddingY;
|
|
184
|
+
if (s.paddingTop !== undefined)
|
|
185
|
+
out.paddingTop = s.paddingTop;
|
|
186
|
+
if (s.paddingRight !== undefined)
|
|
187
|
+
out.paddingRight = s.paddingRight;
|
|
188
|
+
if (s.paddingBottom !== undefined)
|
|
189
|
+
out.paddingBottom = s.paddingBottom;
|
|
190
|
+
if (s.paddingLeft !== undefined)
|
|
191
|
+
out.paddingLeft = s.paddingLeft;
|
|
192
|
+
if (s.margin !== undefined)
|
|
193
|
+
out.margin = s.margin;
|
|
194
|
+
if (s.marginTop !== undefined)
|
|
195
|
+
out.marginTop = s.marginTop;
|
|
196
|
+
if (s.marginRight !== undefined)
|
|
197
|
+
out.marginRight = s.marginRight;
|
|
198
|
+
if (s.marginBottom !== undefined)
|
|
199
|
+
out.marginBottom = s.marginBottom;
|
|
200
|
+
if (s.marginLeft !== undefined)
|
|
201
|
+
out.marginLeft = s.marginLeft;
|
|
202
|
+
if (s.gap !== undefined)
|
|
203
|
+
out.gap = s.gap;
|
|
204
|
+
if (s.height !== undefined)
|
|
205
|
+
out.height = s.height;
|
|
206
|
+
if (s.minHeight !== undefined)
|
|
207
|
+
out.minHeight = s.minHeight;
|
|
208
|
+
if (s.width !== undefined)
|
|
209
|
+
out.width = s.width;
|
|
210
|
+
if (s.maxWidth !== undefined)
|
|
211
|
+
out.maxWidth = s.maxWidth;
|
|
212
|
+
if (s.aspectRatio !== undefined) {
|
|
213
|
+
const ratio = parseAspectRatio(s.aspectRatio);
|
|
214
|
+
if (ratio !== undefined)
|
|
215
|
+
out.aspectRatio = ratio;
|
|
216
|
+
}
|
|
217
|
+
if (s.flex !== undefined)
|
|
218
|
+
out.flexGrow = s.flex;
|
|
219
|
+
if (s.shrink !== undefined)
|
|
220
|
+
out.flexShrink = s.shrink;
|
|
221
|
+
if (s.basis !== undefined)
|
|
222
|
+
out.flexBasis = s.basis;
|
|
223
|
+
if (s.wrap)
|
|
224
|
+
out.flexWrap = "wrap";
|
|
225
|
+
if (s.justify !== undefined)
|
|
226
|
+
out.justifyContent = JUSTIFY[s.justify];
|
|
227
|
+
if (s.items !== undefined)
|
|
228
|
+
out.alignItems = ITEMS[s.items];
|
|
229
|
+
if (s.selfAlign !== undefined)
|
|
230
|
+
out.alignSelf = ITEMS[s.selfAlign];
|
|
231
|
+
if (s.shadow !== undefined)
|
|
232
|
+
Object.assign(out, parseShadow(s.shadow, doc, ctx.onDiagnostic));
|
|
233
|
+
const transform = [];
|
|
234
|
+
if (s.rotate !== undefined)
|
|
235
|
+
transform.push({ rotate: `${s.rotate}deg` });
|
|
236
|
+
if (s.translate !== undefined) {
|
|
237
|
+
// Percentages have no RN equivalent and are dropped rather than guessed
|
|
238
|
+
// at; a px pair translates directly.
|
|
239
|
+
const [x, y] = s.translate.trim().split(/\s+/);
|
|
240
|
+
const px = (v) => {
|
|
241
|
+
if (!v)
|
|
242
|
+
return 0;
|
|
243
|
+
const n = Number(v.endsWith("px") ? v.slice(0, -2) : v);
|
|
244
|
+
return Number.isFinite(n) ? n : 0;
|
|
245
|
+
};
|
|
246
|
+
// A zero component is a no-op; leaving it out keeps the list to the
|
|
247
|
+
// moves the design actually makes.
|
|
248
|
+
if (x !== undefined && !x.endsWith("%") && px(x) !== 0) {
|
|
249
|
+
transform.push({ translateX: px(x) });
|
|
250
|
+
}
|
|
251
|
+
if (y !== undefined && !y.endsWith("%") && px(y) !== 0) {
|
|
252
|
+
transform.push({ translateY: px(y) });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (transform.length)
|
|
256
|
+
out.transform = transform;
|
|
257
|
+
if (s.zIndex !== undefined)
|
|
258
|
+
out.zIndex = s.zIndex;
|
|
259
|
+
if (s.overflow !== undefined)
|
|
260
|
+
out.overflow = s.overflow;
|
|
261
|
+
// Stack placement: `inset` fills the parent, individual offsets pin edges.
|
|
262
|
+
// Both imply absolute positioning, which is inert outside a stack parent.
|
|
263
|
+
if (s.inset) {
|
|
264
|
+
out.position = "absolute";
|
|
265
|
+
out.top = 0;
|
|
266
|
+
out.right = 0;
|
|
267
|
+
out.bottom = 0;
|
|
268
|
+
out.left = 0;
|
|
269
|
+
}
|
|
270
|
+
else if (s.top !== undefined ||
|
|
271
|
+
s.right !== undefined ||
|
|
272
|
+
s.bottom !== undefined ||
|
|
273
|
+
s.left !== undefined) {
|
|
274
|
+
out.position = "absolute";
|
|
275
|
+
if (s.top !== undefined)
|
|
276
|
+
out.top = s.top;
|
|
277
|
+
if (s.right !== undefined)
|
|
278
|
+
out.right = s.right;
|
|
279
|
+
if (s.bottom !== undefined)
|
|
280
|
+
out.bottom = s.bottom;
|
|
281
|
+
if (s.left !== undefined)
|
|
282
|
+
out.left = s.left;
|
|
283
|
+
}
|
|
284
|
+
return out;
|
|
285
|
+
}
|
|
286
|
+
/** The typography half — everything a <Text> understands. */
|
|
287
|
+
export function toTextStyle(s, ctx) {
|
|
288
|
+
if (!s)
|
|
289
|
+
return {};
|
|
290
|
+
const { doc } = ctx;
|
|
291
|
+
const out = {};
|
|
292
|
+
const fontSize = s.fontSize ?? ctx.fontSize;
|
|
293
|
+
if (s.textColor !== undefined)
|
|
294
|
+
out.color = flatColor(s.textColor, doc, ctx.onDiagnostic);
|
|
295
|
+
if (s.fontSize !== undefined)
|
|
296
|
+
out.fontSize = s.fontSize;
|
|
297
|
+
if (s.fontWeight !== undefined)
|
|
298
|
+
out.fontWeight = String(s.fontWeight);
|
|
299
|
+
if (s.fontFamily !== undefined) {
|
|
300
|
+
const family = resolveFontFamily(s.fontFamily, ctx);
|
|
301
|
+
if (family)
|
|
302
|
+
out.fontFamily = family;
|
|
303
|
+
}
|
|
304
|
+
if (s.fontStyle !== undefined)
|
|
305
|
+
out.fontStyle = s.fontStyle;
|
|
306
|
+
if (s.align !== undefined)
|
|
307
|
+
out.textAlign = s.align;
|
|
308
|
+
// em → points, against this block's own size.
|
|
309
|
+
if (s.letterSpacing !== undefined)
|
|
310
|
+
out.letterSpacing = s.letterSpacing * fontSize;
|
|
311
|
+
// Unitless multiplier → points.
|
|
312
|
+
if (s.lineHeight !== undefined)
|
|
313
|
+
out.lineHeight = s.lineHeight * fontSize;
|
|
314
|
+
if (s.textTransform !== undefined)
|
|
315
|
+
out.textTransform = s.textTransform;
|
|
316
|
+
if (s.decoration !== undefined && s.decoration !== "none")
|
|
317
|
+
out.textDecorationLine = s.decoration;
|
|
318
|
+
return out;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* A design font renders only when the host app has registered it — RN cannot
|
|
322
|
+
* load a family by name at runtime. `fontFamilyMap` lets the app point a
|
|
323
|
+
* design's family at whatever it bundled (and drop the ones it did not), so an
|
|
324
|
+
* unregistered face falls back to the system font instead of, on Android,
|
|
325
|
+
* failing to render.
|
|
326
|
+
*
|
|
327
|
+
* The weight matters because RN picks a face by family NAME, not by
|
|
328
|
+
* `fontWeight`: asking for weight 800 of a family registered only at 400 gets
|
|
329
|
+
* a synthesised bold at best. Given a weight table, the nearest registered
|
|
330
|
+
* weight wins.
|
|
331
|
+
*/
|
|
332
|
+
export function resolveFontFamily(family, ctx, weight) {
|
|
333
|
+
if (!family)
|
|
334
|
+
return undefined;
|
|
335
|
+
const mapping = ctx.fontFamilyMap?.[family];
|
|
336
|
+
if (mapping === undefined)
|
|
337
|
+
return undefined;
|
|
338
|
+
if (typeof mapping === "string")
|
|
339
|
+
return mapping;
|
|
340
|
+
const want = Number(weight ?? 400);
|
|
341
|
+
const weights = Object.keys(mapping)
|
|
342
|
+
.map(Number)
|
|
343
|
+
.filter((w) => Number.isFinite(w))
|
|
344
|
+
.sort((a, b) => a - b);
|
|
345
|
+
if (weights.length === 0)
|
|
346
|
+
return undefined;
|
|
347
|
+
const target = Number.isFinite(want) ? want : 400;
|
|
348
|
+
const nearest = weights.reduce((best, w) => Math.abs(w - target) < Math.abs(best - target) ? w : best);
|
|
349
|
+
return mapping[nearest];
|
|
350
|
+
}
|
|
351
|
+
/** A `line` block is drawn from the current color at low alpha — the web
|
|
352
|
+
* renderer uses `currentColor` with opacity, which RN has no notion of, so
|
|
353
|
+
* the caller passes the color that is in force at that point in the tree. */
|
|
354
|
+
export function hairlineColor(currentColor) {
|
|
355
|
+
return withAlpha(currentColor, 0.16);
|
|
356
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// Tag variables — the vocabulary a designed paywall's copy uses to talk about
|
|
2
|
+
// the packages attached to it.
|
|
3
|
+
//
|
|
4
|
+
// A designed paywall must never hardcode a price: the merchant's price differs
|
|
5
|
+
// from the design's sample, and the store charges a different amount (and
|
|
6
|
+
// currency) in every territory. So any text or button in a block tree can
|
|
7
|
+
// write `{price}` and have the live package fill it in.
|
|
8
|
+
//
|
|
9
|
+
// Mirrors revnix-app's src/lib/paywall-blocks/tags.ts. Tags that cannot be
|
|
10
|
+
// answered from real package data are left in place, visible, rather than
|
|
11
|
+
// approximated — a wrong price must never reach a customer silently.
|
|
12
|
+
/** Renewal cycles, in months. Lifetime and one-time products have no cycle. */
|
|
13
|
+
const MONTHS = {
|
|
14
|
+
weekly: 1 / 4.345,
|
|
15
|
+
monthly: 1,
|
|
16
|
+
two_months: 2,
|
|
17
|
+
three_months: 3,
|
|
18
|
+
six_months: 6,
|
|
19
|
+
annual: 12,
|
|
20
|
+
lifetime: undefined,
|
|
21
|
+
non_subscription: undefined,
|
|
22
|
+
consumable: undefined,
|
|
23
|
+
};
|
|
24
|
+
const PERIOD_WORD = {
|
|
25
|
+
weekly: "week",
|
|
26
|
+
monthly: "month",
|
|
27
|
+
two_months: "2 months",
|
|
28
|
+
three_months: "3 months",
|
|
29
|
+
six_months: "6 months",
|
|
30
|
+
annual: "year",
|
|
31
|
+
lifetime: "lifetime",
|
|
32
|
+
};
|
|
33
|
+
const PERIOD_SHORT = {
|
|
34
|
+
weekly: "wk",
|
|
35
|
+
monthly: "mo",
|
|
36
|
+
two_months: "2mo",
|
|
37
|
+
three_months: "3mo",
|
|
38
|
+
six_months: "6mo",
|
|
39
|
+
annual: "yr",
|
|
40
|
+
lifetime: "once",
|
|
41
|
+
};
|
|
42
|
+
/** Minor units per major unit. Not every currency is a hundredth: JPY and KRW
|
|
43
|
+
* have no minor unit at all, so dividing by 100 would understate a price by
|
|
44
|
+
* 100×. Intl knows each currency's exponent — ask it rather than assume.
|
|
45
|
+
*
|
|
46
|
+
* Exported because a host app usually holds the store's price in MAJOR units
|
|
47
|
+
* and has to convert before filling in `RevnixPaywallPackage.amountMinor`;
|
|
48
|
+
* hardcoding 100 there would reintroduce the same bug one layer up. */
|
|
49
|
+
export function minorUnitsForCurrency(currency) {
|
|
50
|
+
try {
|
|
51
|
+
const digits = new Intl.NumberFormat(undefined, {
|
|
52
|
+
style: "currency",
|
|
53
|
+
currency,
|
|
54
|
+
}).resolvedOptions().maximumFractionDigits;
|
|
55
|
+
return 10 ** (digits ?? 2);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return 100;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function money(amountMinor, currency) {
|
|
62
|
+
const per = minorUnitsForCurrency(currency);
|
|
63
|
+
const major = amountMinor / per;
|
|
64
|
+
try {
|
|
65
|
+
return new Intl.NumberFormat(undefined, {
|
|
66
|
+
style: "currency",
|
|
67
|
+
currency,
|
|
68
|
+
// A whole amount reads better without ".00"; a fractional one must keep
|
|
69
|
+
// its digits.
|
|
70
|
+
maximumFractionDigits: amountMinor % per === 0 ? 0 : undefined,
|
|
71
|
+
}).format(major);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Hermes without full ICU, or an unknown currency code.
|
|
75
|
+
return `${major.toFixed(per === 1 ? 0 : 2)} ${currency}`;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function perMonthMinor(pkg) {
|
|
79
|
+
const months = pkg.period ? MONTHS[pkg.period] : undefined;
|
|
80
|
+
if (!months || pkg.amountMinor === undefined)
|
|
81
|
+
return undefined;
|
|
82
|
+
return pkg.amountMinor / months;
|
|
83
|
+
}
|
|
84
|
+
/** Fills a copy template from one package. `all` is the rest of the offering,
|
|
85
|
+
* which {save_percent} needs to have something to compare against. */
|
|
86
|
+
export function resolveTags(text, pkg, all = []) {
|
|
87
|
+
if (!pkg || !text.includes("{"))
|
|
88
|
+
return text;
|
|
89
|
+
return text.replace(/\{(\w+)\}/g, (raw, name) => {
|
|
90
|
+
switch (name) {
|
|
91
|
+
case "title":
|
|
92
|
+
return pkg.title;
|
|
93
|
+
case "price":
|
|
94
|
+
return pkg.priceLabel;
|
|
95
|
+
case "period":
|
|
96
|
+
return pkg.period ? (PERIOD_WORD[pkg.period] ?? raw) : raw;
|
|
97
|
+
case "period_short":
|
|
98
|
+
return pkg.period ? (PERIOD_SHORT[pkg.period] ?? raw) : raw;
|
|
99
|
+
case "price_per_month": {
|
|
100
|
+
const per = perMonthMinor(pkg);
|
|
101
|
+
return per !== undefined && pkg.currency
|
|
102
|
+
? money(Math.round(per), pkg.currency)
|
|
103
|
+
: raw;
|
|
104
|
+
}
|
|
105
|
+
case "save_percent": {
|
|
106
|
+
const mine = perMonthMinor(pkg);
|
|
107
|
+
const dearest = Math.max(...all.map((p) => perMonthMinor(p) ?? 0), 0);
|
|
108
|
+
if (!mine || !dearest || dearest <= mine)
|
|
109
|
+
return raw;
|
|
110
|
+
return `${Math.round((1 - mine / dearest) * 100)}%`;
|
|
111
|
+
}
|
|
112
|
+
default:
|
|
113
|
+
// Unknown tag: leave it visible rather than guess.
|
|
114
|
+
return raw;
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// The paywall block model — a portable description of a designed paywall.
|
|
2
|
+
//
|
|
3
|
+
// A block paywall is a TREE of styled elements: the screen owns a background
|
|
4
|
+
// and a list of blocks; every block carries its own style. The dashboard's
|
|
5
|
+
// builder authors this tree and publishes it on the paywall config as
|
|
6
|
+
// `blocks`, where it takes precedence over the classic `template` layouts.
|
|
7
|
+
//
|
|
8
|
+
// This file mirrors revnix-app's src/lib/paywall-blocks/types.ts one-for-one.
|
|
9
|
+
// The two must stay in lockstep: the dashboard preview and this SDK renderer
|
|
10
|
+
// are two interpreters of the SAME document, and a field only one side knows
|
|
11
|
+
// is a design that ships looking different from the design that was approved.
|
|
12
|
+
/** The device screen `canvas` designs are authored against. */
|
|
13
|
+
export const DESIGN_CANVAS = { width: 393, height: 852 };
|
|
14
|
+
/** Does this tree author a dismiss affordance that is CERTAIN to render?
|
|
15
|
+
*
|
|
16
|
+
* The renderer draws its own close button only when this is false, so a
|
|
17
|
+
* design written before close existed becomes dismissible without being
|
|
18
|
+
* edited, and a design that DOES author a close chip never shows two. The
|
|
19
|
+
* same predicate is implemented in every Revnix SDK — keep them identical.
|
|
20
|
+
*
|
|
21
|
+
* Conditional containers are deliberately not searched: a `repeat` card
|
|
22
|
+
* renders once per package (none, when the offering is empty) and a
|
|
23
|
+
* `packageIndex` card is hidden when the offering does not reach that index,
|
|
24
|
+
* so a close authored inside one MIGHT not appear. Counting it would
|
|
25
|
+
* suppress the fallback and leave the customer with no way out — the exact
|
|
26
|
+
* bug this feature exists to fix. Two close buttons is merely ugly, so the
|
|
27
|
+
* tie breaks toward always having one. */
|
|
28
|
+
export function hasCloseAction(blocks) {
|
|
29
|
+
return blocks.some((b) =>
|
|
30
|
+
// A close the design only draws in one selection state (`visibility`)
|
|
31
|
+
// must not suppress the fallback chip — the other state would have none.
|
|
32
|
+
b.visibility !== undefined
|
|
33
|
+
? false
|
|
34
|
+
: b.type === "card"
|
|
35
|
+
? b.repeat === undefined &&
|
|
36
|
+
b.packageIndex === undefined &&
|
|
37
|
+
hasCloseAction(b.children)
|
|
38
|
+
: (b.type === "text" || b.type === "image" || b.type === "button") &&
|
|
39
|
+
b.action === "close");
|
|
40
|
+
}
|