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.
Files changed (44) hide show
  1. package/README.md +16 -0
  2. package/android/build.gradle +3 -0
  3. package/android/consumer-rules.pro +1 -0
  4. package/android/src/main/kotlin/com/revnix/reactnative/RevnixStoreModule.kt +43 -0
  5. package/dist/blocks/locale.js +12 -0
  6. package/dist/blocks/selection.d.ts +7 -1
  7. package/dist/blocks/selection.js +9 -0
  8. package/dist/blocks/tags.d.ts +3 -0
  9. package/dist/blocks/tags.js +33 -0
  10. package/dist/blocks/types.d.ts +65 -8
  11. package/dist/blocks/types.js +21 -0
  12. package/dist/client.d.ts +4 -0
  13. package/dist/client.js +73 -5
  14. package/dist/esm/blocks/locale.js +12 -0
  15. package/dist/esm/blocks/selection.js +8 -0
  16. package/dist/esm/blocks/tags.js +31 -0
  17. package/dist/esm/blocks/types.js +19 -0
  18. package/dist/esm/client.js +73 -5
  19. package/dist/esm/store/index.js +7 -0
  20. package/dist/esm/ui/RevnixBlockPaywall.js +145 -44
  21. package/dist/esm/ui/RevnixPaywall.js +11 -2
  22. package/dist/esm/version.js +1 -1
  23. package/dist/index.d.ts +1 -1
  24. package/dist/store/index.d.ts +8 -0
  25. package/dist/store/index.js +10 -1
  26. package/dist/types.d.ts +32 -0
  27. package/dist/ui/RevnixBlockPaywall.js +141 -40
  28. package/dist/ui/RevnixPaywall.js +11 -2
  29. package/dist/version.d.ts +1 -1
  30. package/dist/version.js +1 -1
  31. package/ios/RevnixStore.m +4 -0
  32. package/ios/RevnixStore.swift +29 -0
  33. package/package.json +1 -1
  34. package/src/blocks/locale.ts +14 -0
  35. package/src/blocks/selection.ts +13 -0
  36. package/src/blocks/tags.ts +38 -0
  37. package/src/blocks/types.ts +86 -7
  38. package/src/client.ts +90 -8
  39. package/src/index.ts +1 -0
  40. package/src/store/index.ts +15 -0
  41. package/src/types.ts +30 -0
  42. package/src/ui/RevnixBlockPaywall.tsx +265 -42
  43. package/src/ui/RevnixPaywall.tsx +11 -2
  44. 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="Close"
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
- /** The press handler for a block the design marks as the close, or undefined
673
- * when it is not one (or the host wired no dismissal). One helper so text,
674
- * image and button blocks cannot drift on what "action: close" means. */
675
- function closePress(
676
- block: { action?: "close" },
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" && ctx.onClose ? ctx.onClose : undefined;
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={closePress(block, ctx)}
1012
+ onPress={actionPress(block, ctx)}
1013
+ pressLabel={block.action === "close" ? "Close" : undefined}
876
1014
  />
877
1015
  );
878
1016
 
879
- case "image": {
880
- const url = block.url || ctx.heroImageUrl;
881
- const round = block.shape === "circle";
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
- // An icon the design marks as the close (REV-252). Same rule as text:
893
- // the whole box is the target, and the style lands on the Pressable so
894
- // a pinned icon keeps its offsets.
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={closeImage ? "button" : undefined}
900
- accessibilityLabel={closeImage ? "Close" : undefined}
901
- hitSlop={closeImage ? 8 : undefined}
902
- onPress={closeImage}
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 = closePress(block, ctx);
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
- : layout === "grid"
1108
- ? {
1109
- position: "relative",
1110
- flexDirection: "row",
1111
- flexWrap: "wrap",
1112
- gap: style?.gap ?? 10,
1113
- }
1114
- : {
1115
- position: "relative",
1116
- flexDirection: layout === "row" ? "row" : "column",
1117
- gap: style?.gap ?? 10,
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
- {renderChildren(cardPackage, context)}
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 = doc.blocks.map((block) => (
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(doc.blocks) ? (
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
  );
@@ -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
- ? (doc as PaywallBlockDoc)
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.6.0";
4
+ export const SDK_VERSION = "1.8.0";