@rocapine/rocalytics-sdk 0.0.0-stage → 0.3.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 (161) hide show
  1. package/CHANGELOG.md +86 -0
  2. package/LICENSE +21 -0
  3. package/README.md +449 -2
  4. package/client/package.json +1 -0
  5. package/core/package.json +4 -0
  6. package/dist/client/client.d.ts +146 -0
  7. package/dist/client/client.d.ts.map +1 -0
  8. package/dist/client/client.js +376 -0
  9. package/dist/client/client.js.map +1 -0
  10. package/dist/client/index.d.ts +13 -0
  11. package/dist/client/index.d.ts.map +1 -0
  12. package/dist/client/index.js +35 -0
  13. package/dist/client/index.js.map +1 -0
  14. package/dist/client/native.d.ts +68 -0
  15. package/dist/client/native.d.ts.map +1 -0
  16. package/dist/client/native.js +64 -0
  17. package/dist/client/native.js.map +1 -0
  18. package/dist/client/onboardingSink.d.ts +63 -0
  19. package/dist/client/onboardingSink.d.ts.map +1 -0
  20. package/dist/client/onboardingSink.js +126 -0
  21. package/dist/client/onboardingSink.js.map +1 -0
  22. package/dist/client/paywallSink.d.ts +13 -0
  23. package/dist/client/paywallSink.d.ts.map +1 -0
  24. package/dist/client/paywallSink.js +26 -0
  25. package/dist/client/paywallSink.js.map +1 -0
  26. package/dist/client/requests.d.ts +66 -0
  27. package/dist/client/requests.d.ts.map +1 -0
  28. package/dist/client/requests.js +90 -0
  29. package/dist/client/requests.js.map +1 -0
  30. package/dist/client/types.d.ts +180 -0
  31. package/dist/client/types.d.ts.map +1 -0
  32. package/dist/client/types.js +4 -0
  33. package/dist/client/types.js.map +1 -0
  34. package/dist/core/context.d.ts +39 -0
  35. package/dist/core/context.d.ts.map +1 -0
  36. package/dist/core/context.js +45 -0
  37. package/dist/core/context.js.map +1 -0
  38. package/dist/core/delivery.d.ts +63 -0
  39. package/dist/core/delivery.d.ts.map +1 -0
  40. package/dist/core/delivery.js +133 -0
  41. package/dist/core/delivery.js.map +1 -0
  42. package/dist/core/diagnostics.d.ts +16 -0
  43. package/dist/core/diagnostics.d.ts.map +1 -0
  44. package/dist/core/diagnostics.js +24 -0
  45. package/dist/core/diagnostics.js.map +1 -0
  46. package/dist/core/httpSink.d.ts +35 -0
  47. package/dist/core/httpSink.d.ts.map +1 -0
  48. package/dist/core/httpSink.js +65 -0
  49. package/dist/core/httpSink.js.map +1 -0
  50. package/dist/core/ids.d.ts +16 -0
  51. package/dist/core/ids.d.ts.map +1 -0
  52. package/dist/core/ids.js +51 -0
  53. package/dist/core/ids.js.map +1 -0
  54. package/dist/core/index.d.ts +17 -0
  55. package/dist/core/index.d.ts.map +1 -0
  56. package/dist/core/index.js +32 -0
  57. package/dist/core/index.js.map +1 -0
  58. package/dist/core/sink.d.ts +39 -0
  59. package/dist/core/sink.d.ts.map +1 -0
  60. package/dist/core/sink.js +16 -0
  61. package/dist/core/sink.js.map +1 -0
  62. package/dist/core/storage.d.ts +52 -0
  63. package/dist/core/storage.d.ts.map +1 -0
  64. package/dist/core/storage.js +79 -0
  65. package/dist/core/storage.js.map +1 -0
  66. package/dist/core/text.d.ts +9 -0
  67. package/dist/core/text.d.ts.map +1 -0
  68. package/dist/core/text.js +47 -0
  69. package/dist/core/text.js.map +1 -0
  70. package/dist/core/time.d.ts +18 -0
  71. package/dist/core/time.d.ts.map +1 -0
  72. package/dist/core/time.js +18 -0
  73. package/dist/core/time.js.map +1 -0
  74. package/dist/onboarding/contract.d.ts +138 -0
  75. package/dist/onboarding/contract.d.ts.map +1 -0
  76. package/dist/onboarding/contract.js +11 -0
  77. package/dist/onboarding/contract.js.map +1 -0
  78. package/dist/onboarding/index.d.ts +15 -0
  79. package/dist/onboarding/index.d.ts.map +1 -0
  80. package/dist/onboarding/index.js +22 -0
  81. package/dist/onboarding/index.js.map +1 -0
  82. package/dist/onboarding/input.d.ts +102 -0
  83. package/dist/onboarding/input.d.ts.map +1 -0
  84. package/dist/onboarding/input.js +216 -0
  85. package/dist/onboarding/input.js.map +1 -0
  86. package/dist/onboarding/persisted.d.ts +20 -0
  87. package/dist/onboarding/persisted.d.ts.map +1 -0
  88. package/dist/onboarding/persisted.js +75 -0
  89. package/dist/onboarding/persisted.js.map +1 -0
  90. package/dist/onboarding/state.d.ts +65 -0
  91. package/dist/onboarding/state.d.ts.map +1 -0
  92. package/dist/onboarding/state.js +207 -0
  93. package/dist/onboarding/state.js.map +1 -0
  94. package/dist/onboarding/tracker.d.ts +135 -0
  95. package/dist/onboarding/tracker.d.ts.map +1 -0
  96. package/dist/onboarding/tracker.js +734 -0
  97. package/dist/onboarding/tracker.js.map +1 -0
  98. package/dist/paywall/contract.d.ts +67 -0
  99. package/dist/paywall/contract.d.ts.map +1 -0
  100. package/dist/paywall/contract.js +3 -0
  101. package/dist/paywall/contract.js.map +1 -0
  102. package/dist/paywall/index.d.ts +14 -0
  103. package/dist/paywall/index.d.ts.map +1 -0
  104. package/dist/paywall/index.js +19 -0
  105. package/dist/paywall/index.js.map +1 -0
  106. package/dist/paywall/observer.d.ts +38 -0
  107. package/dist/paywall/observer.d.ts.map +1 -0
  108. package/dist/paywall/observer.js +6 -0
  109. package/dist/paywall/observer.js.map +1 -0
  110. package/dist/paywall/state.d.ts +26 -0
  111. package/dist/paywall/state.d.ts.map +1 -0
  112. package/dist/paywall/state.js +132 -0
  113. package/dist/paywall/state.js.map +1 -0
  114. package/dist/paywall/tracker.d.ts +36 -0
  115. package/dist/paywall/tracker.d.ts.map +1 -0
  116. package/dist/paywall/tracker.js +180 -0
  117. package/dist/paywall/tracker.js.map +1 -0
  118. package/dist/version.d.ts +3 -0
  119. package/dist/version.d.ts.map +1 -0
  120. package/dist/version.js +6 -0
  121. package/dist/version.js.map +1 -0
  122. package/docs/onboarding-run-contract.md +1013 -0
  123. package/docs/onboarding-run.examples.ts +306 -0
  124. package/docs/onboarding-run.schema.json +224 -0
  125. package/docs/onboarding-run.types.ts +144 -0
  126. package/docs/paywall-presentation-contract.md +139 -0
  127. package/docs/paywall-presentation.examples.ts +42 -0
  128. package/docs/paywall-presentation.schema.json +268 -0
  129. package/docs/paywall-presentation.types.ts +73 -0
  130. package/onboarding/package.json +4 -0
  131. package/package.json +120 -4
  132. package/paywall/package.json +1 -0
  133. package/src/client/client.ts +417 -0
  134. package/src/client/index.ts +54 -0
  135. package/src/client/native.ts +109 -0
  136. package/src/client/onboardingSink.ts +121 -0
  137. package/src/client/paywallSink.ts +25 -0
  138. package/src/client/requests.ts +126 -0
  139. package/src/client/types.ts +198 -0
  140. package/src/core/context.ts +69 -0
  141. package/src/core/delivery.ts +181 -0
  142. package/src/core/diagnostics.ts +30 -0
  143. package/src/core/httpSink.ts +70 -0
  144. package/src/core/ids.ts +46 -0
  145. package/src/core/index.ts +20 -0
  146. package/src/core/sink.ts +46 -0
  147. package/src/core/storage.ts +112 -0
  148. package/src/core/text.ts +37 -0
  149. package/src/core/time.ts +26 -0
  150. package/src/onboarding/contract.ts +144 -0
  151. package/src/onboarding/index.ts +39 -0
  152. package/src/onboarding/input.ts +246 -0
  153. package/src/onboarding/persisted.ts +93 -0
  154. package/src/onboarding/state.ts +233 -0
  155. package/src/onboarding/tracker.ts +855 -0
  156. package/src/paywall/contract.ts +73 -0
  157. package/src/paywall/index.ts +36 -0
  158. package/src/paywall/observer.ts +43 -0
  159. package/src/paywall/state.ts +125 -0
  160. package/src/paywall/tracker.ts +200 -0
  161. package/src/version.ts +2 -0
@@ -0,0 +1,139 @@
1
+ # Paywall presentation contract
2
+
3
+ Version 1 (`schema_version: 1`). This document is normative. The [JSON Schema](paywall-presentation.schema.json) and the [TypeScript types](paywall-presentation.types.ts) implement it, and [examples](paywall-presentation.examples.ts) illustrate it. Where they disagree, this text wins and the others are bugs.
4
+
5
+ A **presentation** is one paywall shown to one device: from the moment a paywall is resolved for display until it closes. The tracker in `@rocapine/rocalytics-sdk/paywall` sends it as up to three snapshots. Its envelope (`schema_version`, `seq`, `sent_at`, `context`) is the [onboarding run contract](onboarding-run-contract.md)'s, unchanged.
6
+
7
+ ## Contents
8
+
9
+ 1. [Decisions](#1-decisions)
10
+ 2. [A complete payload](#2-a-complete-payload)
11
+ 3. [Fields](#3-fields)
12
+ 4. [Rules beyond the schema](#4-rules-beyond-the-schema)
13
+ 5. [Transport](#5-transport)
14
+ 6. [Measures](#6-measures)
15
+ 7. [Versioning](#7-versioning)
16
+
17
+ ## 1. Decisions
18
+
19
+ | # | Topic | Decision |
20
+ |---|---|---|
21
+ | P1 | Unit | One presentation is one `present()` call, or one display of a `Paywall` onboarding step, that resolved to a paywall. Calls refused before resolution (`unknown-moment`, `already-presenting`) send nothing. Errors after resolution (`parse-error`, `render-error`, `unknown-custom-screen`, `host-never-presented`, `paywall-disappeared`) send a start and an `error` end. |
22
+ | P2 | Identity | `presentation_id` is a client-minted lowercase UUIDv7. The ingest keys rows on `(roca_id, presentation_id)`. |
23
+ | P3 | Envelope | `schema_version`, `seq`, `sent_at` and `context` are exactly as in the onboarding run contract (D4, D5, D24). |
24
+ | P4 | Sends | At most three per presentation: on start (`in_progress`, `shown_at` null), on shown, and on end (`ended`). Each send is a full snapshot that replaces the previous one. |
25
+ | P5 | Terminal | `ended` is terminal: the first ended snapshot the server accepts wins, and anything after it is `ignored`. |
26
+ | P6 | Immutable | `presentation_id`, `started_at`, `paywall`, `surface`, `onboarding_run` and `context` never change within a presentation. A snapshot that changes one is `rejected`. |
27
+ | P7 | Shown | `shown_at` is set when the host acknowledges that the paywall is on screen. A presentation that ends without it was never seen, and counts toward no user-facing measure. |
28
+ | P8 | Outcome | One of `purchased`, `dismissed`, `cancelled` or `error`. `reason` appears only with `error`, carrying the SDK's error reason. `transaction` appears only with `purchased`. |
29
+ | P9 | Join key | `transaction.original_transaction_identifier` is the exact value the app passes to Rocalytics' `purchase` call (`TrackPurchaseParams.originalTransactionIdentifier`, required there). iOS: it is StoreKit's original transaction id and joins the store-notification rows' `original_transaction_identifier` directly, with no lookup. Android: `transaction.purchase_token` is the Play purchase token and is the join key; `original_transaction_identifier` carries the Play order id (`GPA.…`) there and is not used to join. |
30
+ | P10 | Unattributed | A `purchased` outcome without the platform's join key (iOS: `original_transaction_identifier`; Android: `purchase_token`) is a valid snapshot. It counts as a conversion and earns no proceeds. |
31
+ | P11 | Time | Analytics buckets a presentation by the server's receive time of its first snapshot, as onboarding D13. Within a presentation the client clamps timestamps to be monotonic. |
32
+ | P12 | No money | The client never sends a price. Proceeds come from store transaction rows joined on P9. |
33
+ | P13 | Restored | `transaction.restored: true` marks a `purchased` outcome that only restored existing access: a restore or a web redemption. It counts in neither `purchases` nor conversions. Only `true` is ever sent; an absent field means a new purchase. |
34
+
35
+ ## 2. A complete payload
36
+
37
+ The last of three sends of a purchased presentation on Android:
38
+
39
+ ```json
40
+ {
41
+ "schema_version": 1,
42
+ "presentation_id": "0192f1a2-7c3d-7e4f-8a5b-6c7d8e9f0a1b",
43
+ "seq": 3,
44
+ "status": "ended",
45
+ "started_at": "2026-01-10T09:00:00.000Z",
46
+ "shown_at": "2026-01-10T09:00:00.350Z",
47
+ "ended_at": "2026-01-10T09:00:41.120Z",
48
+ "sent_at": "2026-01-10T09:00:41.120Z",
49
+ "paywall": {
50
+ "moment_key": "settings_upgrade",
51
+ "paywall_id": "4b1f0e7c-2d1a-4c55-9a0e-6f2d3c1b7a90",
52
+ "audience_id": "a9e1c3d2-7b6f-4e21-8c0d-5f4a3b2c1d0e",
53
+ "render_mode": "custom",
54
+ "billing": "store"
55
+ },
56
+ "surface": "present",
57
+ "context": {
58
+ "app_version": "2.4.0",
59
+ "build": "412",
60
+ "platform": "android",
61
+ "os_version": "15",
62
+ "locale": "en-US",
63
+ "timezone": "America/New_York",
64
+ "library_version": "0.1.0"
65
+ },
66
+ "outcome": {
67
+ "status": "purchased",
68
+ "transaction": {
69
+ "original_transaction_identifier": "GPA.3311-4849-7511-34728",
70
+ "purchase_token": "mboecpopplapdphlhgegnpol.AO-J1OxExampleToken",
71
+ "product_id": "pro_annual"
72
+ }
73
+ }
74
+ }
75
+ ```
76
+
77
+ ## 3. Fields
78
+
79
+ | Field | Type | Meaning |
80
+ |---|---|---|
81
+ | `schema_version` | `1` | This document's revision. |
82
+ | `presentation_id` | lowercase UUID | Minted by the client when the presentation starts (P2). |
83
+ | `seq` | integer ≥ 1 | Increases with every send of this presentation (onboarding D4). |
84
+ | `status` | `in_progress` \| `ended` | `ended` is terminal (P5). |
85
+ | `started_at` | timestamp | When the paywall was resolved for display. |
86
+ | `shown_at` | timestamp \| null | When the host acknowledged the paywall on screen (P7). |
87
+ | `ended_at` | timestamp \| null | Set exactly when `status` is `ended`. |
88
+ | `sent_at` | timestamp | Device clock at send time. Advisory only. |
89
+ | `paywall.moment_key` | string | The moment the host presented. |
90
+ | `paywall.paywall_id` | string | The paywall the moment resolved to. |
91
+ | `paywall.audience_id` | string \| null | The audience that matched, if the catalog carried one. |
92
+ | `paywall.render_mode` | `elements` \| `custom` | How the paywall was rendered. |
93
+ | `paywall.billing` | `store` \| `stripe` | The paywall's billing path. |
94
+ | `paywall.variant_key`, `paywall.deployment_id` | string, optional | Present only when the catalog carries them. |
95
+ | `surface` | `present` \| `paywall_step` | A `present()` call, or a `Paywall` step inside an onboarding. |
96
+ | `onboarding_run` | object, optional | Only with `paywall_step`: the onboarding run's `run_id` and `step_key`. |
97
+ | `outcome` | object \| null | Set exactly when `status` is `ended` (P8). |
98
+ | `outcome.reason` | string, optional | Only with `error`. |
99
+ | `outcome.transaction` | object, optional | Only with `purchased`, never empty. Fields: `original_transaction_identifier`, `purchase_token`, `product_id`, `restored` (P9, P10, P13). |
100
+ | `context` | object | The onboarding contract's run context, captured once at start (onboarding D5). |
101
+
102
+ All timestamps have exactly three fractional digits (onboarding D24). A snapshot is at most 16 KiB.
103
+
104
+ ## 4. Rules beyond the schema
105
+
106
+ - `ended_at` and `outcome` are non-null exactly when `status` is `ended`.
107
+ - `shown_at`, when set, is not before `started_at`.
108
+ - `ended_at`, when set, is not before `shown_at` (or `started_at` when never shown).
109
+ - `sent_at` is not before `started_at`.
110
+ - `reason` appears only with outcome status `error`.
111
+ - `transaction` appears only with outcome status `purchased`, and is never an empty object.
112
+ - A `purchased` outcome requires `shown_at`.
113
+ - `onboarding_run` appears only with surface `paywall_step`.
114
+
115
+ ## 5. Transport
116
+
117
+ **Endpoint.** `POST /functions/v1/paywall-presentations` on the Rocalytics API. The sender is identified by the same headers the onboarding endpoint uses (`X-Roca-ID`, `X-Application-ID`, `X-Platform`); the body is the bare snapshot, with no device identifiers in it.
118
+
119
+ **Acceptance.** The server keys a presentation on the sender's `roca_id` and `presentation_id`, and answers each snapshot in its body:
120
+
121
+ - `ignored` when the stored snapshot is already `ended` (P5), or when the incoming `seq` is not higher than the stored one.
122
+ - `rejected` when the snapshot fails the schema or section 4, exceeds 16 KiB, or changes an immutable field (P6).
123
+ - `accepted` otherwise: the snapshot replaces the stored one.
124
+
125
+ **Outcomes and retries.** As onboarding D27: the outcome is always in the response body, on a 2xx and on a 4xx alike. A response without an outcome, and no response at all, are transient and retried with backoff. `accepted`, `ignored` and `rejected` settle the snapshot for good.
126
+
127
+ ## 6. Measures
128
+
129
+ Per moment and per paywall, over presentations bucketed by P11:
130
+
131
+ - **Users:** distinct senders with a presentation that was shown or purchased.
132
+ - **Conversions:** distinct senders with a `purchased` presentation that is not `restored` (P13).
133
+ - **Conversion rate:** conversions divided by users.
134
+ - **Unattributed purchases:** `purchased`, not restored, and missing the platform's join key (P10).
135
+ - **Proceeds:** the store transaction rows joined on P9. Never a client-sent price (P12).
136
+
137
+ ## 7. Versioning
138
+
139
+ The schema is closed, so every change increments `schema_version`, as onboarding D20. An additive change is one an older payload needs no conversion for; anything else is breaking.
@@ -0,0 +1,42 @@
1
+ import type { PaywallPresentationSnapshot } from "./paywall-presentation.types";
2
+
3
+ const context = {
4
+ app_version: "2.4.0", build: "412", platform: "android", os_version: "15",
5
+ locale: "en-US", timezone: "America/New_York", library_version: "0.1.0",
6
+ } as const;
7
+ const paywall = {
8
+ moment_key: "settings_upgrade", paywall_id: "4b1f0e7c-2d1a-4c55-9a0e-6f2d3c1b7a90",
9
+ audience_id: "a9e1c3d2-7b6f-4e21-8c0d-5f4a3b2c1d0e", render_mode: "custom", billing: "store",
10
+ } as const;
11
+ const id = "0192f1a2-7c3d-7e4f-8a5b-6c7d8e9f0a1b";
12
+
13
+ /** The three sends of one purchased presentation on Android. */
14
+ export const PURCHASED_ANDROID: PaywallPresentationSnapshot[] = [
15
+ { schema_version: 1, presentation_id: id, seq: 1, status: "in_progress", started_at: "2026-01-10T09:00:00.000Z",
16
+ shown_at: null, ended_at: null, sent_at: "2026-01-10T09:00:00.000Z", paywall, surface: "present", outcome: null, context },
17
+ { schema_version: 1, presentation_id: id, seq: 2, status: "in_progress", started_at: "2026-01-10T09:00:00.000Z",
18
+ shown_at: "2026-01-10T09:00:00.350Z", ended_at: null, sent_at: "2026-01-10T09:00:00.350Z", paywall, surface: "present", outcome: null, context },
19
+ { schema_version: 1, presentation_id: id, seq: 3, status: "ended", started_at: "2026-01-10T09:00:00.000Z",
20
+ shown_at: "2026-01-10T09:00:00.350Z", ended_at: "2026-01-10T09:00:41.120Z", sent_at: "2026-01-10T09:00:41.120Z",
21
+ paywall, surface: "present", context,
22
+ outcome: { status: "purchased", transaction: {
23
+ original_transaction_identifier: "GPA.3311-4849-7511-34728",
24
+ purchase_token: "mboecpopplapdphlhgegnpol.AO-J1OxExampleToken", product_id: "pro_annual" } } },
25
+ ];
26
+
27
+ export const DISMISSED_STEP: PaywallPresentationSnapshot = {
28
+ schema_version: 1, presentation_id: "0192f1a2-7c3d-7e4f-8a5b-6c7d8e9f0a1c", seq: 3, status: "ended",
29
+ started_at: "2026-01-10T09:00:00.000Z", shown_at: "2026-01-10T09:00:00.200Z", ended_at: "2026-01-10T09:00:05.000Z",
30
+ sent_at: "2026-01-10T09:00:05.000Z", paywall: { ...paywall, moment_key: "onboarding_end" }, surface: "paywall_step",
31
+ onboarding_run: { run_id: "0192f1a2-0000-7000-8000-000000000001", step_key: "paywall" },
32
+ outcome: { status: "dismissed" }, context: { ...context, platform: "ios", os_version: "18.1" },
33
+ };
34
+
35
+ export const RENDER_ERROR: PaywallPresentationSnapshot = {
36
+ schema_version: 1, presentation_id: "0192f1a2-7c3d-7e4f-8a5b-6c7d8e9f0a1d", seq: 2, status: "ended",
37
+ started_at: "2026-01-10T09:00:00.000Z", shown_at: null, ended_at: "2026-01-10T09:00:00.010Z",
38
+ sent_at: "2026-01-10T09:00:00.010Z", paywall, surface: "present",
39
+ outcome: { status: "error", reason: "unknown-custom-screen" }, context,
40
+ };
41
+
42
+ export const ALL_EXAMPLES: PaywallPresentationSnapshot[] = [...PURCHASED_ANDROID, DISMISSED_STEP, RENDER_ERROR];
@@ -0,0 +1,268 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "urn:rocalytics-sdk:paywall-presentation:1",
4
+ "title": "Paywall presentation snapshot, schema_version 1",
5
+ "description": "One send of a paywall presentation. Each send replaces the presentation's previous snapshot. Normative text: paywall-presentation-contract.md. The schema is closed: an unknown field is an error.",
6
+ "type": "object",
7
+ "required": [
8
+ "schema_version",
9
+ "presentation_id",
10
+ "seq",
11
+ "status",
12
+ "started_at",
13
+ "shown_at",
14
+ "ended_at",
15
+ "sent_at",
16
+ "paywall",
17
+ "surface",
18
+ "outcome",
19
+ "context"
20
+ ],
21
+ "additionalProperties": false,
22
+ "properties": {
23
+ "schema_version": {
24
+ "const": 1
25
+ },
26
+ "presentation_id": {
27
+ "type": "string",
28
+ "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
29
+ },
30
+ "seq": {
31
+ "type": "integer",
32
+ "minimum": 1
33
+ },
34
+ "status": {
35
+ "enum": [
36
+ "in_progress",
37
+ "ended"
38
+ ]
39
+ },
40
+ "started_at": {
41
+ "$ref": "#/$defs/timestamp"
42
+ },
43
+ "shown_at": {
44
+ "anyOf": [
45
+ {
46
+ "$ref": "#/$defs/timestamp"
47
+ },
48
+ {
49
+ "type": "null"
50
+ }
51
+ ]
52
+ },
53
+ "ended_at": {
54
+ "anyOf": [
55
+ {
56
+ "$ref": "#/$defs/timestamp"
57
+ },
58
+ {
59
+ "type": "null"
60
+ }
61
+ ]
62
+ },
63
+ "sent_at": {
64
+ "$ref": "#/$defs/timestamp"
65
+ },
66
+ "paywall": {
67
+ "type": "object",
68
+ "required": [
69
+ "moment_key",
70
+ "paywall_id",
71
+ "audience_id",
72
+ "render_mode",
73
+ "billing"
74
+ ],
75
+ "additionalProperties": false,
76
+ "properties": {
77
+ "moment_key": {
78
+ "$ref": "#/$defs/id"
79
+ },
80
+ "paywall_id": {
81
+ "$ref": "#/$defs/id"
82
+ },
83
+ "audience_id": {
84
+ "anyOf": [
85
+ {
86
+ "$ref": "#/$defs/id"
87
+ },
88
+ {
89
+ "type": "null"
90
+ }
91
+ ]
92
+ },
93
+ "render_mode": {
94
+ "enum": [
95
+ "elements",
96
+ "custom"
97
+ ]
98
+ },
99
+ "billing": {
100
+ "enum": [
101
+ "store",
102
+ "stripe"
103
+ ]
104
+ },
105
+ "variant_key": {
106
+ "$ref": "#/$defs/id"
107
+ },
108
+ "deployment_id": {
109
+ "$ref": "#/$defs/id"
110
+ }
111
+ }
112
+ },
113
+ "surface": {
114
+ "enum": [
115
+ "present",
116
+ "paywall_step"
117
+ ]
118
+ },
119
+ "onboarding_run": {
120
+ "type": "object",
121
+ "required": [
122
+ "run_id",
123
+ "step_key"
124
+ ],
125
+ "additionalProperties": false,
126
+ "properties": {
127
+ "run_id": {
128
+ "type": "string",
129
+ "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
130
+ },
131
+ "step_key": {
132
+ "$ref": "#/$defs/key"
133
+ }
134
+ }
135
+ },
136
+ "outcome": {
137
+ "anyOf": [
138
+ {
139
+ "type": "null"
140
+ },
141
+ {
142
+ "type": "object",
143
+ "required": [
144
+ "status"
145
+ ],
146
+ "additionalProperties": false,
147
+ "properties": {
148
+ "status": {
149
+ "enum": [
150
+ "purchased",
151
+ "dismissed",
152
+ "cancelled",
153
+ "error"
154
+ ]
155
+ },
156
+ "reason": {
157
+ "$ref": "#/$defs/key"
158
+ },
159
+ "transaction": {
160
+ "type": "object",
161
+ "required": [],
162
+ "additionalProperties": false,
163
+ "properties": {
164
+ "original_transaction_identifier": {
165
+ "$ref": "#/$defs/storeId"
166
+ },
167
+ "purchase_token": {
168
+ "$ref": "#/$defs/storeId"
169
+ },
170
+ "product_id": {
171
+ "$ref": "#/$defs/storeId"
172
+ },
173
+ "restored": {
174
+ "type": "boolean"
175
+ }
176
+ }
177
+ }
178
+ }
179
+ }
180
+ ]
181
+ },
182
+ "context": {
183
+ "description": "Captured once, when the run starts. Country is not sent: the server derives it.",
184
+ "type": "object",
185
+ "required": [
186
+ "app_version",
187
+ "build",
188
+ "platform",
189
+ "os_version",
190
+ "locale",
191
+ "timezone"
192
+ ],
193
+ "additionalProperties": false,
194
+ "properties": {
195
+ "app_version": {
196
+ "$ref": "#/$defs/shortText"
197
+ },
198
+ "build": {
199
+ "anyOf": [
200
+ {
201
+ "$ref": "#/$defs/shortText"
202
+ },
203
+ {
204
+ "type": "null"
205
+ }
206
+ ]
207
+ },
208
+ "platform": {
209
+ "enum": [
210
+ "ios",
211
+ "android",
212
+ "web"
213
+ ]
214
+ },
215
+ "os_version": {
216
+ "anyOf": [
217
+ {
218
+ "$ref": "#/$defs/shortText"
219
+ },
220
+ {
221
+ "type": "null"
222
+ }
223
+ ]
224
+ },
225
+ "locale": {
226
+ "description": "BCP 47 language tag.",
227
+ "type": "string",
228
+ "pattern": "^[A-Za-z]{2,3}(-[A-Za-z0-9]{1,8})*$"
229
+ },
230
+ "timezone": {
231
+ "description": "IANA time zone name.",
232
+ "type": "string",
233
+ "pattern": "^[A-Za-z][A-Za-z0-9_+-]*(/[A-Za-z0-9_+-]+)*$",
234
+ "maxLength": 64
235
+ },
236
+ "library_version": {
237
+ "$ref": "#/$defs/shortText"
238
+ }
239
+ }
240
+ }
241
+ },
242
+ "$defs": {
243
+ "timestamp": {
244
+ "description": "RFC 3339 in UTC with exactly three fractional digits and a Z suffix, e.g. 2026-01-10T09:00:00.000Z. A pattern rather than format: date-time, because format is annotation-only in draft 2020-12.",
245
+ "type": "string",
246
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\\.[0-9]{3}Z$"
247
+ },
248
+ "key": {
249
+ "type": "string",
250
+ "pattern": "^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$"
251
+ },
252
+ "shortText": {
253
+ "type": "string",
254
+ "minLength": 1,
255
+ "maxLength": 32
256
+ },
257
+ "id": {
258
+ "type": "string",
259
+ "minLength": 1,
260
+ "maxLength": 128
261
+ },
262
+ "storeId": {
263
+ "type": "string",
264
+ "minLength": 1,
265
+ "maxLength": 1024
266
+ }
267
+ }
268
+ }
@@ -0,0 +1,73 @@
1
+ // Paywall presentation contract v1: the payload types.
2
+ // Normative text: docs/paywall-presentation-contract.md. src/paywall/contract.ts
3
+ // is this file, byte for byte, except its import line (test/paywall-docs.test.ts).
4
+ import type { RunContext, Timestamp } from "./onboarding-run.types";
5
+
6
+ export type PresentationStatus = "in_progress" | "ended";
7
+ export type PresentationOutcomeStatus = "purchased" | "dismissed" | "cancelled" | "error";
8
+ export type PresentationSurface = "present" | "paywall_step";
9
+
10
+ export interface PresentationPaywall {
11
+ /** The moment's key: what the host passed to `present()`. */
12
+ moment_key: string;
13
+ /** The Studio paywall id the moment's audience waterfall picked. */
14
+ paywall_id: string;
15
+ /** The audience that matched; null when the catalog carried none. */
16
+ audience_id: string | null;
17
+ render_mode: "elements" | "custom";
18
+ billing: "store" | "stripe";
19
+ variant_key?: string;
20
+ deployment_id?: string;
21
+ }
22
+
23
+ export interface PresentationTransaction {
24
+ /**
25
+ * The exact value the app passes to Rocalytics' `purchase` call (`originalTransactionIdentifier`).
26
+ * iOS: StoreKit's original transaction id, the join key to the store-notification rows.
27
+ * Android: the Play order id (GPA.…); not used to join.
28
+ */
29
+ original_transaction_identifier?: string;
30
+ /** Android only: the Play purchase token, the proceeds join key on Android. */
31
+ purchase_token?: string;
32
+ product_id?: string;
33
+ /** True when the purchase only restored existing access (a restore, a web redemption). Not a conversion. */
34
+ restored?: boolean;
35
+ }
36
+
37
+ export interface PresentationOutcome {
38
+ status: PresentationOutcomeStatus;
39
+ /** Only with status "error": the SDK's reason, e.g. "render-error". */
40
+ reason?: string;
41
+ /** Only with status "purchased". Absent when the host reported none. */
42
+ transaction?: PresentationTransaction;
43
+ }
44
+
45
+ interface PresentationBase {
46
+ schema_version: 1;
47
+ /** Client-minted, lowercase UUID. One per presentation. */
48
+ presentation_id: string;
49
+ /** Starts at 1 and increases with every send of this presentation. */
50
+ seq: number;
51
+ started_at: Timestamp;
52
+ shown_at: Timestamp | null;
53
+ sent_at: Timestamp;
54
+ paywall: PresentationPaywall;
55
+ surface: PresentationSurface;
56
+ /** Only for surface "paywall_step": the onboarding run it sits in. */
57
+ onboarding_run?: { run_id: string; step_key: string };
58
+ context: RunContext;
59
+ }
60
+
61
+ export interface InProgressPresentationSnapshot extends PresentationBase {
62
+ status: "in_progress";
63
+ ended_at: null;
64
+ outcome: null;
65
+ }
66
+
67
+ export interface EndedPresentationSnapshot extends PresentationBase {
68
+ status: "ended";
69
+ ended_at: Timestamp;
70
+ outcome: PresentationOutcome;
71
+ }
72
+
73
+ export type PaywallPresentationSnapshot = InProgressPresentationSnapshot | EndedPresentationSnapshot;
@@ -0,0 +1,4 @@
1
+ {
2
+ "main": "../dist/onboarding/index.js",
3
+ "types": "../dist/onboarding/index.d.ts"
4
+ }
package/package.json CHANGED
@@ -1,6 +1,122 @@
1
1
  {
2
2
  "name": "@rocapine/rocalytics-sdk",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.3.0",
4
+ "description": "Headless client SDK for Onboarding Studio: a typed onboarding run tracker for any onboarding, including hand-coded ones, and the Rocalytics client",
5
+ "keywords": [
6
+ "onboarding",
7
+ "analytics",
8
+ "funnel",
9
+ "react-native",
10
+ "headless",
11
+ "rocalytics"
12
+ ],
13
+ "author": "Rocapine",
14
+ "license": "MIT",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/Rocapine/rocalytics-sdk.git"
18
+ },
19
+ "sideEffects": false,
20
+ "exports": {
21
+ "./onboarding": {
22
+ "types": "./dist/onboarding/index.d.ts",
23
+ "default": "./dist/onboarding/index.js"
24
+ },
25
+ "./core": {
26
+ "types": "./dist/core/index.d.ts",
27
+ "default": "./dist/core/index.js"
28
+ },
29
+ "./client": {
30
+ "types": "./dist/client/index.d.ts",
31
+ "default": "./dist/client/index.js"
32
+ },
33
+ "./paywall": {
34
+ "types": "./dist/paywall/index.d.ts",
35
+ "default": "./dist/paywall/index.js"
36
+ },
37
+ "./package.json": "./package.json"
38
+ },
39
+ "typesVersions": {
40
+ "*": {
41
+ "onboarding": [
42
+ "./dist/onboarding/index.d.ts"
43
+ ],
44
+ "core": [
45
+ "./dist/core/index.d.ts"
46
+ ],
47
+ "client": [
48
+ "./dist/client/index.d.ts"
49
+ ],
50
+ "paywall": [
51
+ "./dist/paywall/index.d.ts"
52
+ ]
53
+ }
54
+ },
55
+ "files": [
56
+ "dist",
57
+ "src",
58
+ "docs",
59
+ "onboarding",
60
+ "core",
61
+ "client",
62
+ "paywall",
63
+ "README.md",
64
+ "CHANGELOG.md"
65
+ ],
66
+ "publishConfig": {
67
+ "access": "public",
68
+ "registry": "https://registry.npmjs.org/"
69
+ },
70
+ "scripts": {
71
+ "build": "rm -rf dist && tsc -p tsconfig.json",
72
+ "prepack": "npm run build",
73
+ "prepublishOnly": "npm run lint && npm run type:check:tests && npm test && npm run build && npm run check:exports",
74
+ "type:check": "tsc -p tsconfig.json --noEmit",
75
+ "type:check:tests": "tsc -p tsconfig.test.json",
76
+ "lint": "eslint .",
77
+ "test": "vitest run",
78
+ "test:watch": "vitest",
79
+ "gen:schema-types": "UPDATE_SCHEMA_TYPES=1 vitest run test/schema-types.test.ts test/paywall-schema-types.test.ts",
80
+ "check:exports": "node scripts/check-exports.mjs"
81
+ },
82
+ "peerDependencies": {
83
+ "expo-application": "*",
84
+ "expo-crypto": "*",
85
+ "expo-device": "*",
86
+ "expo-modules-core": "*",
87
+ "expo-network": "*",
88
+ "expo-secure-store": "*",
89
+ "react-native": "*"
90
+ },
91
+ "peerDependenciesMeta": {
92
+ "expo-application": {
93
+ "optional": true
94
+ },
95
+ "expo-crypto": {
96
+ "optional": true
97
+ },
98
+ "expo-device": {
99
+ "optional": true
100
+ },
101
+ "expo-modules-core": {
102
+ "optional": true
103
+ },
104
+ "expo-network": {
105
+ "optional": true
106
+ },
107
+ "expo-secure-store": {
108
+ "optional": true
109
+ },
110
+ "react-native": {
111
+ "optional": true
112
+ }
113
+ },
114
+ "devDependencies": {
115
+ "@eslint/js": "^9.39.5",
116
+ "@types/node": "^22.20.4",
117
+ "eslint": "^9.39.5",
118
+ "typescript": "^5.9.3",
119
+ "typescript-eslint": "^8.71.0",
120
+ "vitest": "^4.1.11"
121
+ }
122
+ }
@@ -0,0 +1 @@
1
+ {"main":"../dist/paywall/index.js","types":"../dist/paywall/index.d.ts"}