react-native-revnix 1.7.0 → 1.9.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 +1 -0
- package/android/src/main/kotlin/com/revnix/reactnative/RevnixStoreModule.kt +29 -9
- package/dist/blocks/locale.js +5 -0
- package/dist/blocks/types.d.ts +20 -5
- package/dist/blocks/types.js +20 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.js +43 -5
- package/dist/errors.d.ts +6 -0
- package/dist/errors.js +11 -1
- package/dist/esm/blocks/locale.js +5 -0
- package/dist/esm/blocks/types.js +18 -0
- package/dist/esm/client.js +44 -6
- package/dist/esm/errors.js +9 -0
- package/dist/esm/index.js +1 -1
- package/dist/esm/store/index.js +1 -1
- package/dist/esm/ui/RevnixBlockPaywall.js +51 -31
- package/dist/esm/ui/RevnixPaywall.js +11 -2
- package/dist/esm/version.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -1
- package/dist/store/index.js +1 -1
- package/dist/store.d.ts +7 -1
- package/dist/types.d.ts +3 -0
- package/dist/ui/RevnixBlockPaywall.js +50 -30
- package/dist/ui/RevnixPaywall.js +11 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/ios/RevnixStore.m +1 -0
- package/ios/RevnixStore.swift +3 -2
- package/package.json +1 -1
- package/src/blocks/locale.ts +7 -0
- package/src/blocks/types.ts +35 -5
- package/src/client.ts +48 -7
- package/src/errors.ts +10 -0
- package/src/index.ts +1 -0
- package/src/store/index.ts +2 -2
- package/src/store.ts +7 -1
- package/src/types.ts +3 -0
- package/src/ui/RevnixBlockPaywall.tsx +103 -46
- package/src/ui/RevnixPaywall.tsx +11 -2
- package/src/version.ts +1 -1
|
@@ -24,7 +24,7 @@ import { backgroundLayers, coverPlacement, } from "../blocks/background.js";
|
|
|
24
24
|
import { detectLocale, linkLabels, localizeBlockDoc } from "../blocks/locale.js";
|
|
25
25
|
import { resolveTags, timerText } from "../blocks/tags.js";
|
|
26
26
|
import { canvasMetrics, effectiveStyle, isVisible, packageContext, selectedPackageId, toggleSelection, } from "../blocks/selection.js";
|
|
27
|
-
import { DESIGN_CANVAS, hasCloseAction, } from "../blocks/types.js";
|
|
27
|
+
import { DESIGN_CANVAS, hasCloseAction, onboardingScreen, stepScreen, } from "../blocks/types.js";
|
|
28
28
|
/** CSS angle (0 = up, 90 = right) → RN gradient start/end points. */
|
|
29
29
|
function angleToPoints(angle) {
|
|
30
30
|
const rad = (angle * Math.PI) / 180;
|
|
@@ -240,7 +240,7 @@ function FallbackClose({ doc, onPress, safeAreaTop, }) {
|
|
|
240
240
|
/** Text blocks always render inside a View: a style may carry both typography
|
|
241
241
|
* and box properties (a round chip is a 34pt circle with a centered glyph),
|
|
242
242
|
* and RN cannot centre a glyph inside <Text> the way flexbox does. */
|
|
243
|
-
function BlockText({ text, style, ctx, inherited, onPress, }) {
|
|
243
|
+
function BlockText({ text, style, ctx, inherited, onPress, pressLabel, }) {
|
|
244
244
|
const styleCtx = { ...ctx.style, fontSize: inherited.fontSize };
|
|
245
245
|
// The block's own typography is folded into the inherited set first, so
|
|
246
246
|
// em letter-spacing and unitless line-height resolve against whichever font
|
|
@@ -255,7 +255,7 @@ function BlockText({ text, style, ctx, inherited, onPress, }) {
|
|
|
255
255
|
// wrapping a styled View would leave a pinned chip resolving its offsets
|
|
256
256
|
// against a zero-sized parent.
|
|
257
257
|
if (onPress) {
|
|
258
|
-
return (React.createElement(Pressable, { accessibilityRole: "button", accessibilityLabel:
|
|
258
|
+
return (React.createElement(Pressable, { accessibilityRole: "button", accessibilityLabel: pressLabel, hitSlop: 8, onPress: onPress, style: toViewStyle(style, styleCtx) }, body));
|
|
259
259
|
}
|
|
260
260
|
return React.createElement(View, { style: toViewStyle(style, styleCtx) }, body);
|
|
261
261
|
}
|
|
@@ -294,27 +294,34 @@ function Carousel({ pages, autoAdvance, doc, }) {
|
|
|
294
294
|
setPage(Math.round(e.nativeEvent.contentOffset.x / size.width));
|
|
295
295
|
}
|
|
296
296
|
} }, pages.map((node, i) => (React.createElement(View, { key: i, style: size }, node)))),
|
|
297
|
-
pages.length > 1 ? (React.createElement(
|
|
297
|
+
pages.length > 1 ? (React.createElement(Dots, { count: pages.length, active: page, doc: doc, style: { bottom: 8 } })) : null));
|
|
298
|
+
}
|
|
299
|
+
function Dots({ count, active, doc, style, }) {
|
|
300
|
+
return (React.createElement(View, { pointerEvents: "none", style: [
|
|
301
|
+
{
|
|
298
302
|
position: "absolute",
|
|
299
|
-
bottom: 8,
|
|
300
303
|
left: 0,
|
|
301
304
|
right: 0,
|
|
302
305
|
flexDirection: "row",
|
|
303
306
|
justifyContent: "center",
|
|
304
307
|
gap: 6,
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
308
|
+
},
|
|
309
|
+
style,
|
|
310
|
+
] }, Array.from({ length: count }, (_, i) => (React.createElement(View, { key: i, style: {
|
|
311
|
+
width: 6,
|
|
312
|
+
height: 6,
|
|
313
|
+
borderRadius: 3,
|
|
314
|
+
backgroundColor: i === active ? doc.accent : withAlpha(doc.textColor, 0.3),
|
|
315
|
+
} })))));
|
|
311
316
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
* image and button blocks cannot drift on what "action: close" means. */
|
|
315
|
-
function closePress(block, ctx) {
|
|
316
|
-
return block.action === "close" && ctx.onClose ? ctx.onClose : undefined;
|
|
317
|
+
function actionPress(block, ctx) {
|
|
318
|
+
return block.action === "close" ? ctx.onClose : ctx.navigate(block.action);
|
|
317
319
|
}
|
|
320
|
+
const ACTION_LABEL = {
|
|
321
|
+
close: "Close",
|
|
322
|
+
next: "Next",
|
|
323
|
+
back: "Back",
|
|
324
|
+
};
|
|
318
325
|
/** Typography that is NOT inherited (decoration) — everything else already
|
|
319
326
|
* arrives through the inherited set. */
|
|
320
327
|
function textOnly(s, ctx) {
|
|
@@ -439,7 +446,7 @@ function Block({ block, ctx, pkg, inherited, selection, }) {
|
|
|
439
446
|
const size = text.fontSize;
|
|
440
447
|
switch (block.type) {
|
|
441
448
|
case "text":
|
|
442
|
-
return (React.createElement(BlockText, { text: resolveTags(block.text, pkg, ctx.packages), style: style, ctx: ctx, inherited: inherited, onPress:
|
|
449
|
+
return (React.createElement(BlockText, { text: resolveTags(block.text, pkg, ctx.packages), style: style, ctx: ctx, inherited: inherited, onPress: actionPress(block, ctx), pressLabel: block.action === "close" ? "Close" : undefined }));
|
|
443
450
|
case "timer":
|
|
444
451
|
return React.createElement(Timer, { block: block, style: style, ctx: ctx, inherited: inherited });
|
|
445
452
|
case "toggle": {
|
|
@@ -470,12 +477,10 @@ function Block({ block, ctx, pkg, inherited, selection, }) {
|
|
|
470
477
|
s.aspectRatio !== undefined ||
|
|
471
478
|
(s.top !== undefined && s.bottom !== undefined) ||
|
|
472
479
|
s.flex !== undefined);
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
const ImageBox = closeImage ? Pressable : View;
|
|
478
|
-
return (React.createElement(ImageBox, { accessibilityRole: closeImage ? "button" : undefined, accessibilityLabel: closeImage ? "Close" : undefined, hitSlop: closeImage ? 8 : undefined, onPress: closeImage, style: [
|
|
480
|
+
const action = block.type === "image" ? block.action : undefined;
|
|
481
|
+
const imagePress = actionPress({ action }, ctx);
|
|
482
|
+
const ImageBox = imagePress ? Pressable : View;
|
|
483
|
+
return (React.createElement(ImageBox, { accessibilityRole: imagePress ? "button" : undefined, accessibilityLabel: imagePress && action ? ACTION_LABEL[action] : undefined, hitSlop: imagePress ? 8 : undefined, onPress: imagePress, style: [
|
|
479
484
|
sized ? null : { height: 160 },
|
|
480
485
|
{ borderRadius: round ? 9999 : sized ? 0 : 16, overflow: "hidden", flexShrink: 0 },
|
|
481
486
|
view,
|
|
@@ -516,12 +521,13 @@ function Block({ block, ctx, pkg, inherited, selection, }) {
|
|
|
516
521
|
// A button the design marks as the close dismisses instead of buying,
|
|
517
522
|
// and takes no accent fill: the CTA must stay the one accented thing on
|
|
518
523
|
// the screen, or a "Not now" competes with "Subscribe" for the eye.
|
|
519
|
-
const close =
|
|
524
|
+
const close = block.action === "close" ? ctx.onClose : undefined;
|
|
525
|
+
const navigates = block.action === "next" || block.action === "back";
|
|
520
526
|
// A purchase in flight: the button keeps its box and fill, loses its
|
|
521
527
|
// label to a spinner in the ink colour, and ignores taps. A close
|
|
522
528
|
// button is never blocked — the user can always leave.
|
|
523
|
-
const busy = !close && ctx.loading;
|
|
524
|
-
return (React.createElement(Pressable, { accessibilityRole: "button", accessibilityState: { disabled: busy, busy }, disabled: busy, onPress: close ?? ctx.onPurchase, style: ({ pressed }) => [
|
|
529
|
+
const busy = !close && !navigates && ctx.loading;
|
|
530
|
+
return (React.createElement(Pressable, { accessibilityRole: "button", accessibilityState: { disabled: busy, busy }, disabled: busy, onPress: navigates ? actionPress(block, ctx) : (close ?? ctx.onPurchase), style: ({ pressed }) => [
|
|
525
531
|
{
|
|
526
532
|
backgroundColor: close ? "transparent" : doc.accent,
|
|
527
533
|
borderRadius: 12,
|
|
@@ -678,6 +684,13 @@ export function RevnixBlockPaywall(props) {
|
|
|
678
684
|
const doc = React.useMemo(() => localizeBlockDoc(props.doc, props.locale ?? detectLocale()), [props.doc, props.locale]);
|
|
679
685
|
const [internalSelected, setInternalSelected] = React.useState(undefined);
|
|
680
686
|
const [shownAt] = React.useState(Date.now);
|
|
687
|
+
const [screenIndex, setScreenIndex] = React.useState(0);
|
|
688
|
+
const [screenDoc, setScreenDoc] = React.useState(props.doc);
|
|
689
|
+
if (screenDoc !== props.doc) {
|
|
690
|
+
setScreenDoc(props.doc);
|
|
691
|
+
setScreenIndex(0);
|
|
692
|
+
}
|
|
693
|
+
const screen = onboardingScreen(doc, screenIndex);
|
|
681
694
|
// Contract §1: host → own tap → design highlight → first package, each
|
|
682
695
|
// only when the offering carries it.
|
|
683
696
|
const selectedId = selectedPackageId(packages, props.selectedPackageId, internalSelected, props.highlightPackageId);
|
|
@@ -713,6 +726,10 @@ export function RevnixBlockPaywall(props) {
|
|
|
713
726
|
onTerms: props.onTerms,
|
|
714
727
|
onPrivacy: props.onPrivacy,
|
|
715
728
|
onClose: props.onClose,
|
|
729
|
+
navigate: (action) => {
|
|
730
|
+
const to = stepScreen(doc, screenIndex, action);
|
|
731
|
+
return to === undefined ? undefined : () => setScreenIndex(to);
|
|
732
|
+
},
|
|
716
733
|
footer: props.footer,
|
|
717
734
|
heroImageUrl: props.heroImageUrl,
|
|
718
735
|
gradient: props.gradient,
|
|
@@ -721,14 +738,14 @@ export function RevnixBlockPaywall(props) {
|
|
|
721
738
|
loading: props.loading === true,
|
|
722
739
|
shownAt,
|
|
723
740
|
};
|
|
724
|
-
const children =
|
|
741
|
+
const children = screen.blocks.map((block) => (React.createElement(Block, { key: block.id, block: block, ctx: ctx, pkg: packages.find((p) => p.packageId === selectedId) ?? packages[0], inherited: rootText })));
|
|
725
742
|
// REV-252: the renderer's own close, drawn only for a design that authors
|
|
726
743
|
// none. Rendered as a sibling of the tree — outside the canvas scale — so
|
|
727
744
|
// it keeps its tap size and its distance from the screen edge whatever the
|
|
728
745
|
// device width does to the design.
|
|
729
746
|
const safeAreaTop = props.safeAreaTop ??
|
|
730
747
|
(Platform.OS === "android" ? (StatusBar.currentHeight ?? 0) : 0);
|
|
731
|
-
const fallbackClose = props.onClose && !hasCloseAction(
|
|
748
|
+
const fallbackClose = props.onClose && !hasCloseAction(screen.blocks) ? (React.createElement(FallbackClose, { doc: doc, onPress: props.onClose, safeAreaTop: safeAreaTop })) : null;
|
|
732
749
|
const [width, setWidth] = React.useState(null);
|
|
733
750
|
const [viewportHeight, setViewportHeight] = React.useState(null);
|
|
734
751
|
if (doc.layout === "canvas") {
|
|
@@ -749,7 +766,7 @@ export function RevnixBlockPaywall(props) {
|
|
|
749
766
|
const canvasHeight = metrics?.scaledHeight ?? DESIGN_CANVAS.height;
|
|
750
767
|
const left = metrics?.left ?? 0;
|
|
751
768
|
return (React.createElement(ScreenBackground, { doc: doc, gradient: props.gradient, blur: props.blur, onDiagnostic: props.onDiagnostic, style: [{ flex: 1 }, props.style] },
|
|
752
|
-
React.createElement(ScrollView, { contentContainerStyle: { height: canvasHeight },
|
|
769
|
+
React.createElement(ScrollView, { key: screenIndex, contentContainerStyle: { height: canvasHeight },
|
|
753
770
|
// A designed screen never shows a scrollbar — the web preview has
|
|
754
771
|
// none, and in an RTL app the indicator lands on the left edge of
|
|
755
772
|
// the artwork. When nothing overflows, scrolling is off entirely so
|
|
@@ -773,15 +790,18 @@ export function RevnixBlockPaywall(props) {
|
|
|
773
790
|
{ translateY: -((canvasHeight / scale) * (1 - scale)) / 2 },
|
|
774
791
|
{ scale },
|
|
775
792
|
],
|
|
776
|
-
} },
|
|
793
|
+
} },
|
|
794
|
+
children,
|
|
795
|
+
screen.dots ? (React.createElement(Dots, { count: screen.dots.count, active: screen.dots.active, doc: doc, style: { top: 20, zIndex: 50 } })) : null))),
|
|
777
796
|
fallbackClose));
|
|
778
797
|
}
|
|
779
798
|
return (React.createElement(ScreenBackground, { doc: doc, gradient: props.gradient, blur: props.blur, onDiagnostic: props.onDiagnostic, style: [{ flex: 1 }, props.style] },
|
|
780
|
-
React.createElement(ScrollView, { contentContainerStyle: {
|
|
799
|
+
React.createElement(ScrollView, { key: screenIndex, contentContainerStyle: {
|
|
781
800
|
gap: 12,
|
|
782
801
|
paddingHorizontal: 20,
|
|
783
802
|
paddingTop: 18,
|
|
784
803
|
paddingBottom: 30,
|
|
785
804
|
}, showsVerticalScrollIndicator: false, alwaysBounceVertical: false, overScrollMode: "never" }, children),
|
|
805
|
+
screen.dots ? (React.createElement(Dots, { count: screen.dots.count, active: screen.dots.active, doc: doc, style: { top: 20 + safeAreaTop, zIndex: 50 } })) : null,
|
|
786
806
|
fallbackClose));
|
|
787
807
|
}
|
|
@@ -38,7 +38,16 @@ function asBlockDoc(value) {
|
|
|
38
38
|
typeof doc.textColor === "string" &&
|
|
39
39
|
typeof doc.accent === "string" &&
|
|
40
40
|
typeof doc.accentInk === "string"
|
|
41
|
-
?
|
|
41
|
+
? {
|
|
42
|
+
...doc,
|
|
43
|
+
...(doc.screens !== undefined
|
|
44
|
+
? {
|
|
45
|
+
screens: Array.isArray(doc.screens)
|
|
46
|
+
? doc.screens.filter((s) => Array.isArray(s) && s.length > 0)
|
|
47
|
+
: [],
|
|
48
|
+
}
|
|
49
|
+
: {}),
|
|
50
|
+
}
|
|
42
51
|
: null;
|
|
43
52
|
}
|
|
44
53
|
export const DEFAULT_PAYWALL_THEME = {
|
|
@@ -251,7 +260,7 @@ export function RevnixPaywall(props) {
|
|
|
251
260
|
// arrives after a placeholder (or is swapped by an A/B assignment) would
|
|
252
261
|
// otherwise crash on "rendered more hooks than during the previous render".
|
|
253
262
|
const { config } = props;
|
|
254
|
-
const blocks = asBlockDoc(config.blocks);
|
|
263
|
+
const blocks = React.useMemo(() => asBlockDoc(config.blocks), [config.blocks]);
|
|
255
264
|
// Both sinks fire when both are wired; nil when neither is, which is what
|
|
256
265
|
// stops the renderer building a message nobody will read.
|
|
257
266
|
const renderDiagnostic = client?.reportRenderDiagnostic || props.onDiagnostic
|
package/dist/esm/version.js
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.9.0";
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { RevnixClient } from "./client.js";
|
|
|
2
2
|
import { setLocale } from "./blocks/locale.js";
|
|
3
3
|
import type { RevnixConfig } from "./types.js";
|
|
4
4
|
export { RevnixClient } from "./client.js";
|
|
5
|
-
export { PurchaseBlockedError, RevnixAuthError, RevnixError, RevnixNetworkError, RevnixRateLimitError, RevnixTimeoutError, } from "./errors.js";
|
|
5
|
+
export { PurchaseAlreadyOwnedError, PurchaseBlockedError, RevnixAuthError, RevnixError, RevnixNetworkError, RevnixRateLimitError, RevnixTimeoutError, } from "./errors.js";
|
|
6
6
|
export { purchaseInputFromIap, type IapPurchase } from "./iap.js";
|
|
7
7
|
export { setStoreAdapter, type StoreAdapter, type StoreTransaction, } from "./store.js";
|
|
8
8
|
export { memoryStorage, generateAnonymousId } from "./storage.js";
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.Revnix = exports.PREVIEW_PLACEMENT_KEY = exports.IMPLICIT_PLACEMENTS = exports.HOST_CALL_PLACEMENT = exports.DEFAULT_SESSION_TIMEOUT_MS = exports.browserLifecycle = exports.encodeDeviceHeader = exports.DEVICE_HEADER = exports.SDK_VERSION = exports.generateAnonymousId = exports.memoryStorage = exports.setStoreAdapter = exports.purchaseInputFromIap = exports.RevnixTimeoutError = exports.RevnixRateLimitError = exports.RevnixNetworkError = exports.RevnixError = exports.RevnixAuthError = exports.PurchaseBlockedError = exports.RevnixClient = void 0;
|
|
3
|
+
exports.Revnix = exports.PREVIEW_PLACEMENT_KEY = exports.IMPLICIT_PLACEMENTS = exports.HOST_CALL_PLACEMENT = exports.DEFAULT_SESSION_TIMEOUT_MS = exports.browserLifecycle = exports.encodeDeviceHeader = exports.DEVICE_HEADER = exports.SDK_VERSION = exports.generateAnonymousId = exports.memoryStorage = exports.setStoreAdapter = exports.purchaseInputFromIap = exports.RevnixTimeoutError = exports.RevnixRateLimitError = exports.RevnixNetworkError = exports.RevnixError = exports.RevnixAuthError = exports.PurchaseBlockedError = exports.PurchaseAlreadyOwnedError = exports.RevnixClient = void 0;
|
|
4
4
|
const client_js_1 = require("./client.js");
|
|
5
5
|
const locale_js_1 = require("./blocks/locale.js");
|
|
6
6
|
var client_js_2 = require("./client.js");
|
|
7
7
|
Object.defineProperty(exports, "RevnixClient", { enumerable: true, get: function () { return client_js_2.RevnixClient; } });
|
|
8
8
|
var errors_js_1 = require("./errors.js");
|
|
9
|
+
Object.defineProperty(exports, "PurchaseAlreadyOwnedError", { enumerable: true, get: function () { return errors_js_1.PurchaseAlreadyOwnedError; } });
|
|
9
10
|
Object.defineProperty(exports, "PurchaseBlockedError", { enumerable: true, get: function () { return errors_js_1.PurchaseBlockedError; } });
|
|
10
11
|
Object.defineProperty(exports, "RevnixAuthError", { enumerable: true, get: function () { return errors_js_1.RevnixAuthError; } });
|
|
11
12
|
Object.defineProperty(exports, "RevnixError", { enumerable: true, get: function () { return errors_js_1.RevnixError; } });
|
package/dist/store/index.js
CHANGED
|
@@ -9,7 +9,7 @@ exports.RevnixStore = native
|
|
|
9
9
|
? {
|
|
10
10
|
purchase: (productId, { customerId }) => native.purchase(productId, customerId),
|
|
11
11
|
restore: () => native.restore(),
|
|
12
|
-
finish: (transactionId) => native.finish(transactionId),
|
|
12
|
+
finish: (transactionId, consume) => native.finish(transactionId, consume === true),
|
|
13
13
|
observe(onTransaction) {
|
|
14
14
|
const sub = new react_native_1.NativeEventEmitter(react_native_1.NativeModules.RevnixStore).addListener("revnixStoreTransaction", (tx) => onTransaction(tx));
|
|
15
15
|
return () => sub.remove();
|
package/dist/store.d.ts
CHANGED
|
@@ -12,6 +12,11 @@ export interface StoreTransaction {
|
|
|
12
12
|
expiresAt?: number;
|
|
13
13
|
/** Apple only: the StoreKit 2 JWS. */
|
|
14
14
|
signedTransactionInfo?: string;
|
|
15
|
+
/** Google only: already acknowledged in Play. */
|
|
16
|
+
acknowledged?: boolean;
|
|
17
|
+
/** Google only: purchase() answered with an earlier purchase Play reported
|
|
18
|
+
* as already owned, not a new charge. */
|
|
19
|
+
alreadyOwned?: boolean;
|
|
15
20
|
}
|
|
16
21
|
export interface StoreAdapter {
|
|
17
22
|
/** Resolves null when the customer cancelled or the purchase is pending. */
|
|
@@ -19,7 +24,8 @@ export interface StoreAdapter {
|
|
|
19
24
|
customerId: string;
|
|
20
25
|
}): Promise<StoreTransaction | null>;
|
|
21
26
|
restore(): Promise<StoreTransaction[]>;
|
|
22
|
-
|
|
27
|
+
/** `consume`: Google only, consume instead of acknowledge. */
|
|
28
|
+
finish(transactionId: string, consume?: boolean): Promise<void>;
|
|
23
29
|
/** Transactions arriving outside purchase(): renewals, other devices, Ask
|
|
24
30
|
* to Buy, refunds. Returns an unsubscribe. */
|
|
25
31
|
observe(onTransaction: (tx: StoreTransaction) => void): () => void;
|
package/dist/types.d.ts
CHANGED
|
@@ -149,6 +149,9 @@ export interface RevnixConfig {
|
|
|
149
149
|
* read. A null answer deletes a previously stored `gps_adid`; a
|
|
150
150
|
* rejection changes nothing and reports `op: "advertisingId"`. */
|
|
151
151
|
advertisingId?: () => Promise<string | null | undefined>;
|
|
152
|
+
/** Google Play product ids sold as consumables. A registered purchase of
|
|
153
|
+
* one is consumed instead of acknowledged so it can be bought again. */
|
|
154
|
+
consumableProductIds?: string[];
|
|
152
155
|
}
|
|
153
156
|
/** Device integrity evidence for `POST /v1/installs` and `/v1/attribution`.
|
|
154
157
|
* iOS: an App Attest key id and its attestation (standard base64) over
|
|
@@ -276,7 +276,7 @@ function FallbackClose({ doc, onPress, safeAreaTop, }) {
|
|
|
276
276
|
/** Text blocks always render inside a View: a style may carry both typography
|
|
277
277
|
* and box properties (a round chip is a 34pt circle with a centered glyph),
|
|
278
278
|
* and RN cannot centre a glyph inside <Text> the way flexbox does. */
|
|
279
|
-
function BlockText({ text, style, ctx, inherited, onPress, }) {
|
|
279
|
+
function BlockText({ text, style, ctx, inherited, onPress, pressLabel, }) {
|
|
280
280
|
const styleCtx = { ...ctx.style, fontSize: inherited.fontSize };
|
|
281
281
|
// The block's own typography is folded into the inherited set first, so
|
|
282
282
|
// em letter-spacing and unitless line-height resolve against whichever font
|
|
@@ -291,7 +291,7 @@ function BlockText({ text, style, ctx, inherited, onPress, }) {
|
|
|
291
291
|
// wrapping a styled View would leave a pinned chip resolving its offsets
|
|
292
292
|
// against a zero-sized parent.
|
|
293
293
|
if (onPress) {
|
|
294
|
-
return (React.createElement(react_native_1.Pressable, { accessibilityRole: "button", accessibilityLabel:
|
|
294
|
+
return (React.createElement(react_native_1.Pressable, { accessibilityRole: "button", accessibilityLabel: pressLabel, hitSlop: 8, onPress: onPress, style: (0, style_js_1.toViewStyle)(style, styleCtx) }, body));
|
|
295
295
|
}
|
|
296
296
|
return React.createElement(react_native_1.View, { style: (0, style_js_1.toViewStyle)(style, styleCtx) }, body);
|
|
297
297
|
}
|
|
@@ -330,27 +330,34 @@ function Carousel({ pages, autoAdvance, doc, }) {
|
|
|
330
330
|
setPage(Math.round(e.nativeEvent.contentOffset.x / size.width));
|
|
331
331
|
}
|
|
332
332
|
} }, pages.map((node, i) => (React.createElement(react_native_1.View, { key: i, style: size }, node)))),
|
|
333
|
-
pages.length > 1 ? (React.createElement(
|
|
333
|
+
pages.length > 1 ? (React.createElement(Dots, { count: pages.length, active: page, doc: doc, style: { bottom: 8 } })) : null));
|
|
334
|
+
}
|
|
335
|
+
function Dots({ count, active, doc, style, }) {
|
|
336
|
+
return (React.createElement(react_native_1.View, { pointerEvents: "none", style: [
|
|
337
|
+
{
|
|
334
338
|
position: "absolute",
|
|
335
|
-
bottom: 8,
|
|
336
339
|
left: 0,
|
|
337
340
|
right: 0,
|
|
338
341
|
flexDirection: "row",
|
|
339
342
|
justifyContent: "center",
|
|
340
343
|
gap: 6,
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
344
|
+
},
|
|
345
|
+
style,
|
|
346
|
+
] }, Array.from({ length: count }, (_, i) => (React.createElement(react_native_1.View, { key: i, style: {
|
|
347
|
+
width: 6,
|
|
348
|
+
height: 6,
|
|
349
|
+
borderRadius: 3,
|
|
350
|
+
backgroundColor: i === active ? doc.accent : (0, color_js_1.withAlpha)(doc.textColor, 0.3),
|
|
351
|
+
} })))));
|
|
347
352
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
* image and button blocks cannot drift on what "action: close" means. */
|
|
351
|
-
function closePress(block, ctx) {
|
|
352
|
-
return block.action === "close" && ctx.onClose ? ctx.onClose : undefined;
|
|
353
|
+
function actionPress(block, ctx) {
|
|
354
|
+
return block.action === "close" ? ctx.onClose : ctx.navigate(block.action);
|
|
353
355
|
}
|
|
356
|
+
const ACTION_LABEL = {
|
|
357
|
+
close: "Close",
|
|
358
|
+
next: "Next",
|
|
359
|
+
back: "Back",
|
|
360
|
+
};
|
|
354
361
|
/** Typography that is NOT inherited (decoration) — everything else already
|
|
355
362
|
* arrives through the inherited set. */
|
|
356
363
|
function textOnly(s, ctx) {
|
|
@@ -475,7 +482,7 @@ function Block({ block, ctx, pkg, inherited, selection, }) {
|
|
|
475
482
|
const size = text.fontSize;
|
|
476
483
|
switch (block.type) {
|
|
477
484
|
case "text":
|
|
478
|
-
return (React.createElement(BlockText, { text: (0, tags_js_1.resolveTags)(block.text, pkg, ctx.packages), style: style, ctx: ctx, inherited: inherited, onPress:
|
|
485
|
+
return (React.createElement(BlockText, { text: (0, tags_js_1.resolveTags)(block.text, pkg, ctx.packages), style: style, ctx: ctx, inherited: inherited, onPress: actionPress(block, ctx), pressLabel: block.action === "close" ? "Close" : undefined }));
|
|
479
486
|
case "timer":
|
|
480
487
|
return React.createElement(Timer, { block: block, style: style, ctx: ctx, inherited: inherited });
|
|
481
488
|
case "toggle": {
|
|
@@ -506,12 +513,10 @@ function Block({ block, ctx, pkg, inherited, selection, }) {
|
|
|
506
513
|
s.aspectRatio !== undefined ||
|
|
507
514
|
(s.top !== undefined && s.bottom !== undefined) ||
|
|
508
515
|
s.flex !== undefined);
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
const ImageBox = closeImage ? react_native_1.Pressable : react_native_1.View;
|
|
514
|
-
return (React.createElement(ImageBox, { accessibilityRole: closeImage ? "button" : undefined, accessibilityLabel: closeImage ? "Close" : undefined, hitSlop: closeImage ? 8 : undefined, onPress: closeImage, style: [
|
|
516
|
+
const action = block.type === "image" ? block.action : undefined;
|
|
517
|
+
const imagePress = actionPress({ action }, ctx);
|
|
518
|
+
const ImageBox = imagePress ? react_native_1.Pressable : react_native_1.View;
|
|
519
|
+
return (React.createElement(ImageBox, { accessibilityRole: imagePress ? "button" : undefined, accessibilityLabel: imagePress && action ? ACTION_LABEL[action] : undefined, hitSlop: imagePress ? 8 : undefined, onPress: imagePress, style: [
|
|
515
520
|
sized ? null : { height: 160 },
|
|
516
521
|
{ borderRadius: round ? 9999 : sized ? 0 : 16, overflow: "hidden", flexShrink: 0 },
|
|
517
522
|
view,
|
|
@@ -552,12 +557,13 @@ function Block({ block, ctx, pkg, inherited, selection, }) {
|
|
|
552
557
|
// A button the design marks as the close dismisses instead of buying,
|
|
553
558
|
// and takes no accent fill: the CTA must stay the one accented thing on
|
|
554
559
|
// the screen, or a "Not now" competes with "Subscribe" for the eye.
|
|
555
|
-
const close =
|
|
560
|
+
const close = block.action === "close" ? ctx.onClose : undefined;
|
|
561
|
+
const navigates = block.action === "next" || block.action === "back";
|
|
556
562
|
// A purchase in flight: the button keeps its box and fill, loses its
|
|
557
563
|
// label to a spinner in the ink colour, and ignores taps. A close
|
|
558
564
|
// button is never blocked — the user can always leave.
|
|
559
|
-
const busy = !close && ctx.loading;
|
|
560
|
-
return (React.createElement(react_native_1.Pressable, { accessibilityRole: "button", accessibilityState: { disabled: busy, busy }, disabled: busy, onPress: close ?? ctx.onPurchase, style: ({ pressed }) => [
|
|
565
|
+
const busy = !close && !navigates && ctx.loading;
|
|
566
|
+
return (React.createElement(react_native_1.Pressable, { accessibilityRole: "button", accessibilityState: { disabled: busy, busy }, disabled: busy, onPress: navigates ? actionPress(block, ctx) : (close ?? ctx.onPurchase), style: ({ pressed }) => [
|
|
561
567
|
{
|
|
562
568
|
backgroundColor: close ? "transparent" : doc.accent,
|
|
563
569
|
borderRadius: 12,
|
|
@@ -714,6 +720,13 @@ function RevnixBlockPaywall(props) {
|
|
|
714
720
|
const doc = React.useMemo(() => (0, locale_js_1.localizeBlockDoc)(props.doc, props.locale ?? (0, locale_js_1.detectLocale)()), [props.doc, props.locale]);
|
|
715
721
|
const [internalSelected, setInternalSelected] = React.useState(undefined);
|
|
716
722
|
const [shownAt] = React.useState(Date.now);
|
|
723
|
+
const [screenIndex, setScreenIndex] = React.useState(0);
|
|
724
|
+
const [screenDoc, setScreenDoc] = React.useState(props.doc);
|
|
725
|
+
if (screenDoc !== props.doc) {
|
|
726
|
+
setScreenDoc(props.doc);
|
|
727
|
+
setScreenIndex(0);
|
|
728
|
+
}
|
|
729
|
+
const screen = (0, types_js_1.onboardingScreen)(doc, screenIndex);
|
|
717
730
|
// Contract §1: host → own tap → design highlight → first package, each
|
|
718
731
|
// only when the offering carries it.
|
|
719
732
|
const selectedId = (0, selection_js_1.selectedPackageId)(packages, props.selectedPackageId, internalSelected, props.highlightPackageId);
|
|
@@ -749,6 +762,10 @@ function RevnixBlockPaywall(props) {
|
|
|
749
762
|
onTerms: props.onTerms,
|
|
750
763
|
onPrivacy: props.onPrivacy,
|
|
751
764
|
onClose: props.onClose,
|
|
765
|
+
navigate: (action) => {
|
|
766
|
+
const to = (0, types_js_1.stepScreen)(doc, screenIndex, action);
|
|
767
|
+
return to === undefined ? undefined : () => setScreenIndex(to);
|
|
768
|
+
},
|
|
752
769
|
footer: props.footer,
|
|
753
770
|
heroImageUrl: props.heroImageUrl,
|
|
754
771
|
gradient: props.gradient,
|
|
@@ -757,14 +774,14 @@ function RevnixBlockPaywall(props) {
|
|
|
757
774
|
loading: props.loading === true,
|
|
758
775
|
shownAt,
|
|
759
776
|
};
|
|
760
|
-
const children =
|
|
777
|
+
const children = screen.blocks.map((block) => (React.createElement(Block, { key: block.id, block: block, ctx: ctx, pkg: packages.find((p) => p.packageId === selectedId) ?? packages[0], inherited: rootText })));
|
|
761
778
|
// REV-252: the renderer's own close, drawn only for a design that authors
|
|
762
779
|
// none. Rendered as a sibling of the tree — outside the canvas scale — so
|
|
763
780
|
// it keeps its tap size and its distance from the screen edge whatever the
|
|
764
781
|
// device width does to the design.
|
|
765
782
|
const safeAreaTop = props.safeAreaTop ??
|
|
766
783
|
(react_native_1.Platform.OS === "android" ? (react_native_1.StatusBar.currentHeight ?? 0) : 0);
|
|
767
|
-
const fallbackClose = props.onClose && !(0, types_js_1.hasCloseAction)(
|
|
784
|
+
const fallbackClose = props.onClose && !(0, types_js_1.hasCloseAction)(screen.blocks) ? (React.createElement(FallbackClose, { doc: doc, onPress: props.onClose, safeAreaTop: safeAreaTop })) : null;
|
|
768
785
|
const [width, setWidth] = React.useState(null);
|
|
769
786
|
const [viewportHeight, setViewportHeight] = React.useState(null);
|
|
770
787
|
if (doc.layout === "canvas") {
|
|
@@ -785,7 +802,7 @@ function RevnixBlockPaywall(props) {
|
|
|
785
802
|
const canvasHeight = metrics?.scaledHeight ?? types_js_1.DESIGN_CANVAS.height;
|
|
786
803
|
const left = metrics?.left ?? 0;
|
|
787
804
|
return (React.createElement(ScreenBackground, { doc: doc, gradient: props.gradient, blur: props.blur, onDiagnostic: props.onDiagnostic, style: [{ flex: 1 }, props.style] },
|
|
788
|
-
React.createElement(react_native_1.ScrollView, { contentContainerStyle: { height: canvasHeight },
|
|
805
|
+
React.createElement(react_native_1.ScrollView, { key: screenIndex, contentContainerStyle: { height: canvasHeight },
|
|
789
806
|
// A designed screen never shows a scrollbar — the web preview has
|
|
790
807
|
// none, and in an RTL app the indicator lands on the left edge of
|
|
791
808
|
// the artwork. When nothing overflows, scrolling is off entirely so
|
|
@@ -809,15 +826,18 @@ function RevnixBlockPaywall(props) {
|
|
|
809
826
|
{ translateY: -((canvasHeight / scale) * (1 - scale)) / 2 },
|
|
810
827
|
{ scale },
|
|
811
828
|
],
|
|
812
|
-
} },
|
|
829
|
+
} },
|
|
830
|
+
children,
|
|
831
|
+
screen.dots ? (React.createElement(Dots, { count: screen.dots.count, active: screen.dots.active, doc: doc, style: { top: 20, zIndex: 50 } })) : null))),
|
|
813
832
|
fallbackClose));
|
|
814
833
|
}
|
|
815
834
|
return (React.createElement(ScreenBackground, { doc: doc, gradient: props.gradient, blur: props.blur, onDiagnostic: props.onDiagnostic, style: [{ flex: 1 }, props.style] },
|
|
816
|
-
React.createElement(react_native_1.ScrollView, { contentContainerStyle: {
|
|
835
|
+
React.createElement(react_native_1.ScrollView, { key: screenIndex, contentContainerStyle: {
|
|
817
836
|
gap: 12,
|
|
818
837
|
paddingHorizontal: 20,
|
|
819
838
|
paddingTop: 18,
|
|
820
839
|
paddingBottom: 30,
|
|
821
840
|
}, showsVerticalScrollIndicator: false, alwaysBounceVertical: false, overScrollMode: "never" }, children),
|
|
841
|
+
screen.dots ? (React.createElement(Dots, { count: screen.dots.count, active: screen.dots.active, doc: doc, style: { top: 20 + safeAreaTop, zIndex: 50 } })) : null,
|
|
822
842
|
fallbackClose));
|
|
823
843
|
}
|
package/dist/ui/RevnixPaywall.js
CHANGED
|
@@ -76,7 +76,16 @@ function asBlockDoc(value) {
|
|
|
76
76
|
typeof doc.textColor === "string" &&
|
|
77
77
|
typeof doc.accent === "string" &&
|
|
78
78
|
typeof doc.accentInk === "string"
|
|
79
|
-
?
|
|
79
|
+
? {
|
|
80
|
+
...doc,
|
|
81
|
+
...(doc.screens !== undefined
|
|
82
|
+
? {
|
|
83
|
+
screens: Array.isArray(doc.screens)
|
|
84
|
+
? doc.screens.filter((s) => Array.isArray(s) && s.length > 0)
|
|
85
|
+
: [],
|
|
86
|
+
}
|
|
87
|
+
: {}),
|
|
88
|
+
}
|
|
80
89
|
: null;
|
|
81
90
|
}
|
|
82
91
|
exports.DEFAULT_PAYWALL_THEME = {
|
|
@@ -289,7 +298,7 @@ function RevnixPaywall(props) {
|
|
|
289
298
|
// arrives after a placeholder (or is swapped by an A/B assignment) would
|
|
290
299
|
// otherwise crash on "rendered more hooks than during the previous render".
|
|
291
300
|
const { config } = props;
|
|
292
|
-
const blocks = asBlockDoc(config.blocks);
|
|
301
|
+
const blocks = React.useMemo(() => asBlockDoc(config.blocks), [config.blocks]);
|
|
293
302
|
// Both sinks fire when both are wired; nil when neither is, which is what
|
|
294
303
|
// stops the renderer building a message nobody will read.
|
|
295
304
|
const renderDiagnostic = client?.reportRenderDiagnostic || props.onDiagnostic
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const SDK_VERSION = "1.
|
|
1
|
+
export declare const SDK_VERSION = "1.9.0";
|
package/dist/version.js
CHANGED
|
@@ -4,4 +4,4 @@ exports.SDK_VERSION = void 0;
|
|
|
4
4
|
// L3: the single source of the SDK version inside src/. Must match
|
|
5
5
|
// package.json "version" — version.test.ts fails the build when they drift
|
|
6
6
|
// (they had: client.ts said 0.7.0 while the package shipped 0.9.0).
|
|
7
|
-
exports.SDK_VERSION = "1.
|
|
7
|
+
exports.SDK_VERSION = "1.9.0";
|
package/ios/RevnixStore.m
CHANGED
|
@@ -12,6 +12,7 @@ RCT_EXTERN_METHOD(restore:(RCTPromiseResolveBlock)resolve
|
|
|
12
12
|
rejecter:(RCTPromiseRejectBlock)reject)
|
|
13
13
|
|
|
14
14
|
RCT_EXTERN_METHOD(finish:(NSString *)transactionId
|
|
15
|
+
consume:(BOOL)consume
|
|
15
16
|
resolver:(RCTPromiseResolveBlock)resolve
|
|
16
17
|
rejecter:(RCTPromiseRejectBlock)reject)
|
|
17
18
|
|
package/ios/RevnixStore.swift
CHANGED
|
@@ -20,7 +20,7 @@ class RevnixStore: RCTEventEmitter {
|
|
|
20
20
|
override func startObserving() {
|
|
21
21
|
updates?.cancel()
|
|
22
22
|
updates = Task { [weak self] in
|
|
23
|
-
for await update in Transaction.updates
|
|
23
|
+
for await update in Transaction.updates {
|
|
24
24
|
self?.sendEvent(withName: "revnixStoreTransaction", body: RevnixStore.payload(update))
|
|
25
25
|
}
|
|
26
26
|
}
|
|
@@ -72,9 +72,10 @@ class RevnixStore: RCTEventEmitter {
|
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
@objc(finish:resolver:rejecter:)
|
|
75
|
+
@objc(finish:consume:resolver:rejecter:)
|
|
76
76
|
func finish(
|
|
77
77
|
_ transactionId: String,
|
|
78
|
+
consume _: Bool,
|
|
78
79
|
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
79
80
|
rejecter reject: @escaping RCTPromiseRejectBlock
|
|
80
81
|
) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-revnix",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "Revnix SDK for React, React Native & Next.js: purchase registration, entitlements, identity, and placements over the Revnix /v1 API.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
package/src/blocks/locale.ts
CHANGED
|
@@ -193,6 +193,13 @@ export function localizeBlockDoc(
|
|
|
193
193
|
...doc,
|
|
194
194
|
defaultLocale: chain[0],
|
|
195
195
|
blocks: doc.blocks.map((b) => localizeBlock(b, lookup)),
|
|
196
|
+
...(doc.screens
|
|
197
|
+
? {
|
|
198
|
+
screens: doc.screens.map((screen) =>
|
|
199
|
+
screen.map((b) => localizeBlock(b, lookup))
|
|
200
|
+
),
|
|
201
|
+
}
|
|
202
|
+
: {}),
|
|
196
203
|
};
|
|
197
204
|
}
|
|
198
205
|
|