react-native-revnix 1.6.0 → 1.8.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/README.md +16 -0
- package/android/build.gradle +3 -0
- package/android/consumer-rules.pro +1 -0
- package/android/src/main/kotlin/com/revnix/reactnative/RevnixStoreModule.kt +43 -0
- package/dist/blocks/locale.js +12 -0
- package/dist/blocks/selection.d.ts +7 -1
- package/dist/blocks/selection.js +9 -0
- package/dist/blocks/tags.d.ts +3 -0
- package/dist/blocks/tags.js +33 -0
- package/dist/blocks/types.d.ts +65 -8
- package/dist/blocks/types.js +21 -0
- package/dist/client.d.ts +4 -0
- package/dist/client.js +73 -5
- package/dist/esm/blocks/locale.js +12 -0
- package/dist/esm/blocks/selection.js +8 -0
- package/dist/esm/blocks/tags.js +31 -0
- package/dist/esm/blocks/types.js +19 -0
- package/dist/esm/client.js +73 -5
- package/dist/esm/store/index.js +7 -0
- package/dist/esm/ui/RevnixBlockPaywall.js +145 -44
- package/dist/esm/ui/RevnixPaywall.js +11 -2
- package/dist/esm/version.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/store/index.d.ts +8 -0
- package/dist/store/index.js +10 -1
- package/dist/types.d.ts +32 -0
- package/dist/ui/RevnixBlockPaywall.js +141 -40
- package/dist/ui/RevnixPaywall.js +11 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/ios/RevnixStore.m +4 -0
- package/ios/RevnixStore.swift +29 -0
- package/package.json +1 -1
- package/src/blocks/locale.ts +14 -0
- package/src/blocks/selection.ts +13 -0
- package/src/blocks/tags.ts +38 -0
- package/src/blocks/types.ts +86 -7
- package/src/client.ts +90 -8
- package/src/index.ts +1 -0
- package/src/store/index.ts +15 -0
- package/src/types.ts +30 -0
- package/src/ui/RevnixBlockPaywall.tsx +265 -42
- package/src/ui/RevnixPaywall.tsx +11 -2
- package/src/version.ts +1 -1
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
Pressable,
|
|
27
27
|
ScrollView,
|
|
28
28
|
StatusBar,
|
|
29
|
+
Switch,
|
|
29
30
|
Text,
|
|
30
31
|
View,
|
|
31
32
|
type ColorValue,
|
|
@@ -60,22 +61,27 @@ import {
|
|
|
60
61
|
type ResolvedBackgroundImage,
|
|
61
62
|
} from "../blocks/background.js";
|
|
62
63
|
import { detectLocale, linkLabels, localizeBlockDoc } from "../blocks/locale.js";
|
|
63
|
-
import { resolveTags } from "../blocks/tags.js";
|
|
64
|
+
import { resolveTags, timerText } from "../blocks/tags.js";
|
|
64
65
|
import {
|
|
65
66
|
canvasMetrics,
|
|
66
67
|
effectiveStyle,
|
|
67
68
|
isVisible,
|
|
68
69
|
packageContext,
|
|
69
70
|
selectedPackageId,
|
|
71
|
+
toggleSelection,
|
|
70
72
|
} from "../blocks/selection.js";
|
|
71
73
|
import type { SelectionContext } from "../blocks/selection.js";
|
|
72
74
|
import {
|
|
73
75
|
DESIGN_CANVAS,
|
|
74
76
|
hasCloseAction,
|
|
77
|
+
onboardingScreen,
|
|
78
|
+
stepScreen,
|
|
79
|
+
type BlockAction,
|
|
75
80
|
type BlockStyle,
|
|
76
81
|
type PaywallBlock,
|
|
77
82
|
type PaywallBlockDoc,
|
|
78
83
|
type ProductsBlock,
|
|
84
|
+
type TimerBlock,
|
|
79
85
|
} from "../blocks/types.js";
|
|
80
86
|
import type { RevnixPaywallPackage } from "./RevnixPaywall.js";
|
|
81
87
|
|
|
@@ -597,6 +603,7 @@ interface RenderContext {
|
|
|
597
603
|
onTerms?: () => void;
|
|
598
604
|
onPrivacy?: () => void;
|
|
599
605
|
onClose?: () => void;
|
|
606
|
+
navigate: (action: BlockAction | undefined) => (() => void) | undefined;
|
|
600
607
|
footer?: { termsUrl?: string; privacyUrl?: string };
|
|
601
608
|
heroImageUrl?: string;
|
|
602
609
|
gradient?: RevnixGradientComponent;
|
|
@@ -607,6 +614,7 @@ interface RenderContext {
|
|
|
607
614
|
style: StyleContext;
|
|
608
615
|
/** Host purchase in flight: purchase buttons disable and spin. */
|
|
609
616
|
loading: boolean;
|
|
617
|
+
shownAt: number;
|
|
610
618
|
}
|
|
611
619
|
|
|
612
620
|
/** Text blocks always render inside a View: a style may carry both typography
|
|
@@ -618,13 +626,14 @@ function BlockText({
|
|
|
618
626
|
ctx,
|
|
619
627
|
inherited,
|
|
620
628
|
onPress,
|
|
629
|
+
pressLabel,
|
|
621
630
|
}: {
|
|
622
631
|
text: string;
|
|
623
632
|
style: BlockStyle | undefined;
|
|
624
633
|
ctx: RenderContext;
|
|
625
634
|
inherited: InheritedText;
|
|
626
|
-
/** Set when the design marks this element as the close (REV-252). */
|
|
627
635
|
onPress?: () => void;
|
|
636
|
+
pressLabel?: string;
|
|
628
637
|
}) {
|
|
629
638
|
const styleCtx = { ...ctx.style, fontSize: inherited.fontSize };
|
|
630
639
|
// The block's own typography is folded into the inherited set first, so
|
|
@@ -657,7 +666,7 @@ function BlockText({
|
|
|
657
666
|
return (
|
|
658
667
|
<Pressable
|
|
659
668
|
accessibilityRole="button"
|
|
660
|
-
accessibilityLabel=
|
|
669
|
+
accessibilityLabel={pressLabel}
|
|
661
670
|
hitSlop={8}
|
|
662
671
|
onPress={onPress}
|
|
663
672
|
style={toViewStyle(style, styleCtx)}
|
|
@@ -669,16 +678,139 @@ function BlockText({
|
|
|
669
678
|
return <View style={toViewStyle(style, styleCtx)}>{body}</View>;
|
|
670
679
|
}
|
|
671
680
|
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
681
|
+
function Timer({
|
|
682
|
+
block,
|
|
683
|
+
style,
|
|
684
|
+
ctx,
|
|
685
|
+
inherited,
|
|
686
|
+
}: {
|
|
687
|
+
block: TimerBlock;
|
|
688
|
+
style: BlockStyle | undefined;
|
|
689
|
+
ctx: RenderContext;
|
|
690
|
+
inherited: InheritedText;
|
|
691
|
+
}) {
|
|
692
|
+
const [now, setNow] = React.useState(Date.now);
|
|
693
|
+
React.useEffect(() => {
|
|
694
|
+
const id = setInterval(() => setNow(Date.now()), 1000);
|
|
695
|
+
return () => clearInterval(id);
|
|
696
|
+
}, []);
|
|
697
|
+
const text = timerText(block, ctx.shownAt, now);
|
|
698
|
+
if (text === null) return null;
|
|
699
|
+
return <BlockText text={text} style={style} ctx={ctx} inherited={inherited} />;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function Carousel({
|
|
703
|
+
pages,
|
|
704
|
+
autoAdvance,
|
|
705
|
+
doc,
|
|
706
|
+
}: {
|
|
707
|
+
pages: React.ReactNode[];
|
|
708
|
+
autoAdvance?: number;
|
|
709
|
+
doc: PaywallBlockDoc;
|
|
710
|
+
}) {
|
|
711
|
+
const scroller = React.useRef<ScrollView>(null);
|
|
712
|
+
const [size, setSize] = React.useState({ width: 0, height: 0 });
|
|
713
|
+
const [page, setPage] = React.useState(0);
|
|
714
|
+
React.useEffect(() => {
|
|
715
|
+
if (!autoAdvance || size.width === 0 || pages.length < 2) return;
|
|
716
|
+
const id = setInterval(() => {
|
|
717
|
+
scroller.current?.scrollTo({
|
|
718
|
+
x: ((page + 1) % pages.length) * size.width,
|
|
719
|
+
animated: true,
|
|
720
|
+
});
|
|
721
|
+
}, autoAdvance * 1000);
|
|
722
|
+
return () => clearInterval(id);
|
|
723
|
+
}, [autoAdvance, size.width, page, pages.length]);
|
|
724
|
+
return (
|
|
725
|
+
<View
|
|
726
|
+
style={{ flex: 1 }}
|
|
727
|
+
onLayout={(e) =>
|
|
728
|
+
setSize({
|
|
729
|
+
width: e.nativeEvent.layout.width,
|
|
730
|
+
height: e.nativeEvent.layout.height,
|
|
731
|
+
})
|
|
732
|
+
}
|
|
733
|
+
>
|
|
734
|
+
<ScrollView
|
|
735
|
+
ref={scroller}
|
|
736
|
+
horizontal
|
|
737
|
+
pagingEnabled
|
|
738
|
+
showsHorizontalScrollIndicator={false}
|
|
739
|
+
scrollEventThrottle={16}
|
|
740
|
+
onScroll={(e) => {
|
|
741
|
+
if (size.width > 0) {
|
|
742
|
+
setPage(Math.round(e.nativeEvent.contentOffset.x / size.width));
|
|
743
|
+
}
|
|
744
|
+
}}
|
|
745
|
+
>
|
|
746
|
+
{pages.map((node, i) => (
|
|
747
|
+
<View key={i} style={size}>
|
|
748
|
+
{node}
|
|
749
|
+
</View>
|
|
750
|
+
))}
|
|
751
|
+
</ScrollView>
|
|
752
|
+
{pages.length > 1 ? (
|
|
753
|
+
<Dots count={pages.length} active={page} doc={doc} style={{ bottom: 8 }} />
|
|
754
|
+
) : null}
|
|
755
|
+
</View>
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function Dots({
|
|
760
|
+
count,
|
|
761
|
+
active,
|
|
762
|
+
doc,
|
|
763
|
+
style,
|
|
764
|
+
}: {
|
|
765
|
+
count: number;
|
|
766
|
+
active: number;
|
|
767
|
+
doc: PaywallBlockDoc;
|
|
768
|
+
style: ViewStyle;
|
|
769
|
+
}) {
|
|
770
|
+
return (
|
|
771
|
+
<View
|
|
772
|
+
pointerEvents="none"
|
|
773
|
+
style={[
|
|
774
|
+
{
|
|
775
|
+
position: "absolute",
|
|
776
|
+
left: 0,
|
|
777
|
+
right: 0,
|
|
778
|
+
flexDirection: "row",
|
|
779
|
+
justifyContent: "center",
|
|
780
|
+
gap: 6,
|
|
781
|
+
},
|
|
782
|
+
style,
|
|
783
|
+
]}
|
|
784
|
+
>
|
|
785
|
+
{Array.from({ length: count }, (_, i) => (
|
|
786
|
+
<View
|
|
787
|
+
key={i}
|
|
788
|
+
style={{
|
|
789
|
+
width: 6,
|
|
790
|
+
height: 6,
|
|
791
|
+
borderRadius: 3,
|
|
792
|
+
backgroundColor:
|
|
793
|
+
i === active ? doc.accent : withAlpha(doc.textColor, 0.3),
|
|
794
|
+
}}
|
|
795
|
+
/>
|
|
796
|
+
))}
|
|
797
|
+
</View>
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function actionPress(
|
|
802
|
+
block: { action?: BlockAction },
|
|
677
803
|
ctx: RenderContext
|
|
678
804
|
): (() => void) | undefined {
|
|
679
|
-
return block.action === "close"
|
|
805
|
+
return block.action === "close" ? ctx.onClose : ctx.navigate(block.action);
|
|
680
806
|
}
|
|
681
807
|
|
|
808
|
+
const ACTION_LABEL: Record<BlockAction, string> = {
|
|
809
|
+
close: "Close",
|
|
810
|
+
next: "Next",
|
|
811
|
+
back: "Back",
|
|
812
|
+
};
|
|
813
|
+
|
|
682
814
|
/** Typography that is NOT inherited (decoration) — everything else already
|
|
683
815
|
* arrives through the inherited set. */
|
|
684
816
|
function textOnly(s: BlockStyle | undefined, ctx: StyleContext) {
|
|
@@ -844,6 +976,11 @@ function Block({
|
|
|
844
976
|
selection?: SelectionContext;
|
|
845
977
|
}): React.ReactElement | null {
|
|
846
978
|
const { doc } = ctx;
|
|
979
|
+
React.useEffect(() => {
|
|
980
|
+
if (block.type === "video") {
|
|
981
|
+
ctx.onDiagnostic?.("video drawn as its poster: no built-in player");
|
|
982
|
+
}
|
|
983
|
+
}, [block.type]);
|
|
847
984
|
const styleCtx = { ...ctx.style, fontSize: inherited.fontSize };
|
|
848
985
|
// A pinned card judges its own context; every other block inherits the
|
|
849
986
|
// nearest plan card's. (A repeat card is judged per instance below.)
|
|
@@ -872,13 +1009,53 @@ function Block({
|
|
|
872
1009
|
style={style}
|
|
873
1010
|
ctx={ctx}
|
|
874
1011
|
inherited={inherited}
|
|
875
|
-
onPress={
|
|
1012
|
+
onPress={actionPress(block, ctx)}
|
|
1013
|
+
pressLabel={block.action === "close" ? "Close" : undefined}
|
|
876
1014
|
/>
|
|
877
1015
|
);
|
|
878
1016
|
|
|
879
|
-
case "
|
|
880
|
-
|
|
881
|
-
|
|
1017
|
+
case "timer":
|
|
1018
|
+
return <Timer block={block} style={style} ctx={ctx} inherited={inherited} />;
|
|
1019
|
+
|
|
1020
|
+
case "toggle": {
|
|
1021
|
+
const toggle = toggleSelection(block, ctx.packages, ctx.selectedId);
|
|
1022
|
+
if (!toggle) return null;
|
|
1023
|
+
const flip = () => ctx.onSelect(toggle.target);
|
|
1024
|
+
return (
|
|
1025
|
+
<Pressable
|
|
1026
|
+
accessibilityRole="switch"
|
|
1027
|
+
accessibilityState={{ checked: toggle.on }}
|
|
1028
|
+
onPress={flip}
|
|
1029
|
+
style={[{ flexDirection: "row", alignItems: "center", gap: 12 }, view]}
|
|
1030
|
+
>
|
|
1031
|
+
<BlockFill block={block} ctx={ctx} />
|
|
1032
|
+
<Text
|
|
1033
|
+
style={[
|
|
1034
|
+
{ flex: 1 },
|
|
1035
|
+
inheritedTextStyle(text, ctx.style),
|
|
1036
|
+
textOnly(style, styleCtx),
|
|
1037
|
+
]}
|
|
1038
|
+
>
|
|
1039
|
+
{resolveTags(
|
|
1040
|
+
block.label,
|
|
1041
|
+
ctx.packages.find((p) => p.packageId === ctx.selectedId),
|
|
1042
|
+
ctx.packages
|
|
1043
|
+
)}
|
|
1044
|
+
</Text>
|
|
1045
|
+
<Switch
|
|
1046
|
+
value={toggle.on}
|
|
1047
|
+
onValueChange={flip}
|
|
1048
|
+
trackColor={{ true: doc.accent }}
|
|
1049
|
+
/>
|
|
1050
|
+
</Pressable>
|
|
1051
|
+
);
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
case "image":
|
|
1055
|
+
case "video": {
|
|
1056
|
+
const image = block.type === "image";
|
|
1057
|
+
const url = image ? block.url || ctx.heroImageUrl : block.poster;
|
|
1058
|
+
const round = image && block.shape === "circle";
|
|
882
1059
|
const s = style;
|
|
883
1060
|
// A converted design sizes its own slot; the 160pt default is only for
|
|
884
1061
|
// a slot dropped into a flow column, and must not fight it.
|
|
@@ -889,17 +1066,17 @@ function Block({
|
|
|
889
1066
|
s.aspectRatio !== undefined ||
|
|
890
1067
|
(s.top !== undefined && s.bottom !== undefined) ||
|
|
891
1068
|
s.flex !== undefined);
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
const closeImage = closePress(block, ctx);
|
|
896
|
-
const ImageBox = closeImage ? Pressable : View;
|
|
1069
|
+
const action = block.type === "image" ? block.action : undefined;
|
|
1070
|
+
const imagePress = actionPress({ action }, ctx);
|
|
1071
|
+
const ImageBox = imagePress ? Pressable : View;
|
|
897
1072
|
return (
|
|
898
1073
|
<ImageBox
|
|
899
|
-
accessibilityRole={
|
|
900
|
-
accessibilityLabel={
|
|
901
|
-
|
|
902
|
-
|
|
1074
|
+
accessibilityRole={imagePress ? "button" : undefined}
|
|
1075
|
+
accessibilityLabel={
|
|
1076
|
+
imagePress && action ? ACTION_LABEL[action] : undefined
|
|
1077
|
+
}
|
|
1078
|
+
hitSlop={imagePress ? 8 : undefined}
|
|
1079
|
+
onPress={imagePress}
|
|
903
1080
|
style={[
|
|
904
1081
|
sized ? null : { height: 160 },
|
|
905
1082
|
{ borderRadius: round ? 9999 : sized ? 0 : 16, overflow: "hidden", flexShrink: 0 },
|
|
@@ -913,7 +1090,7 @@ function Block({
|
|
|
913
1090
|
resizeMode={block.fit ?? "cover"}
|
|
914
1091
|
style={{ width: "100%", height: "100%" }}
|
|
915
1092
|
/>
|
|
916
|
-
) : (
|
|
1093
|
+
) : image ? (
|
|
917
1094
|
<View
|
|
918
1095
|
style={{
|
|
919
1096
|
width: "100%",
|
|
@@ -921,7 +1098,7 @@ function Block({
|
|
|
921
1098
|
backgroundColor: withAlpha("#7d879b", 0.2),
|
|
922
1099
|
}}
|
|
923
1100
|
/>
|
|
924
|
-
)}
|
|
1101
|
+
) : null}
|
|
925
1102
|
</ImageBox>
|
|
926
1103
|
);
|
|
927
1104
|
}
|
|
@@ -980,17 +1157,18 @@ function Block({
|
|
|
980
1157
|
// A button the design marks as the close dismisses instead of buying,
|
|
981
1158
|
// and takes no accent fill: the CTA must stay the one accented thing on
|
|
982
1159
|
// the screen, or a "Not now" competes with "Subscribe" for the eye.
|
|
983
|
-
const close =
|
|
1160
|
+
const close = block.action === "close" ? ctx.onClose : undefined;
|
|
1161
|
+
const navigates = block.action === "next" || block.action === "back";
|
|
984
1162
|
// A purchase in flight: the button keeps its box and fill, loses its
|
|
985
1163
|
// label to a spinner in the ink colour, and ignores taps. A close
|
|
986
1164
|
// button is never blocked — the user can always leave.
|
|
987
|
-
const busy = !close && ctx.loading;
|
|
1165
|
+
const busy = !close && !navigates && ctx.loading;
|
|
988
1166
|
return (
|
|
989
1167
|
<Pressable
|
|
990
1168
|
accessibilityRole="button"
|
|
991
1169
|
accessibilityState={{ disabled: busy, busy }}
|
|
992
1170
|
disabled={busy}
|
|
993
|
-
onPress={close ?? ctx.onPurchase}
|
|
1171
|
+
onPress={navigates ? actionPress(block, ctx) : (close ?? ctx.onPurchase)}
|
|
994
1172
|
style={({ pressed }) => [
|
|
995
1173
|
{
|
|
996
1174
|
backgroundColor: close ? "transparent" : doc.accent,
|
|
@@ -1101,21 +1279,24 @@ function Block({
|
|
|
1101
1279
|
|
|
1102
1280
|
case "card": {
|
|
1103
1281
|
const layout = block.layout ?? "column";
|
|
1282
|
+
const carousel = layout === "carousel";
|
|
1104
1283
|
const base: ViewStyle =
|
|
1105
1284
|
layout === "stack"
|
|
1106
1285
|
? { position: "relative" }
|
|
1107
|
-
:
|
|
1108
|
-
? {
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1286
|
+
: carousel
|
|
1287
|
+
? { position: "relative", overflow: "hidden" }
|
|
1288
|
+
: layout === "grid"
|
|
1289
|
+
? {
|
|
1290
|
+
position: "relative",
|
|
1291
|
+
flexDirection: "row",
|
|
1292
|
+
flexWrap: "wrap",
|
|
1293
|
+
gap: style?.gap ?? 10,
|
|
1294
|
+
}
|
|
1295
|
+
: {
|
|
1296
|
+
position: "relative",
|
|
1297
|
+
flexDirection: layout === "row" ? "row" : "column",
|
|
1298
|
+
gap: style?.gap ?? 10,
|
|
1299
|
+
};
|
|
1119
1300
|
|
|
1120
1301
|
const tracks = layout === "grid" ? gridTracks(block) : null;
|
|
1121
1302
|
const renderChildren = (
|
|
@@ -1202,6 +1383,9 @@ function Block({
|
|
|
1202
1383
|
base,
|
|
1203
1384
|
layout === "stack" ? null : { borderRadius: 16 },
|
|
1204
1385
|
view,
|
|
1386
|
+
carousel
|
|
1387
|
+
? { height: typeof style?.height === "number" ? style.height : 220 }
|
|
1388
|
+
: null,
|
|
1205
1389
|
];
|
|
1206
1390
|
|
|
1207
1391
|
const cardPackage =
|
|
@@ -1222,7 +1406,15 @@ function Block({
|
|
|
1222
1406
|
radius={style?.radius ?? (layout === "stack" ? undefined : 16)}
|
|
1223
1407
|
onPress={pinned ? () => ctx.onSelect(cardPackage!.packageId) : undefined}
|
|
1224
1408
|
>
|
|
1225
|
-
{
|
|
1409
|
+
{carousel ? (
|
|
1410
|
+
<Carousel
|
|
1411
|
+
pages={renderChildren(cardPackage, context)}
|
|
1412
|
+
autoAdvance={block.autoAdvance}
|
|
1413
|
+
doc={doc}
|
|
1414
|
+
/>
|
|
1415
|
+
) : (
|
|
1416
|
+
renderChildren(cardPackage, context)
|
|
1417
|
+
)}
|
|
1226
1418
|
</CardBox>
|
|
1227
1419
|
);
|
|
1228
1420
|
}
|
|
@@ -1243,6 +1435,14 @@ export function RevnixBlockPaywall(props: RevnixBlockPaywallProps) {
|
|
|
1243
1435
|
const [internalSelected, setInternalSelected] = React.useState<
|
|
1244
1436
|
string | undefined
|
|
1245
1437
|
>(undefined);
|
|
1438
|
+
const [shownAt] = React.useState(Date.now);
|
|
1439
|
+
const [screenIndex, setScreenIndex] = React.useState(0);
|
|
1440
|
+
const [screenDoc, setScreenDoc] = React.useState(props.doc);
|
|
1441
|
+
if (screenDoc !== props.doc) {
|
|
1442
|
+
setScreenDoc(props.doc);
|
|
1443
|
+
setScreenIndex(0);
|
|
1444
|
+
}
|
|
1445
|
+
const screen = onboardingScreen(doc, screenIndex);
|
|
1246
1446
|
// Contract §1: host → own tap → design highlight → first package, each
|
|
1247
1447
|
// only when the offering carries it.
|
|
1248
1448
|
const selectedId = selectedPackageId(
|
|
@@ -1286,15 +1486,20 @@ export function RevnixBlockPaywall(props: RevnixBlockPaywallProps) {
|
|
|
1286
1486
|
onTerms: props.onTerms,
|
|
1287
1487
|
onPrivacy: props.onPrivacy,
|
|
1288
1488
|
onClose: props.onClose,
|
|
1489
|
+
navigate: (action) => {
|
|
1490
|
+
const to = stepScreen(doc, screenIndex, action);
|
|
1491
|
+
return to === undefined ? undefined : () => setScreenIndex(to);
|
|
1492
|
+
},
|
|
1289
1493
|
footer: props.footer,
|
|
1290
1494
|
heroImageUrl: props.heroImageUrl,
|
|
1291
1495
|
gradient: props.gradient,
|
|
1292
1496
|
onDiagnostic: props.onDiagnostic,
|
|
1293
1497
|
style: styleCtx,
|
|
1294
1498
|
loading: props.loading === true,
|
|
1499
|
+
shownAt,
|
|
1295
1500
|
};
|
|
1296
1501
|
|
|
1297
|
-
const children =
|
|
1502
|
+
const children = screen.blocks.map((block) => (
|
|
1298
1503
|
<Block
|
|
1299
1504
|
key={block.id}
|
|
1300
1505
|
block={block}
|
|
@@ -1312,7 +1517,7 @@ export function RevnixBlockPaywall(props: RevnixBlockPaywallProps) {
|
|
|
1312
1517
|
props.safeAreaTop ??
|
|
1313
1518
|
(Platform.OS === "android" ? (StatusBar.currentHeight ?? 0) : 0);
|
|
1314
1519
|
const fallbackClose =
|
|
1315
|
-
props.onClose && !hasCloseAction(
|
|
1520
|
+
props.onClose && !hasCloseAction(screen.blocks) ? (
|
|
1316
1521
|
<FallbackClose
|
|
1317
1522
|
doc={doc}
|
|
1318
1523
|
onPress={props.onClose}
|
|
@@ -1352,6 +1557,7 @@ export function RevnixBlockPaywall(props: RevnixBlockPaywallProps) {
|
|
|
1352
1557
|
style={[{ flex: 1 }, props.style]}
|
|
1353
1558
|
>
|
|
1354
1559
|
<ScrollView
|
|
1560
|
+
key={screenIndex}
|
|
1355
1561
|
contentContainerStyle={{ height: canvasHeight }}
|
|
1356
1562
|
// A designed screen never shows a scrollbar — the web preview has
|
|
1357
1563
|
// none, and in an RTL app the indicator lands on the left edge of
|
|
@@ -1388,6 +1594,14 @@ export function RevnixBlockPaywall(props: RevnixBlockPaywallProps) {
|
|
|
1388
1594
|
}}
|
|
1389
1595
|
>
|
|
1390
1596
|
{children}
|
|
1597
|
+
{screen.dots ? (
|
|
1598
|
+
<Dots
|
|
1599
|
+
count={screen.dots.count}
|
|
1600
|
+
active={screen.dots.active}
|
|
1601
|
+
doc={doc}
|
|
1602
|
+
style={{ top: 20, zIndex: 50 }}
|
|
1603
|
+
/>
|
|
1604
|
+
) : null}
|
|
1391
1605
|
</View>
|
|
1392
1606
|
</View>
|
|
1393
1607
|
</ScrollView>
|
|
@@ -1405,6 +1619,7 @@ export function RevnixBlockPaywall(props: RevnixBlockPaywallProps) {
|
|
|
1405
1619
|
style={[{ flex: 1 }, props.style]}
|
|
1406
1620
|
>
|
|
1407
1621
|
<ScrollView
|
|
1622
|
+
key={screenIndex}
|
|
1408
1623
|
contentContainerStyle={{
|
|
1409
1624
|
gap: 12,
|
|
1410
1625
|
paddingHorizontal: 20,
|
|
@@ -1417,6 +1632,14 @@ export function RevnixBlockPaywall(props: RevnixBlockPaywallProps) {
|
|
|
1417
1632
|
>
|
|
1418
1633
|
{children}
|
|
1419
1634
|
</ScrollView>
|
|
1635
|
+
{screen.dots ? (
|
|
1636
|
+
<Dots
|
|
1637
|
+
count={screen.dots.count}
|
|
1638
|
+
active={screen.dots.active}
|
|
1639
|
+
doc={doc}
|
|
1640
|
+
style={{ top: 20 + safeAreaTop, zIndex: 50 }}
|
|
1641
|
+
/>
|
|
1642
|
+
) : null}
|
|
1420
1643
|
{fallbackClose}
|
|
1421
1644
|
</ScreenBackground>
|
|
1422
1645
|
);
|
package/src/ui/RevnixPaywall.tsx
CHANGED
|
@@ -65,7 +65,16 @@ function asBlockDoc(value: unknown): PaywallBlockDoc | null {
|
|
|
65
65
|
typeof doc.textColor === "string" &&
|
|
66
66
|
typeof doc.accent === "string" &&
|
|
67
67
|
typeof doc.accentInk === "string"
|
|
68
|
-
?
|
|
68
|
+
? {
|
|
69
|
+
...(doc as PaywallBlockDoc),
|
|
70
|
+
...(doc.screens !== undefined
|
|
71
|
+
? {
|
|
72
|
+
screens: Array.isArray(doc.screens)
|
|
73
|
+
? doc.screens.filter((s) => Array.isArray(s) && s.length > 0)
|
|
74
|
+
: [],
|
|
75
|
+
}
|
|
76
|
+
: {}),
|
|
77
|
+
}
|
|
69
78
|
: null;
|
|
70
79
|
}
|
|
71
80
|
|
|
@@ -480,7 +489,7 @@ export function RevnixPaywall(props: RevnixPaywallProps) {
|
|
|
480
489
|
// arrives after a placeholder (or is swapped by an A/B assignment) would
|
|
481
490
|
// otherwise crash on "rendered more hooks than during the previous render".
|
|
482
491
|
const { config } = props;
|
|
483
|
-
const blocks = asBlockDoc(config.blocks);
|
|
492
|
+
const blocks = React.useMemo(() => asBlockDoc(config.blocks), [config.blocks]);
|
|
484
493
|
// Both sinks fire when both are wired; nil when neither is, which is what
|
|
485
494
|
// stops the renderer building a message nobody will read.
|
|
486
495
|
const renderDiagnostic =
|
package/src/version.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
// L3: the single source of the SDK version inside src/. Must match
|
|
2
2
|
// package.json "version" — version.test.ts fails the build when they drift
|
|
3
3
|
// (they had: client.ts said 0.7.0 while the package shipped 0.9.0).
|
|
4
|
-
export const SDK_VERSION = "1.
|
|
4
|
+
export const SDK_VERSION = "1.8.0";
|