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,149 @@
|
|
|
1
|
+
{
|
|
2
|
+
"contract": "cto-review/2026-09-03-paywall-render-contract-v2.md",
|
|
3
|
+
"doc": {
|
|
4
|
+
"version": 1,
|
|
5
|
+
"layout": "flow",
|
|
6
|
+
"background": "#000000",
|
|
7
|
+
"textColor": "#ffffff",
|
|
8
|
+
"accent": "#00ff00",
|
|
9
|
+
"accentInk": "#000000",
|
|
10
|
+
"blocks": [
|
|
11
|
+
{
|
|
12
|
+
"id": "plans",
|
|
13
|
+
"type": "card",
|
|
14
|
+
"layout": "column",
|
|
15
|
+
"children": [
|
|
16
|
+
{
|
|
17
|
+
"id": "c0",
|
|
18
|
+
"type": "card",
|
|
19
|
+
"layout": "row",
|
|
20
|
+
"packageIndex": 0,
|
|
21
|
+
"style": { "fill": "#111111", "borderColor": "#333333", "borderWidth": 1, "radius": 16 },
|
|
22
|
+
"selectedStyle": { "fill": "#222222", "borderColor": "@accent" },
|
|
23
|
+
"children": [
|
|
24
|
+
{
|
|
25
|
+
"id": "c0-ring",
|
|
26
|
+
"type": "card",
|
|
27
|
+
"layout": "column",
|
|
28
|
+
"style": { "width": 20, "height": 20, "radius": 9999, "borderWidth": 2, "borderColor": "#555555" },
|
|
29
|
+
"selectedStyle": { "borderColor": "@accent" },
|
|
30
|
+
"children": [
|
|
31
|
+
{
|
|
32
|
+
"id": "c0-dot",
|
|
33
|
+
"type": "card",
|
|
34
|
+
"layout": "column",
|
|
35
|
+
"visibility": "selected",
|
|
36
|
+
"style": { "width": 10, "height": 10, "radius": 9999, "fill": "@accent" },
|
|
37
|
+
"children": []
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
},
|
|
41
|
+
{ "id": "c0-title", "type": "text", "text": "{title} — {price}/{period_short}" },
|
|
42
|
+
{ "id": "c0-on", "type": "text", "text": "Selected", "visibility": "selected" },
|
|
43
|
+
{ "id": "c0-off", "type": "text", "text": "Tap to select", "visibility": "unselected" }
|
|
44
|
+
]
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"id": "c1",
|
|
48
|
+
"type": "card",
|
|
49
|
+
"layout": "row",
|
|
50
|
+
"packageIndex": 1,
|
|
51
|
+
"style": { "fill": "#111111", "borderColor": "#333333", "borderWidth": 1, "radius": 16 },
|
|
52
|
+
"selectedStyle": { "fill": "#222222", "borderColor": "@accent" },
|
|
53
|
+
"children": [
|
|
54
|
+
{
|
|
55
|
+
"id": "c1-ring",
|
|
56
|
+
"type": "card",
|
|
57
|
+
"layout": "column",
|
|
58
|
+
"style": { "width": 20, "height": 20, "radius": 9999, "borderWidth": 2, "borderColor": "#555555" },
|
|
59
|
+
"selectedStyle": { "borderColor": "@accent" },
|
|
60
|
+
"children": [
|
|
61
|
+
{
|
|
62
|
+
"id": "c1-dot",
|
|
63
|
+
"type": "card",
|
|
64
|
+
"layout": "column",
|
|
65
|
+
"visibility": "selected",
|
|
66
|
+
"style": { "width": 10, "height": 10, "radius": 9999, "fill": "@accent" },
|
|
67
|
+
"children": []
|
|
68
|
+
}
|
|
69
|
+
]
|
|
70
|
+
},
|
|
71
|
+
{ "id": "c1-title", "type": "text", "text": "{title} — {price}/{period_short}" },
|
|
72
|
+
{ "id": "c1-on", "type": "text", "text": "Selected", "visibility": "selected" },
|
|
73
|
+
{ "id": "c1-off", "type": "text", "text": "Tap to select", "visibility": "unselected" }
|
|
74
|
+
]
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
},
|
|
78
|
+
{ "id": "footer", "type": "text", "text": "7 days free, then {price}/{period_short}. Cancel anytime." },
|
|
79
|
+
{ "id": "root-hidden", "type": "text", "text": "always drawn", "visibility": "selected" },
|
|
80
|
+
{ "id": "cta", "type": "button", "label": "Continue" }
|
|
81
|
+
]
|
|
82
|
+
},
|
|
83
|
+
"packages": [
|
|
84
|
+
{ "packageId": "monthly", "title": "Monthly", "priceLabel": "$9.99", "period": "monthly", "amountMinor": 999, "currency": "USD" },
|
|
85
|
+
{ "packageId": "yearly", "title": "Yearly", "priceLabel": "$59.99", "period": "annual", "amountMinor": 5999, "currency": "USD" }
|
|
86
|
+
],
|
|
87
|
+
"cases": [
|
|
88
|
+
{
|
|
89
|
+
"name": "host selects yearly",
|
|
90
|
+
"selected": "yearly",
|
|
91
|
+
"highlight": null,
|
|
92
|
+
"styles": {
|
|
93
|
+
"c0": { "fill": "#111111", "borderColor": "#333333", "borderWidth": 1 },
|
|
94
|
+
"c1": { "fill": "#222222", "borderColor": "@accent", "borderWidth": 1 },
|
|
95
|
+
"c0-ring": { "borderColor": "#555555" },
|
|
96
|
+
"c1-ring": { "borderColor": "@accent" }
|
|
97
|
+
},
|
|
98
|
+
"visible": ["c1-dot", "c1-on", "c0-off", "root-hidden", "footer", "cta"],
|
|
99
|
+
"hidden": ["c0-dot", "c0-on", "c1-off"],
|
|
100
|
+
"texts": {
|
|
101
|
+
"c0-title": "Monthly — $9.99/mo",
|
|
102
|
+
"c1-title": "Yearly — $59.99/yr",
|
|
103
|
+
"footer": "7 days free, then $59.99/yr. Cancel anytime."
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
"name": "host selects monthly",
|
|
108
|
+
"selected": "monthly",
|
|
109
|
+
"highlight": null,
|
|
110
|
+
"styles": {
|
|
111
|
+
"c0": { "fill": "#222222", "borderColor": "@accent", "borderWidth": 1 },
|
|
112
|
+
"c1": { "fill": "#111111", "borderColor": "#333333", "borderWidth": 1 },
|
|
113
|
+
"c0-ring": { "borderColor": "@accent" },
|
|
114
|
+
"c1-ring": { "borderColor": "#555555" }
|
|
115
|
+
},
|
|
116
|
+
"visible": ["c0-dot", "c0-on", "c1-off", "root-hidden", "footer", "cta"],
|
|
117
|
+
"hidden": ["c1-dot", "c1-on", "c0-off"],
|
|
118
|
+
"texts": {
|
|
119
|
+
"c0-title": "Monthly — $9.99/mo",
|
|
120
|
+
"c1-title": "Yearly — $59.99/yr",
|
|
121
|
+
"footer": "7 days free, then $9.99/mo. Cancel anytime."
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"name": "no host selection, highlight yearly",
|
|
126
|
+
"selected": null,
|
|
127
|
+
"highlight": "yearly",
|
|
128
|
+
"styles": {
|
|
129
|
+
"c0": { "fill": "#111111", "borderColor": "#333333", "borderWidth": 1 },
|
|
130
|
+
"c1": { "fill": "#222222", "borderColor": "@accent", "borderWidth": 1 }
|
|
131
|
+
},
|
|
132
|
+
"visible": ["c1-dot", "c1-on", "c0-off", "root-hidden"],
|
|
133
|
+
"hidden": ["c0-dot", "c0-on", "c1-off"],
|
|
134
|
+
"texts": { "footer": "7 days free, then $59.99/yr. Cancel anytime." }
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
"name": "no selection, no highlight: first package",
|
|
138
|
+
"selected": null,
|
|
139
|
+
"highlight": null,
|
|
140
|
+
"styles": {
|
|
141
|
+
"c0": { "fill": "#222222", "borderColor": "@accent", "borderWidth": 1 },
|
|
142
|
+
"c1": { "fill": "#111111", "borderColor": "#333333", "borderWidth": 1 }
|
|
143
|
+
},
|
|
144
|
+
"visible": ["c0-dot", "c0-on", "c1-off", "root-hidden"],
|
|
145
|
+
"hidden": ["c1-dot", "c1-on", "c0-off"],
|
|
146
|
+
"texts": { "footer": "7 days free, then $9.99/mo. Cancel anytime." }
|
|
147
|
+
}
|
|
148
|
+
]
|
|
149
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// REV-262: the selection half of the designed-paywall render contract v2
|
|
2
|
+
// (revnix/cto-review/2026-09-03-paywall-render-contract-v2.md), kept pure so
|
|
3
|
+
// the wire fixture can be walked without React Native.
|
|
4
|
+
//
|
|
5
|
+
// Mirrors revnix-app's src/lib/paywall-blocks/selection.ts: the dashboard
|
|
6
|
+
// preview and this renderer must agree on which package is "selected", what
|
|
7
|
+
// a block looks like in that state and whether it is drawn at all.
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
BlockStyle,
|
|
11
|
+
CardBlock,
|
|
12
|
+
PaywallBlock,
|
|
13
|
+
PaywallBlockDoc,
|
|
14
|
+
} from "./types.js";
|
|
15
|
+
import { DESIGN_CANVAS } from "./types.js";
|
|
16
|
+
import { resolveTags } from "./tags.js";
|
|
17
|
+
import type { RevnixPaywallPackage } from "../ui/RevnixPaywall.js";
|
|
18
|
+
|
|
19
|
+
/** Where a block sits: inside the selected plan card, inside another plan
|
|
20
|
+
* card, or outside any plan card (undefined). */
|
|
21
|
+
export type SelectionContext = "selected" | "unselected" | undefined;
|
|
22
|
+
|
|
23
|
+
/** The package a designed paywall is showing as chosen: the host's choice,
|
|
24
|
+
* then the renderer's own after a tap, then the design's highlight, then the
|
|
25
|
+
* first package — each only when the offering actually carries it. */
|
|
26
|
+
export function selectedPackageId(
|
|
27
|
+
packages: ReadonlyArray<{ packageId: string }>,
|
|
28
|
+
...candidates: Array<string | undefined>
|
|
29
|
+
): string | undefined {
|
|
30
|
+
for (const id of candidates) {
|
|
31
|
+
if (id !== undefined && packages.some((p) => p.packageId === id)) return id;
|
|
32
|
+
}
|
|
33
|
+
return packages[0]?.packageId;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The context a package-bearing card opens for itself and its subtree. */
|
|
37
|
+
export function packageContext(
|
|
38
|
+
pkg: { packageId: string } | undefined,
|
|
39
|
+
selectedId: string | undefined
|
|
40
|
+
): SelectionContext {
|
|
41
|
+
if (pkg === undefined) return undefined;
|
|
42
|
+
return pkg.packageId === selectedId ? "selected" : "unselected";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** `selectedStyle` merged over `style` while selected; `style` otherwise. */
|
|
46
|
+
export function effectiveStyle(
|
|
47
|
+
block: PaywallBlock,
|
|
48
|
+
context: SelectionContext
|
|
49
|
+
): BlockStyle | undefined {
|
|
50
|
+
if (context === "selected" && block.selectedStyle) {
|
|
51
|
+
return { ...block.style, ...block.selectedStyle };
|
|
52
|
+
}
|
|
53
|
+
return block.style;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Whether a block is drawn. Outside any plan card every block is drawn,
|
|
57
|
+
* whatever `visibility` says — a root block can never disappear. */
|
|
58
|
+
export function isVisible(
|
|
59
|
+
block: PaywallBlock,
|
|
60
|
+
context: SelectionContext
|
|
61
|
+
): boolean {
|
|
62
|
+
if (block.visibility === undefined || context === undefined) return true;
|
|
63
|
+
return (block.visibility === "selected") === (context === "selected");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Canvas designs are authored at 393×852 and scaled by width, capped so a
|
|
67
|
+
* tablet or a landscape phone shows a centred phone-sized design instead of
|
|
68
|
+
* a blown-up one. */
|
|
69
|
+
export const CANVAS_MAX_WIDTH = 480;
|
|
70
|
+
|
|
71
|
+
export interface CanvasMetrics {
|
|
72
|
+
scale: number;
|
|
73
|
+
/** Horizontal offset of the scaled design inside the viewport. */
|
|
74
|
+
left: number;
|
|
75
|
+
/** Height of the laid-out design in DESIGN units. */
|
|
76
|
+
designHeight: number;
|
|
77
|
+
/** Height of the scaled design in viewport points. */
|
|
78
|
+
scaledHeight: number;
|
|
79
|
+
/** Whether the design is taller than the viewport and must scroll. */
|
|
80
|
+
overflows: boolean;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function canvasMetrics(
|
|
84
|
+
viewportWidth: number,
|
|
85
|
+
viewportHeight: number | null
|
|
86
|
+
): CanvasMetrics {
|
|
87
|
+
const scale = Math.min(viewportWidth, CANVAS_MAX_WIDTH) / DESIGN_CANVAS.width;
|
|
88
|
+
const left = (viewportWidth - DESIGN_CANVAS.width * scale) / 2;
|
|
89
|
+
const designHeight =
|
|
90
|
+
viewportHeight === null
|
|
91
|
+
? DESIGN_CANVAS.height
|
|
92
|
+
: Math.max(DESIGN_CANVAS.height, viewportHeight / scale);
|
|
93
|
+
const scaledHeight = designHeight * scale;
|
|
94
|
+
const overflows =
|
|
95
|
+
viewportHeight !== null && DESIGN_CANVAS.height * scale > viewportHeight + 1;
|
|
96
|
+
return { scale, left, designHeight, scaledHeight, overflows };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface ResolvedBlock {
|
|
100
|
+
block: PaywallBlock;
|
|
101
|
+
context: SelectionContext;
|
|
102
|
+
visible: boolean;
|
|
103
|
+
style: BlockStyle | undefined;
|
|
104
|
+
/** Resolved copy for text and button blocks. */
|
|
105
|
+
text?: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Walk a document exactly the way the renderer does — pinned cards judge
|
|
109
|
+
* their own context, repeat cards yield one entry per package (keyed
|
|
110
|
+
* `id@packageId`), hidden subtrees are not entered — and report what every
|
|
111
|
+
* block resolves to. The wire fixture test reads this. */
|
|
112
|
+
export function resolveBlocks(
|
|
113
|
+
doc: PaywallBlockDoc,
|
|
114
|
+
packages: RevnixPaywallPackage[],
|
|
115
|
+
selectedId: string | undefined
|
|
116
|
+
): Map<string, ResolvedBlock> {
|
|
117
|
+
const out = new Map<string, ResolvedBlock>();
|
|
118
|
+
const root = packages.find((p) => p.packageId === selectedId) ?? packages[0];
|
|
119
|
+
const visit = (
|
|
120
|
+
block: PaywallBlock,
|
|
121
|
+
inherited: SelectionContext,
|
|
122
|
+
pkg: RevnixPaywallPackage | undefined,
|
|
123
|
+
key: string
|
|
124
|
+
) => {
|
|
125
|
+
if (block.type === "card" && block.repeat === "packages") {
|
|
126
|
+
const list = packages.length ? packages : [undefined];
|
|
127
|
+
for (const p of list) {
|
|
128
|
+
visitCard(block, packageContext(p, selectedId) ?? inherited, p ?? pkg, `${key}@${p?.packageId ?? "-"}`);
|
|
129
|
+
}
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (block.type === "card" && block.packageIndex !== undefined) {
|
|
133
|
+
const own = packages[block.packageIndex];
|
|
134
|
+
if (own === undefined) return; // dropped: the offering does not reach it
|
|
135
|
+
visitCard(block, packageContext(own, selectedId), own, key);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const context = inherited;
|
|
139
|
+
const visible = isVisible(block, context);
|
|
140
|
+
const entry: ResolvedBlock = { block, context, visible, style: effectiveStyle(block, context) };
|
|
141
|
+
if (block.type === "text") entry.text = resolveTags(block.text, pkg, packages);
|
|
142
|
+
if (block.type === "button") entry.text = resolveTags(block.label, pkg, packages);
|
|
143
|
+
out.set(key, entry);
|
|
144
|
+
if (visible && block.type === "card") {
|
|
145
|
+
for (const child of (block as CardBlock).children) visit(child, context, pkg, child.id);
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
const visitCard = (
|
|
149
|
+
block: CardBlock,
|
|
150
|
+
context: SelectionContext,
|
|
151
|
+
pkg: RevnixPaywallPackage | undefined,
|
|
152
|
+
key: string
|
|
153
|
+
) => {
|
|
154
|
+
const visible = isVisible(block, context);
|
|
155
|
+
out.set(key, { block, context, visible, style: effectiveStyle(block, context) });
|
|
156
|
+
if (!visible) return;
|
|
157
|
+
for (const child of block.children) visit(child, context, pkg, child.id);
|
|
158
|
+
};
|
|
159
|
+
for (const b of doc.blocks) visit(b, undefined, root, b.id);
|
|
160
|
+
return out;
|
|
161
|
+
}
|