react-native-revnix 1.5.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.
Files changed (49) 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 +7 -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 +45 -3
  11. package/dist/blocks/types.js +1 -0
  12. package/dist/client.d.ts +50 -11
  13. package/dist/client.js +408 -119
  14. package/dist/esm/blocks/locale.js +7 -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 +1 -0
  18. package/dist/esm/client.js +407 -119
  19. package/dist/esm/store/index.js +7 -0
  20. package/dist/esm/ui/RevnixBlockPaywall.js +103 -22
  21. package/dist/esm/ui/RevnixPaywall.js +7 -0
  22. package/dist/esm/ui/index.js +1 -1
  23. package/dist/esm/version.js +1 -1
  24. package/dist/index.d.ts +1 -1
  25. package/dist/store/index.d.ts +8 -0
  26. package/dist/store/index.js +10 -1
  27. package/dist/types.d.ts +32 -0
  28. package/dist/ui/RevnixBlockPaywall.js +100 -19
  29. package/dist/ui/RevnixPaywall.d.ts +6 -2
  30. package/dist/ui/RevnixPaywall.js +8 -0
  31. package/dist/ui/index.d.ts +1 -1
  32. package/dist/ui/index.js +2 -1
  33. package/dist/version.d.ts +1 -1
  34. package/dist/version.js +1 -1
  35. package/ios/RevnixStore.m +4 -0
  36. package/ios/RevnixStore.swift +29 -0
  37. package/package.json +1 -1
  38. package/src/blocks/locale.ts +7 -0
  39. package/src/blocks/selection.ts +13 -0
  40. package/src/blocks/tags.ts +38 -0
  41. package/src/blocks/types.ts +51 -2
  42. package/src/client.ts +478 -154
  43. package/src/index.ts +1 -0
  44. package/src/store/index.ts +15 -0
  45. package/src/types.ts +30 -0
  46. package/src/ui/RevnixBlockPaywall.tsx +186 -20
  47. package/src/ui/RevnixPaywall.tsx +15 -1
  48. package/src/ui/index.ts +1 -0
  49. package/src/version.ts +1 -1
@@ -1,3 +1,5 @@
1
+ import CryptoKit
2
+ import DeviceCheck
1
3
  import Foundation
2
4
  import React
3
5
  import StoreKit
@@ -88,6 +90,33 @@ class RevnixStore: RCTEventEmitter {
88
90
  }
89
91
  }
90
92
 
93
+ @objc(deviceIntegrity:resolver:rejecter:)
94
+ func deviceIntegrity(
95
+ _ customerId: String,
96
+ resolver resolve: @escaping RCTPromiseResolveBlock,
97
+ rejecter reject: @escaping RCTPromiseRejectBlock
98
+ ) {
99
+ let service = DCAppAttestService.shared
100
+ guard service.isSupported else {
101
+ resolve(nil)
102
+ return
103
+ }
104
+ Task {
105
+ do {
106
+ let keyId = try await service.generateKey()
107
+ let clientDataHash = Data(SHA256.hash(data: Data(customerId.utf8)))
108
+ let attestation = try await service.attestKey(keyId, clientDataHash: clientDataHash)
109
+ resolve([
110
+ "platform": "ios",
111
+ "keyId": keyId,
112
+ "attestation": attestation.base64EncodedString(),
113
+ ])
114
+ } catch {
115
+ resolve(nil)
116
+ }
117
+ }
118
+ }
119
+
91
120
  private static func payload(_ verification: VerificationResult<Transaction>) -> [String: Any] {
92
121
  let transaction = verification.unsafePayloadValue
93
122
  var payload: [String: Any] = [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-revnix",
3
- "version": "1.5.0",
3
+ "version": "1.7.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": {
@@ -125,7 +125,14 @@ function localizeBlock(
125
125
  case "text":
126
126
  return { ...block, text: at("text", block.text) };
127
127
  case "button":
128
+ case "toggle":
128
129
  return { ...block, label: at("label", block.label) };
130
+ case "timer":
131
+ return {
132
+ ...block,
133
+ format: opt("format", block.format),
134
+ expiredText: opt("expiredText", block.expiredText),
135
+ };
129
136
  case "list":
130
137
  return {
131
138
  ...block,
@@ -11,6 +11,7 @@ import type {
11
11
  CardBlock,
12
12
  PaywallBlock,
13
13
  PaywallBlockDoc,
14
+ ToggleBlock,
14
15
  } from "./types.js";
15
16
  import { DESIGN_CANVAS } from "./types.js";
16
17
  import { resolveTags } from "./tags.js";
@@ -33,6 +34,18 @@ export function selectedPackageId(
33
34
  return packages[0]?.packageId;
34
35
  }
35
36
 
37
+ export function toggleSelection(
38
+ block: ToggleBlock,
39
+ packages: ReadonlyArray<{ packageId: string }>,
40
+ selectedId: string | undefined
41
+ ): { on: boolean; target: string } | null {
42
+ const on = packages[block.onPackageIndex];
43
+ const off = packages[block.offPackageIndex];
44
+ if (on === undefined || off === undefined) return null;
45
+ const isOn = on.packageId === selectedId;
46
+ return { on: isOn, target: (isOn ? off : on).packageId };
47
+ }
48
+
36
49
  /** The context a package-bearing card opens for itself and its subtree. */
37
50
  export function packageContext(
38
51
  pkg: { packageId: string } | undefined,
@@ -11,6 +11,7 @@
11
11
  // approximated — a wrong price must never reach a customer silently.
12
12
 
13
13
  import type { RevnixPaywallPackage } from "../ui/RevnixPaywall.js";
14
+ import type { TimerBlock } from "./types.js";
14
15
 
15
16
  /** Renewal cycles, in months. Lifetime and one-time products have no cycle. */
16
17
  const MONTHS: Record<string, number | undefined> = {
@@ -123,3 +124,40 @@ export function resolveTags(
123
124
  }
124
125
  });
125
126
  }
127
+
128
+ export function formatCountdown(format: string, remaining: number): string {
129
+ const pad = (n: number) => String(n).padStart(2, "0");
130
+ let span = Infinity;
131
+ const part = (token: string, unit: number) => {
132
+ const value = Math.floor((remaining % span) / unit);
133
+ if (format.includes(token)) span = unit;
134
+ return value;
135
+ };
136
+ const days = part("{d}", 86400);
137
+ const hours = part("{hh}", 3600);
138
+ const minutes = part("{mm}", 60);
139
+ const seconds = part("{ss}", 1);
140
+ return format
141
+ .replace(/\{d\}/g, String(days))
142
+ .replace(/\{hh\}/g, pad(hours))
143
+ .replace(/\{mm\}/g, pad(minutes))
144
+ .replace(/\{ss\}/g, pad(seconds));
145
+ }
146
+
147
+ export function timerText(
148
+ block: TimerBlock,
149
+ shownAt: number,
150
+ now: number,
151
+ ): string | null {
152
+ const deadline =
153
+ block.endsAt !== undefined
154
+ ? Date.parse(block.endsAt)
155
+ : shownAt + (block.duration ?? 0) * 1000;
156
+ const remaining = Number.isNaN(deadline)
157
+ ? 0
158
+ : Math.max(0, Math.floor((deadline - now) / 1000));
159
+ if (remaining > 0) {
160
+ return formatCountdown(block.format ?? "{hh}:{mm}:{ss}", remaining);
161
+ }
162
+ return block.expiredText ?? null;
163
+ }
@@ -257,13 +257,58 @@ export interface SpacerBlock {
257
257
  visibility?: BlockVisibility;
258
258
  }
259
259
 
260
+ /** Countdown text. `endsAt` is one deadline for everyone; `duration` counts
261
+ * from the moment this paywall view opened. At zero it draws `expiredText`,
262
+ * or nothing when that is absent. Tokens in `format`: {d} {hh} {mm} {ss}. */
263
+ export interface TimerBlock {
264
+ id: string;
265
+ type: "timer";
266
+ endsAt?: string;
267
+ duration?: number;
268
+ format?: string;
269
+ expiredText?: string;
270
+ style?: BlockStyle;
271
+ selectedStyle?: BlockStyle;
272
+ visibility?: BlockVisibility;
273
+ }
274
+
275
+ /** A muted looping clip. React Native has no built-in player, so this SDK
276
+ * draws `poster` in its place, sized like an image block. */
277
+ export interface VideoBlock {
278
+ id: string;
279
+ type: "video";
280
+ url: string;
281
+ poster?: string;
282
+ fit?: "cover" | "contain";
283
+ style?: BlockStyle;
284
+ selectedStyle?: BlockStyle;
285
+ visibility?: BlockVisibility;
286
+ }
287
+
288
+ /** The free-trial switch: ON while package `onPackageIndex` is selected, and
289
+ * tapping it selects the other one. Hidden when the offering lacks either
290
+ * index. */
291
+ export interface ToggleBlock {
292
+ id: string;
293
+ type: "toggle";
294
+ label: string;
295
+ onPackageIndex: number;
296
+ offPackageIndex: number;
297
+ style?: BlockStyle;
298
+ selectedStyle?: BlockStyle;
299
+ visibility?: BlockVisibility;
300
+ }
301
+
260
302
  /** The one container block. `layout` picks how children are placed: column /
261
303
  * row are flex lines, `stack` layers them (children position with
262
- * style.inset or the edge offsets), and `grid` is an N-column grid. */
304
+ * style.inset or the edge offsets), `grid` is an N-column grid, and
305
+ * `carousel` pages through its children one swipe at a time. */
263
306
  export interface CardBlock {
264
307
  id: string;
265
308
  type: "card";
266
- layout?: "column" | "row" | "stack" | "grid";
309
+ layout?: "column" | "row" | "stack" | "grid" | "carousel";
310
+ /** carousel only: seconds per page before it moves on by itself. */
311
+ autoAdvance?: number;
267
312
  /** Renders this container once per package in the attached offering. Its
268
313
  * subtree reads the package through tag variables. */
269
314
  repeat?: "packages";
@@ -292,6 +337,9 @@ export type PaywallBlock =
292
337
  | LinksBlock
293
338
  | LineBlock
294
339
  | SpacerBlock
340
+ | TimerBlock
341
+ | VideoBlock
342
+ | ToggleBlock
295
343
  | CardBlock;
296
344
 
297
345
  /** The published document: screen palette plus the block tree. */
@@ -345,6 +393,7 @@ export function hasCloseAction(blocks: PaywallBlock[]): boolean {
345
393
  : b.type === "card"
346
394
  ? b.repeat === undefined &&
347
395
  b.packageIndex === undefined &&
396
+ b.layout !== "carousel" &&
348
397
  hasCloseAction(b.children)
349
398
  : (b.type === "text" || b.type === "image" || b.type === "button") &&
350
399
  b.action === "close"