react-native-revnix 1.6.0 → 1.7.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 +7 -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 +45 -3
- package/dist/blocks/types.js +1 -0
- package/dist/client.d.ts +4 -0
- package/dist/client.js +73 -5
- package/dist/esm/blocks/locale.js +7 -0
- package/dist/esm/blocks/selection.js +8 -0
- package/dist/esm/blocks/tags.js +31 -0
- package/dist/esm/blocks/types.js +1 -0
- package/dist/esm/client.js +73 -5
- package/dist/esm/store/index.js +7 -0
- package/dist/esm/ui/RevnixBlockPaywall.js +103 -22
- 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 +100 -19
- 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 +7 -0
- package/src/blocks/selection.ts +13 -0
- package/src/blocks/tags.ts +38 -0
- package/src/blocks/types.ts +51 -2
- 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 +186 -20
- package/src/version.ts +1 -1
package/src/types.ts
CHANGED
|
@@ -139,8 +139,38 @@ export interface RevnixConfig {
|
|
|
139
139
|
* Never throws into your app: an exception from this handler is reported
|
|
140
140
|
* through `onDiagnostic` and swallowed. */
|
|
141
141
|
onAttribution?: (attribution: RevnixAttribution) => void;
|
|
142
|
+
/** Opt-in device integrity evidence (Apple App Attest / Google Play
|
|
143
|
+
* Integrity) attached to every install report and `setAttribution` call.
|
|
144
|
+
* Pair it with the dashboard's "Require device integrity" setting, which
|
|
145
|
+
* strips attribution from installs that arrive without evidence. In React
|
|
146
|
+
* Native pass `nativeDeviceIntegrity` from "react-native-revnix/store";
|
|
147
|
+
* it needs iOS 14+ with App Attest, or Google Play services on Android.
|
|
148
|
+
* Simulators and emulators return nothing.
|
|
149
|
+
*
|
|
150
|
+
* Called at most once per customer id and bounded to 10s. A rejection,
|
|
151
|
+
* timeout, or missing/malformed result sends the request without
|
|
152
|
+
* evidence and reports `op: "deviceIntegrity"` through `onDiagnostic`. */
|
|
153
|
+
deviceIntegrity?: (
|
|
154
|
+
customerId: string
|
|
155
|
+
) => Promise<IntegrityEvidence | null | undefined>;
|
|
156
|
+
/** Google Advertising ID source, stored on the customer as `gps_adid` at
|
|
157
|
+
* launch and resent only when it changes. In React Native pass
|
|
158
|
+
* `nativeAdvertisingId` from "react-native-revnix/store"; it needs
|
|
159
|
+
* `play-services-ads-identifier` and the `AD_ID` permission in the app and
|
|
160
|
+
* answers null on iOS and on opt-out, and rejects when the id cannot be
|
|
161
|
+
* read. A null answer deletes a previously stored `gps_adid`; a
|
|
162
|
+
* rejection changes nothing and reports `op: "advertisingId"`. */
|
|
163
|
+
advertisingId?: () => Promise<string | null | undefined>;
|
|
142
164
|
}
|
|
143
165
|
|
|
166
|
+
/** Device integrity evidence for `POST /v1/installs` and `/v1/attribution`.
|
|
167
|
+
* iOS: an App Attest key id and its attestation (standard base64) over
|
|
168
|
+
* SHA-256 of the customer id. Android: a classic Play Integrity token whose
|
|
169
|
+
* nonce is base64url (no padding) of SHA-256 of the customer id. */
|
|
170
|
+
export type IntegrityEvidence =
|
|
171
|
+
| { platform: "ios"; keyId: string; attestation: string }
|
|
172
|
+
| { platform: "android"; token: string };
|
|
173
|
+
|
|
144
174
|
/** AT11: the install-attribution verdict — GET
|
|
145
175
|
* /v1/customers/{id}/attribution. `installMatch: "unknown"` (no install
|
|
146
176
|
* recorded yet, a normal cold-start race) is never sent to hosts as such;
|
|
@@ -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,13 +61,14 @@ 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 {
|
|
@@ -76,6 +78,7 @@ import {
|
|
|
76
78
|
type PaywallBlock,
|
|
77
79
|
type PaywallBlockDoc,
|
|
78
80
|
type ProductsBlock,
|
|
81
|
+
type TimerBlock,
|
|
79
82
|
} from "../blocks/types.js";
|
|
80
83
|
import type { RevnixPaywallPackage } from "./RevnixPaywall.js";
|
|
81
84
|
|
|
@@ -607,6 +610,7 @@ interface RenderContext {
|
|
|
607
610
|
style: StyleContext;
|
|
608
611
|
/** Host purchase in flight: purchase buttons disable and spin. */
|
|
609
612
|
loading: boolean;
|
|
613
|
+
shownAt: number;
|
|
610
614
|
}
|
|
611
615
|
|
|
612
616
|
/** Text blocks always render inside a View: a style may carry both typography
|
|
@@ -669,6 +673,108 @@ function BlockText({
|
|
|
669
673
|
return <View style={toViewStyle(style, styleCtx)}>{body}</View>;
|
|
670
674
|
}
|
|
671
675
|
|
|
676
|
+
function Timer({
|
|
677
|
+
block,
|
|
678
|
+
style,
|
|
679
|
+
ctx,
|
|
680
|
+
inherited,
|
|
681
|
+
}: {
|
|
682
|
+
block: TimerBlock;
|
|
683
|
+
style: BlockStyle | undefined;
|
|
684
|
+
ctx: RenderContext;
|
|
685
|
+
inherited: InheritedText;
|
|
686
|
+
}) {
|
|
687
|
+
const [now, setNow] = React.useState(Date.now);
|
|
688
|
+
React.useEffect(() => {
|
|
689
|
+
const id = setInterval(() => setNow(Date.now()), 1000);
|
|
690
|
+
return () => clearInterval(id);
|
|
691
|
+
}, []);
|
|
692
|
+
const text = timerText(block, ctx.shownAt, now);
|
|
693
|
+
if (text === null) return null;
|
|
694
|
+
return <BlockText text={text} style={style} ctx={ctx} inherited={inherited} />;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function Carousel({
|
|
698
|
+
pages,
|
|
699
|
+
autoAdvance,
|
|
700
|
+
doc,
|
|
701
|
+
}: {
|
|
702
|
+
pages: React.ReactNode[];
|
|
703
|
+
autoAdvance?: number;
|
|
704
|
+
doc: PaywallBlockDoc;
|
|
705
|
+
}) {
|
|
706
|
+
const scroller = React.useRef<ScrollView>(null);
|
|
707
|
+
const [size, setSize] = React.useState({ width: 0, height: 0 });
|
|
708
|
+
const [page, setPage] = React.useState(0);
|
|
709
|
+
React.useEffect(() => {
|
|
710
|
+
if (!autoAdvance || size.width === 0 || pages.length < 2) return;
|
|
711
|
+
const id = setInterval(() => {
|
|
712
|
+
scroller.current?.scrollTo({
|
|
713
|
+
x: ((page + 1) % pages.length) * size.width,
|
|
714
|
+
animated: true,
|
|
715
|
+
});
|
|
716
|
+
}, autoAdvance * 1000);
|
|
717
|
+
return () => clearInterval(id);
|
|
718
|
+
}, [autoAdvance, size.width, page, pages.length]);
|
|
719
|
+
return (
|
|
720
|
+
<View
|
|
721
|
+
style={{ flex: 1 }}
|
|
722
|
+
onLayout={(e) =>
|
|
723
|
+
setSize({
|
|
724
|
+
width: e.nativeEvent.layout.width,
|
|
725
|
+
height: e.nativeEvent.layout.height,
|
|
726
|
+
})
|
|
727
|
+
}
|
|
728
|
+
>
|
|
729
|
+
<ScrollView
|
|
730
|
+
ref={scroller}
|
|
731
|
+
horizontal
|
|
732
|
+
pagingEnabled
|
|
733
|
+
showsHorizontalScrollIndicator={false}
|
|
734
|
+
scrollEventThrottle={16}
|
|
735
|
+
onScroll={(e) => {
|
|
736
|
+
if (size.width > 0) {
|
|
737
|
+
setPage(Math.round(e.nativeEvent.contentOffset.x / size.width));
|
|
738
|
+
}
|
|
739
|
+
}}
|
|
740
|
+
>
|
|
741
|
+
{pages.map((node, i) => (
|
|
742
|
+
<View key={i} style={size}>
|
|
743
|
+
{node}
|
|
744
|
+
</View>
|
|
745
|
+
))}
|
|
746
|
+
</ScrollView>
|
|
747
|
+
{pages.length > 1 ? (
|
|
748
|
+
<View
|
|
749
|
+
pointerEvents="none"
|
|
750
|
+
style={{
|
|
751
|
+
position: "absolute",
|
|
752
|
+
bottom: 8,
|
|
753
|
+
left: 0,
|
|
754
|
+
right: 0,
|
|
755
|
+
flexDirection: "row",
|
|
756
|
+
justifyContent: "center",
|
|
757
|
+
gap: 6,
|
|
758
|
+
}}
|
|
759
|
+
>
|
|
760
|
+
{pages.map((_, i) => (
|
|
761
|
+
<View
|
|
762
|
+
key={i}
|
|
763
|
+
style={{
|
|
764
|
+
width: 6,
|
|
765
|
+
height: 6,
|
|
766
|
+
borderRadius: 3,
|
|
767
|
+
backgroundColor:
|
|
768
|
+
i === page ? doc.accent : withAlpha(doc.textColor, 0.3),
|
|
769
|
+
}}
|
|
770
|
+
/>
|
|
771
|
+
))}
|
|
772
|
+
</View>
|
|
773
|
+
) : null}
|
|
774
|
+
</View>
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
|
|
672
778
|
/** The press handler for a block the design marks as the close, or undefined
|
|
673
779
|
* when it is not one (or the host wired no dismissal). One helper so text,
|
|
674
780
|
* image and button blocks cannot drift on what "action: close" means. */
|
|
@@ -844,6 +950,11 @@ function Block({
|
|
|
844
950
|
selection?: SelectionContext;
|
|
845
951
|
}): React.ReactElement | null {
|
|
846
952
|
const { doc } = ctx;
|
|
953
|
+
React.useEffect(() => {
|
|
954
|
+
if (block.type === "video") {
|
|
955
|
+
ctx.onDiagnostic?.("video drawn as its poster: no built-in player");
|
|
956
|
+
}
|
|
957
|
+
}, [block.type]);
|
|
847
958
|
const styleCtx = { ...ctx.style, fontSize: inherited.fontSize };
|
|
848
959
|
// A pinned card judges its own context; every other block inherits the
|
|
849
960
|
// nearest plan card's. (A repeat card is judged per instance below.)
|
|
@@ -876,9 +987,48 @@ function Block({
|
|
|
876
987
|
/>
|
|
877
988
|
);
|
|
878
989
|
|
|
879
|
-
case "
|
|
880
|
-
|
|
881
|
-
|
|
990
|
+
case "timer":
|
|
991
|
+
return <Timer block={block} style={style} ctx={ctx} inherited={inherited} />;
|
|
992
|
+
|
|
993
|
+
case "toggle": {
|
|
994
|
+
const toggle = toggleSelection(block, ctx.packages, ctx.selectedId);
|
|
995
|
+
if (!toggle) return null;
|
|
996
|
+
const flip = () => ctx.onSelect(toggle.target);
|
|
997
|
+
return (
|
|
998
|
+
<Pressable
|
|
999
|
+
accessibilityRole="switch"
|
|
1000
|
+
accessibilityState={{ checked: toggle.on }}
|
|
1001
|
+
onPress={flip}
|
|
1002
|
+
style={[{ flexDirection: "row", alignItems: "center", gap: 12 }, view]}
|
|
1003
|
+
>
|
|
1004
|
+
<BlockFill block={block} ctx={ctx} />
|
|
1005
|
+
<Text
|
|
1006
|
+
style={[
|
|
1007
|
+
{ flex: 1 },
|
|
1008
|
+
inheritedTextStyle(text, ctx.style),
|
|
1009
|
+
textOnly(style, styleCtx),
|
|
1010
|
+
]}
|
|
1011
|
+
>
|
|
1012
|
+
{resolveTags(
|
|
1013
|
+
block.label,
|
|
1014
|
+
ctx.packages.find((p) => p.packageId === ctx.selectedId),
|
|
1015
|
+
ctx.packages
|
|
1016
|
+
)}
|
|
1017
|
+
</Text>
|
|
1018
|
+
<Switch
|
|
1019
|
+
value={toggle.on}
|
|
1020
|
+
onValueChange={flip}
|
|
1021
|
+
trackColor={{ true: doc.accent }}
|
|
1022
|
+
/>
|
|
1023
|
+
</Pressable>
|
|
1024
|
+
);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
case "image":
|
|
1028
|
+
case "video": {
|
|
1029
|
+
const image = block.type === "image";
|
|
1030
|
+
const url = image ? block.url || ctx.heroImageUrl : block.poster;
|
|
1031
|
+
const round = image && block.shape === "circle";
|
|
882
1032
|
const s = style;
|
|
883
1033
|
// A converted design sizes its own slot; the 160pt default is only for
|
|
884
1034
|
// a slot dropped into a flow column, and must not fight it.
|
|
@@ -892,7 +1042,7 @@ function Block({
|
|
|
892
1042
|
// An icon the design marks as the close (REV-252). Same rule as text:
|
|
893
1043
|
// the whole box is the target, and the style lands on the Pressable so
|
|
894
1044
|
// a pinned icon keeps its offsets.
|
|
895
|
-
const closeImage = closePress(block, ctx);
|
|
1045
|
+
const closeImage = image ? closePress(block, ctx) : undefined;
|
|
896
1046
|
const ImageBox = closeImage ? Pressable : View;
|
|
897
1047
|
return (
|
|
898
1048
|
<ImageBox
|
|
@@ -913,7 +1063,7 @@ function Block({
|
|
|
913
1063
|
resizeMode={block.fit ?? "cover"}
|
|
914
1064
|
style={{ width: "100%", height: "100%" }}
|
|
915
1065
|
/>
|
|
916
|
-
) : (
|
|
1066
|
+
) : image ? (
|
|
917
1067
|
<View
|
|
918
1068
|
style={{
|
|
919
1069
|
width: "100%",
|
|
@@ -921,7 +1071,7 @@ function Block({
|
|
|
921
1071
|
backgroundColor: withAlpha("#7d879b", 0.2),
|
|
922
1072
|
}}
|
|
923
1073
|
/>
|
|
924
|
-
)}
|
|
1074
|
+
) : null}
|
|
925
1075
|
</ImageBox>
|
|
926
1076
|
);
|
|
927
1077
|
}
|
|
@@ -1101,21 +1251,24 @@ function Block({
|
|
|
1101
1251
|
|
|
1102
1252
|
case "card": {
|
|
1103
1253
|
const layout = block.layout ?? "column";
|
|
1254
|
+
const carousel = layout === "carousel";
|
|
1104
1255
|
const base: ViewStyle =
|
|
1105
1256
|
layout === "stack"
|
|
1106
1257
|
? { position: "relative" }
|
|
1107
|
-
:
|
|
1108
|
-
? {
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1258
|
+
: carousel
|
|
1259
|
+
? { position: "relative", overflow: "hidden" }
|
|
1260
|
+
: layout === "grid"
|
|
1261
|
+
? {
|
|
1262
|
+
position: "relative",
|
|
1263
|
+
flexDirection: "row",
|
|
1264
|
+
flexWrap: "wrap",
|
|
1265
|
+
gap: style?.gap ?? 10,
|
|
1266
|
+
}
|
|
1267
|
+
: {
|
|
1268
|
+
position: "relative",
|
|
1269
|
+
flexDirection: layout === "row" ? "row" : "column",
|
|
1270
|
+
gap: style?.gap ?? 10,
|
|
1271
|
+
};
|
|
1119
1272
|
|
|
1120
1273
|
const tracks = layout === "grid" ? gridTracks(block) : null;
|
|
1121
1274
|
const renderChildren = (
|
|
@@ -1202,6 +1355,9 @@ function Block({
|
|
|
1202
1355
|
base,
|
|
1203
1356
|
layout === "stack" ? null : { borderRadius: 16 },
|
|
1204
1357
|
view,
|
|
1358
|
+
carousel
|
|
1359
|
+
? { height: typeof style?.height === "number" ? style.height : 220 }
|
|
1360
|
+
: null,
|
|
1205
1361
|
];
|
|
1206
1362
|
|
|
1207
1363
|
const cardPackage =
|
|
@@ -1222,7 +1378,15 @@ function Block({
|
|
|
1222
1378
|
radius={style?.radius ?? (layout === "stack" ? undefined : 16)}
|
|
1223
1379
|
onPress={pinned ? () => ctx.onSelect(cardPackage!.packageId) : undefined}
|
|
1224
1380
|
>
|
|
1225
|
-
{
|
|
1381
|
+
{carousel ? (
|
|
1382
|
+
<Carousel
|
|
1383
|
+
pages={renderChildren(cardPackage, context)}
|
|
1384
|
+
autoAdvance={block.autoAdvance}
|
|
1385
|
+
doc={doc}
|
|
1386
|
+
/>
|
|
1387
|
+
) : (
|
|
1388
|
+
renderChildren(cardPackage, context)
|
|
1389
|
+
)}
|
|
1226
1390
|
</CardBox>
|
|
1227
1391
|
);
|
|
1228
1392
|
}
|
|
@@ -1243,6 +1407,7 @@ export function RevnixBlockPaywall(props: RevnixBlockPaywallProps) {
|
|
|
1243
1407
|
const [internalSelected, setInternalSelected] = React.useState<
|
|
1244
1408
|
string | undefined
|
|
1245
1409
|
>(undefined);
|
|
1410
|
+
const [shownAt] = React.useState(Date.now);
|
|
1246
1411
|
// Contract §1: host → own tap → design highlight → first package, each
|
|
1247
1412
|
// only when the offering carries it.
|
|
1248
1413
|
const selectedId = selectedPackageId(
|
|
@@ -1292,6 +1457,7 @@ export function RevnixBlockPaywall(props: RevnixBlockPaywallProps) {
|
|
|
1292
1457
|
onDiagnostic: props.onDiagnostic,
|
|
1293
1458
|
style: styleCtx,
|
|
1294
1459
|
loading: props.loading === true,
|
|
1460
|
+
shownAt,
|
|
1295
1461
|
};
|
|
1296
1462
|
|
|
1297
1463
|
const children = doc.blocks.map((block) => (
|
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.7.0";
|