@mysuperwall/react-native 0.0.0-stage → 1.1.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 (37) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/LICENSE +21 -0
  3. package/README.md +157 -2
  4. package/lib/MySuperwall.d.ts +64 -0
  5. package/lib/MySuperwall.js +249 -0
  6. package/lib/engine/localRuleEngine.d.ts +44 -0
  7. package/lib/engine/localRuleEngine.js +318 -0
  8. package/lib/hooks/usePaywall.d.ts +7 -0
  9. package/lib/hooks/usePaywall.js +25 -0
  10. package/lib/index.d.ts +11 -0
  11. package/lib/index.js +27 -0
  12. package/lib/models/manifest.d.ts +76 -0
  13. package/lib/models/manifest.js +2 -0
  14. package/lib/models/paywall.d.ts +36 -0
  15. package/lib/models/paywall.js +2 -0
  16. package/lib/services/anonymousIdentity.d.ts +6 -0
  17. package/lib/services/anonymousIdentity.js +36 -0
  18. package/lib/services/followUpService.d.ts +9 -0
  19. package/lib/services/followUpService.js +26 -0
  20. package/lib/services/manifestService.d.ts +21 -0
  21. package/lib/services/manifestService.js +80 -0
  22. package/lib/services/offlineEventQueue.d.ts +18 -0
  23. package/lib/services/offlineEventQueue.js +101 -0
  24. package/lib/ui/PaywallModal.d.ts +8 -0
  25. package/lib/ui/PaywallModal.js +136 -0
  26. package/package.json +51 -4
  27. package/src/MySuperwall.ts +272 -0
  28. package/src/engine/localRuleEngine.ts +375 -0
  29. package/src/hooks/usePaywall.ts +27 -0
  30. package/src/index.ts +12 -0
  31. package/src/models/manifest.ts +81 -0
  32. package/src/models/paywall.ts +40 -0
  33. package/src/services/anonymousIdentity.ts +28 -0
  34. package/src/services/followUpService.ts +21 -0
  35. package/src/services/manifestService.ts +63 -0
  36. package/src/services/offlineEventQueue.ts +88 -0
  37. package/src/ui/PaywallModal.tsx +122 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,10 @@
1
+ # Changelog
2
+
3
+ ## 1.1.0
4
+
5
+ - Add compiled JavaScript and TypeScript declarations to npm packages.
6
+ - Keep native integrations as peer dependencies, without pinning host apps to one major.
7
+ - Persist anonymous installation identity per application and server.
8
+ - Send Android package identifiers as packageName for manifests and analytics.
9
+ - Export initialization option types and include the MIT license.
10
+
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 MySuperWall
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,158 @@
1
- # Temporary Holding Version
1
+ # MySuperwall React Native SDK
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Official React Native SDK for [MySuperwall](https://mysuperwall.com). Remotely configure, evaluate, and display dynamic paywalls in your React Native apps using cached, on-device rule evaluation.
4
+
5
+ ## Features
6
+
7
+ - **⚡ Local rule evaluation:** Evaluates audience rules and deterministic A/B variants locally in-memory on the device.
8
+ - **💾 Offline Cache:** Automatically caches active paywall manifests with `@react-native-async-storage/async-storage` for instant cold starts without network wait.
9
+ - **🔄 ETag / 304 Synchronization:** Background sync fetches updates with zero bandwidth waste when rules haven't changed.
10
+ - **🎨 HTML/CSS Dynamic Paywalls:** High-performance `react-native-webview` rendering with two-way JS bridge.
11
+ - **📊 Offline Event Queue:** Fire-and-forget analytics event batching to `POST /paywall/events/batch`.
12
+
13
+ ## Compatibility
14
+
15
+ The package includes compiled JavaScript and TypeScript declarations. Native WebView
16
+ and AsyncStorage remain peer dependencies so your app controls their versions.
17
+
18
+ Verified dependency combinations (installation and TypeScript checks):
19
+
20
+ | React | React Native | WebView | AsyncStorage |
21
+ | --- | --- | --- | --- |
22
+ | 18.2.0 | 0.73.4 | 13.8.1 | 1.21.0 |
23
+ | 19.2.3 | 0.87.1 | 14.0.1 | 3.1.1 |
24
+
25
+ For modern React Native use your app's `moduleResolution: "bundler"` TypeScript configuration.
26
+ These checks do not replace an iOS/Android device integration test. Versions below
27
+ these combinations are not covered by this audit, even if the peer range permits them.
28
+ Cached manifests are available after initialization; the first installation still
29
+ needs a successful background manifest download before a trigger can resolve.
30
+ For Android pass `platform: 'android'`; `bundleId` is sent as the Android package name.
31
+
32
+ ## Installation
33
+
34
+ ```sh
35
+ npm install @mysuperwall/react-native react-native-webview @react-native-async-storage/async-storage
36
+ ```
37
+
38
+ Choose WebView and AsyncStorage versions compatible with your existing React Native
39
+ or Expo project. For Expo-managed dependencies use `npx expo install`.
40
+ Bare iOS projects also require `pod install` from the `ios` directory.
41
+ Expo requires a native development build.
42
+
43
+ Existing checksum-pinned source snapshots remain an alternative installation method.
44
+ Do not install both the snapshot and npm package in the same app.
45
+
46
+ ## Quick Start
47
+
48
+ ### 1. Initialize on App Launch
49
+
50
+ In your root `App.tsx` or `index.js`:
51
+
52
+ ```tsx
53
+ import React, { useEffect } from 'react';
54
+ import { Platform } from 'react-native';
55
+ import { MySuperwall, PaywallModal, usePaywall } from '@mysuperwall/react-native';
56
+
57
+ export default function App() {
58
+ const { activePaywall, isVisible, dismiss, callbacks } = usePaywall();
59
+
60
+ useEffect(() => {
61
+ MySuperwall.initialize({
62
+ appKey: 'YOUR_APP_KEY',
63
+ platform: Platform.OS === 'android' ? 'android' : 'ios'
64
+ });
65
+ }, []);
66
+
67
+ return (
68
+ <>
69
+ <MainAppScreen />
70
+ <PaywallModal
71
+ visible={isVisible}
72
+ paywall={activePaywall}
73
+ onClose={dismiss}
74
+ {...callbacks}
75
+ />
76
+ </>
77
+ );
78
+ }
79
+ ```
80
+
81
+ ### 2. Trigger a Paywall
82
+
83
+ Fires immediately with **no network request for cached rules**:
84
+
85
+ ```tsx
86
+ MySuperwall.shared.handleTrigger('onboarding_finish', {
87
+ onPurchase: async (productId) => {
88
+ console.log('User wants to purchase:', productId);
89
+ // Call your IAP flow (RevenueCat, react-native-iap)
90
+ },
91
+ onRestore: () => {
92
+ console.log('User requested restore');
93
+ },
94
+ onDismiss: () => {
95
+ console.log('Paywall closed');
96
+ }
97
+ });
98
+ ```
99
+
100
+ ### 3. User Attributes & Identification
101
+
102
+ ```tsx
103
+ // Identify authenticated users
104
+ MySuperwall.shared.identify('user_12345');
105
+
106
+ // Set custom attributes for audience targeting
107
+ MySuperwall.shared.setUserAttributes({
108
+ tier: 'gold',
109
+ level: 15,
110
+ isSubscriber: false
111
+ });
112
+ ```
113
+
114
+
115
+ ### Experiment attribution
116
+
117
+ Manifest resolution retains optional server-issued experiment, audience, variant and
118
+ published-paywall version identifiers. Older manifests remain usable without this
119
+ context. Fetching or resolving alone does not record an impression. Built-in UI
120
+ records `paywall_shown` after the page loads while displayed; custom presenters
121
+ must call `trackPaywallEvent` when they actually show the paywall.
122
+
123
+ Purchase and restore button requests record `purchase_started` and
124
+ `restore_started`, not successful transactions. After your billing provider confirms
125
+ a purchase, pass the **original displayed paywall** to the SDK:
126
+
127
+ ```
128
+ await MySuperwall.shared.trackPaywallEvent('purchase_success', displayedPaywall, productId)
129
+ ```
130
+
131
+ Do not resolve the trigger again to attribute a completed purchase. An identity
132
+ change invalidates attribution from the previous user's paywall. Offline retries
133
+ retain the original event ID and context; events are removed by acknowledged ID.
134
+ Verified revenue remains a separate backend/provider concern.
135
+
136
+ ## Locale, targeting and recovery (1.1.0)
137
+
138
+ Initialize with optional `locale`, `country` (ISO region, e.g. `TR`) and `appVersion`.
139
+ The default language and region come from the device's Intl locale when available.
140
+ React Native does not provide a universal built-in app-version API: pass your real
141
+ version from your existing app-info integration, or call `setAppVersion(version)`.
142
+ `await MySuperwall.shared.setLocale('tr')` switches to the language-specific cache
143
+ and refreshes its manifest; `setCountry('US')` does not change the paywall language.
144
+ Call `setSubscriptionStatus(true/false)` from your billing entitlement state for
145
+ `skipIfPremium` recovery rules.
146
+
147
+ `handleTrigger` automatically handles configured trigger-based follow-up rules and
148
+ the legacy dismiss second screen. Explicit rules take priority. Session/cooldown,
149
+ chain-depth and repeat checks bound the flow. A purchase tap does not run a success
150
+ rule: the host must call `trackPaywallEvent('purchase_success', displayedPaywall)`
151
+ after its billing provider confirms success. Use the captured presenter payload.
152
+ Identity/configuration/language changes cancel delayed recovery. The stock modal
153
+ calls the dismissal callback once for either a close action or Android Back.
154
+ Programmatic dismissal through the hook clears the modal without starting recovery.
155
+
156
+ Manifest and offline event storage are isolated by app/server scope. Unscoped legacy
157
+ caches are ignored; source updates need one successful manifest sync before offline use.
158
+ No store billing is replaced by this SDK.
@@ -0,0 +1,64 @@
1
+ import { LocalRuleEngine } from './engine/localRuleEngine';
2
+ import { ManifestService } from './services/manifestService';
3
+ import { OfflineEventQueue } from './services/offlineEventQueue';
4
+ import { PaywallResult, PaywallData, PaywallCallbackActions } from './models/paywall';
5
+ export interface MySuperwallInitOptions {
6
+ appKey?: string;
7
+ userId?: string;
8
+ baseUrl?: string;
9
+ bundleId?: string;
10
+ platform?: 'ios' | 'android' | 'web';
11
+ locale?: string;
12
+ country?: string;
13
+ appVersion?: string;
14
+ }
15
+ export declare class MySuperwall {
16
+ private static instance;
17
+ appKey?: string;
18
+ userId?: string;
19
+ baseUrl: string;
20
+ bundleId?: string;
21
+ platform: string;
22
+ locale: string;
23
+ country: string;
24
+ appVersion?: string;
25
+ private premium;
26
+ private epoch;
27
+ private presentationSequence;
28
+ private followUps;
29
+ private activeFlow?;
30
+ userAttributes: Record<string, any>;
31
+ manifestService: ManifestService;
32
+ ruleEngine: LocalRuleEngine;
33
+ eventQueue: OfflineEventQueue;
34
+ private onPresentPaywallListener?;
35
+ private constructor();
36
+ static get shared(): MySuperwall;
37
+ /**
38
+ * Initializes MySuperwall with cached local rule evaluation.
39
+ */
40
+ static initialize(options?: MySuperwallInitOptions): Promise<MySuperwall>;
41
+ identify(userId: string): void;
42
+ private resetFlow;
43
+ private manifestOptions;
44
+ setLocale(locale: string): Promise<void>;
45
+ setCountry(country: string): void;
46
+ setAppVersion(version: string): void;
47
+ setSubscriptionStatus(premium: boolean): void;
48
+ setUserAttributes(attributes: Record<string, any>): void;
49
+ registerPresenter(listener: (paywall: PaywallData, callbacks: PaywallCallbackActions) => void): () => void;
50
+ /**
51
+ * Evaluates rules locally in-memory in memory.
52
+ */
53
+ resolveTrigger(triggerKey: string, metadata?: Record<string, any>): PaywallResult;
54
+ /** Attribute actions to the displayed paywall, never a new assignment. */
55
+ trackPaywallEvent(type: string, paywall: PaywallData, productId?: string): Promise<void>;
56
+ /**
57
+ * Fires a trigger, presents the paywall if resolved, and wires up callbacks.
58
+ */
59
+ handleTrigger(triggerKey: string, callbacks?: PaywallCallbackActions & {
60
+ metadata?: Record<string, any>;
61
+ }): PaywallResult;
62
+ private present;
63
+ private scheduleFollowUp;
64
+ }
@@ -0,0 +1,249 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MySuperwall = void 0;
4
+ const anonymousIdentity_1 = require("./services/anonymousIdentity");
5
+ const followUpService_1 = require("./services/followUpService");
6
+ const localRuleEngine_1 = require("./engine/localRuleEngine");
7
+ const manifestService_1 = require("./services/manifestService");
8
+ const offlineEventQueue_1 = require("./services/offlineEventQueue");
9
+ class MySuperwall {
10
+ constructor() {
11
+ this.baseUrl = 'https://mysuperwall.com/';
12
+ this.platform = 'ios';
13
+ this.locale = 'en';
14
+ this.country = '';
15
+ this.premium = false;
16
+ this.epoch = 0;
17
+ this.presentationSequence = 0;
18
+ this.followUps = new followUpService_1.FollowUpService();
19
+ this.userAttributes = {};
20
+ this.manifestService = new manifestService_1.ManifestService();
21
+ this.ruleEngine = new localRuleEngine_1.LocalRuleEngine();
22
+ this.eventQueue = new offlineEventQueue_1.OfflineEventQueue();
23
+ }
24
+ static get shared() {
25
+ if (!MySuperwall.instance) {
26
+ MySuperwall.instance = new MySuperwall();
27
+ }
28
+ return MySuperwall.instance;
29
+ }
30
+ /**
31
+ * Initializes MySuperwall with cached local rule evaluation.
32
+ */
33
+ static async initialize(options = {}) {
34
+ const inst = MySuperwall.shared;
35
+ inst.resetFlow();
36
+ inst.userAttributes = {};
37
+ inst.premium = false;
38
+ inst.appKey = options.appKey;
39
+ inst.baseUrl = options.baseUrl
40
+ ? options.baseUrl.endsWith('/')
41
+ ? options.baseUrl
42
+ : `${options.baseUrl}/`
43
+ : 'https://mysuperwall.com/';
44
+ inst.bundleId = options.bundleId;
45
+ inst.platform = options.platform || 'ios';
46
+ let deviceLocale = 'en';
47
+ try {
48
+ deviceLocale = Intl.DateTimeFormat().resolvedOptions().locale;
49
+ }
50
+ catch (_) { }
51
+ inst.locale = (options.locale || deviceLocale).replace(/_/g, '-').toLowerCase();
52
+ inst.country = (options.country || inst.locale.match(/-([a-z]{2})(?:-|$)/i)?.[1] || '').toUpperCase();
53
+ inst.appVersion = options.appVersion;
54
+ inst.manifestService.configure(inst.manifestOptions());
55
+ inst.eventQueue = new offlineEventQueue_1.OfflineEventQueue(JSON.stringify([inst.baseUrl, inst.appKey, inst.bundleId, inst.platform]));
56
+ const epoch = inst.epoch;
57
+ const userId = options.userId?.trim() || await anonymousIdentity_1.AnonymousIdentity.get(JSON.stringify([inst.baseUrl, inst.appKey, inst.bundleId, inst.platform]));
58
+ if (epoch !== inst.epoch)
59
+ return inst;
60
+ inst.userId = userId;
61
+ // 1. Load this app’s cached manifest into memory
62
+ await inst.manifestService.loadFromDisk();
63
+ if (epoch !== inst.epoch)
64
+ return inst;
65
+ // 2. Background non-blocking sync with ETag / 304 validation
66
+ void inst.manifestService.syncInBackground(inst.manifestOptions());
67
+ return inst;
68
+ }
69
+ identify(userId) {
70
+ if (!userId.trim())
71
+ throw new Error('User ID cannot be empty');
72
+ if (this.userId !== userId.trim()) {
73
+ this.resetFlow();
74
+ this.userAttributes = {};
75
+ this.premium = false;
76
+ }
77
+ this.userId = userId.trim();
78
+ }
79
+ resetFlow() { this.epoch++; this.followUps.reset(); this.activeFlow = undefined; }
80
+ manifestOptions() {
81
+ return { baseUrl: this.baseUrl, appKey: this.appKey, bundleId: this.bundleId, platform: this.platform, lang: this.locale };
82
+ }
83
+ async setLocale(locale) {
84
+ const normalized = locale.trim().replace(/_/g, '-').toLowerCase();
85
+ if (!normalized || normalized === this.locale)
86
+ return;
87
+ this.resetFlow();
88
+ this.locale = normalized;
89
+ const epoch = this.epoch;
90
+ this.manifestService.configure(this.manifestOptions());
91
+ await this.manifestService.loadFromDisk();
92
+ if (epoch === this.epoch)
93
+ await this.manifestService.syncInBackground(this.manifestOptions());
94
+ }
95
+ setCountry(country) { this.country = country.trim().toUpperCase(); }
96
+ setAppVersion(version) { this.appVersion = version; }
97
+ setSubscriptionStatus(premium) { this.premium = premium; }
98
+ setUserAttributes(attributes) {
99
+ Object.assign(this.userAttributes, attributes);
100
+ }
101
+ registerPresenter(listener) {
102
+ this.onPresentPaywallListener = listener;
103
+ return () => {
104
+ if (this.onPresentPaywallListener === listener) {
105
+ this.onPresentPaywallListener = undefined;
106
+ }
107
+ };
108
+ }
109
+ /**
110
+ * Evaluates rules locally in-memory in memory.
111
+ */
112
+ resolveTrigger(triggerKey, metadata) {
113
+ const manifest = this.manifestService.manifest;
114
+ if (!manifest) {
115
+ return { show: false, paywall: null, reason: 'manifest_not_cached' };
116
+ }
117
+ const trigger = (manifest.triggers || []).find(t => t.key.trim().toLowerCase() === triggerKey.trim().toLowerCase());
118
+ if (!trigger) {
119
+ return { show: false, paywall: null, reason: 'trigger_not_found' };
120
+ }
121
+ const evalContext = {
122
+ platform: this.platform,
123
+ country: this.country,
124
+ appVersion: this.appVersion,
125
+ metadata: metadata || {},
126
+ userAttributes: this.userAttributes,
127
+ ...(metadata || {})
128
+ };
129
+ const resolution = this.ruleEngine.resolveTrigger(trigger, this.userId, evalContext);
130
+ if (resolution.isHoldout) {
131
+ return { show: false, paywall: null, reason: 'holdout' };
132
+ }
133
+ if (resolution.show && resolution.paywallId) {
134
+ const manifestPaywall = manifest.paywalls[resolution.paywallId];
135
+ if (manifestPaywall) {
136
+ const paywallData = {
137
+ followUpRules: manifestPaywall.contentJson?.followUpRules,
138
+ dismissFollowUpPaywallId: trigger.dismissAction?.type === 'follow_up_paywall' ? trigger.dismissAction.followUpPaywallId || undefined : undefined,
139
+ id: manifestPaywall.id,
140
+ name: manifestPaywall.name,
141
+ html: manifestPaywall.html,
142
+ productIds: manifestPaywall.productIds,
143
+ products: manifestPaywall.products,
144
+ triggerKey,
145
+ abVariant: resolution.abVariant || 'A',
146
+ attributionUserId: this.userId,
147
+ attributionScope: manifestService_1.ManifestService.scopeKey(this.manifestOptions()),
148
+ experimentContext: (() => {
149
+ const rule = trigger.rules.find(rule => rule.id === resolution.matchedRuleId);
150
+ const audience = rule || trigger;
151
+ const matches = audience.variants.filter(variant => variant.paywallId === manifestPaywall.id && variant.variant === resolution.abVariant);
152
+ const variant = matches.length === 1 ? matches[0] : undefined;
153
+ if (!variant?.variantId || !audience.experimentId || !audience.experimentVersion ||
154
+ !audience.audienceId || manifestPaywall.publishedVersion == null)
155
+ return undefined;
156
+ return { experimentId: audience.experimentId, experimentVersion: audience.experimentVersion,
157
+ audienceId: audience.audienceId, variantId: variant.variantId,
158
+ variantLabel: variant.variantLabel || variant.variant,
159
+ paywallId: manifestPaywall.id, paywallVersion: manifestPaywall.publishedVersion };
160
+ })()
161
+ };
162
+ return { show: true, paywall: paywallData, reason: resolution.reason };
163
+ }
164
+ }
165
+ return { show: false, paywall: null, reason: resolution.reason };
166
+ }
167
+ /** Attribute actions to the displayed paywall, never a new assignment. */
168
+ async trackPaywallEvent(type, paywall, productId) {
169
+ if (paywall.attributionScope !== undefined && paywall.attributionScope !== manifestService_1.ManifestService.scopeKey(this.manifestOptions()))
170
+ return;
171
+ if (paywall.attributionUserId !== undefined && paywall.attributionUserId !== this.userId)
172
+ return;
173
+ const flow = this.activeFlow;
174
+ if (flow && flow.paywall.presentationId === paywall.presentationId && flow.epoch === this.epoch) {
175
+ const keys = { purchase: 'onPurchase', purchase_success: 'onPurchase', purchase_failed: 'onPurchaseFailed', restore: 'onRestore', restore_success: 'onRestore' };
176
+ if (keys[type]) {
177
+ flow.outcome = true;
178
+ this.scheduleFollowUp(flow, keys[type]);
179
+ }
180
+ }
181
+ await this.eventQueue.enqueue({ type, paywallId: paywall.id,
182
+ triggerKey: paywall.triggerKey, abVariant: paywall.abVariant,
183
+ experimentContext: paywall.attributionUserId === this.userId ? paywall.experimentContext : undefined, userId: this.userId, productId });
184
+ await this.eventQueue.flush({ baseUrl: this.baseUrl, appKey: this.appKey,
185
+ bundleId: this.bundleId, platform: this.platform });
186
+ }
187
+ /**
188
+ * Fires a trigger, presents the paywall if resolved, and wires up callbacks.
189
+ */
190
+ handleTrigger(triggerKey, callbacks) {
191
+ this.epoch++;
192
+ this.followUps.cancel();
193
+ this.activeFlow = undefined;
194
+ const result = this.resolveTrigger(triggerKey, callbacks?.metadata);
195
+ if (result.show && result.paywall) {
196
+ const paywall = this.present(result.paywall, callbacks || {}, callbacks?.metadata, 0, []);
197
+ return { ...result, paywall };
198
+ }
199
+ return result;
200
+ }
201
+ present(paywall, callbacks, metadata, depth, seen) {
202
+ paywall = { ...paywall, presentationId: `${this.epoch}:${++this.presentationSequence}` };
203
+ const flow = { paywall, callbacks, metadata, depth, seen: [...seen, paywall.id], epoch: this.epoch };
204
+ this.activeFlow = flow;
205
+ let dismissed = false;
206
+ this.onPresentPaywallListener?.(paywall, { ...callbacks, onDismiss: () => {
207
+ if (dismissed)
208
+ return;
209
+ dismissed = true;
210
+ if (this.activeFlow !== flow || flow.epoch !== this.epoch)
211
+ return;
212
+ if (!flow.outcome)
213
+ this.scheduleFollowUp(flow, 'onDismiss');
214
+ void callbacks.onDismiss?.();
215
+ } });
216
+ return paywall;
217
+ }
218
+ scheduleFollowUp(flow, event) {
219
+ const rule = flow.paywall.followUpRules?.[event];
220
+ const valid = () => this.activeFlow === flow && flow.epoch === this.epoch && flow.paywall.attributionUserId === this.userId;
221
+ if (!valid())
222
+ return;
223
+ if (rule?.enabled && (rule.actionType || 'trigger') === 'trigger' && rule.triggerKey?.trim()) {
224
+ this.followUps.schedule(`${flow.paywall.id}:${event}:${rule.triggerKey}`, rule, flow.depth, this.premium, () => {
225
+ if (!valid())
226
+ return;
227
+ const result = this.resolveTrigger(rule.triggerKey, flow.metadata);
228
+ if (!result.show || !result.paywall || (rule.skipIfPremium && this.premium) ||
229
+ (rule.preventRepeatPaywall !== false && flow.seen.includes(result.paywall.id)))
230
+ return;
231
+ this.present(result.paywall, flow.callbacks, flow.metadata, flow.depth + 1, flow.seen);
232
+ });
233
+ return;
234
+ }
235
+ // Legacy close recovery is one additional screen, never an unbounded chain.
236
+ const id = flow.paywall.dismissFollowUpPaywallId;
237
+ if (event !== 'onDismiss' || flow.depth !== 0 || !id || flow.seen.includes(id))
238
+ return;
239
+ const next = this.manifestService.manifest?.paywalls[id];
240
+ if (!next)
241
+ return;
242
+ this.followUps.schedule(`${flow.paywall.id}:legacy`, { enabled: true, actionType: 'trigger', triggerKey: 'legacy', maxChainDepth: 1 }, 0, false, () => {
243
+ if (valid())
244
+ this.present({ ...next, attributionUserId: this.userId, attributionScope: manifestService_1.ManifestService.scopeKey(this.manifestOptions()), triggerKey: flow.paywall.triggerKey,
245
+ followUpRules: next.contentJson?.followUpRules }, flow.callbacks, flow.metadata, 1, flow.seen);
246
+ });
247
+ }
248
+ }
249
+ exports.MySuperwall = MySuperwall;
@@ -0,0 +1,44 @@
1
+ import { ManifestCondition, ManifestRule, ManifestTrigger, ManifestVariant } from '../models/manifest';
2
+ export interface RuleEngineResolution {
3
+ matchedRuleId?: string;
4
+ show: boolean;
5
+ paywallId: string | null;
6
+ abVariant: string | null;
7
+ matchedRuleName: string | null;
8
+ isHoldout: boolean;
9
+ reason: string;
10
+ }
11
+ export declare class LocalRuleEngine {
12
+ /**
13
+ * Deterministic bucket from 0 to 99 matching backend SHA-256 logic.
14
+ */
15
+ getDeterministicBucket(userId?: string | null, salt?: string): number;
16
+ /**
17
+ * Compares two semantic version strings.
18
+ */
19
+ compareVersions(v1?: string | null, v2?: string | null): number;
20
+ /**
21
+ * Resolves nested field access like "metadata.tier".
22
+ */
23
+ resolveContextValue(field: string, context: Record<string, any>): any;
24
+ /**
25
+ * Evaluates a single condition.
26
+ */
27
+ evaluateCondition(condition: ManifestCondition, context: Record<string, any>): boolean;
28
+ /**
29
+ * Checks if a rule matches given context.
30
+ */
31
+ matchesRule(rule: ManifestRule, context: Record<string, any>): boolean;
32
+ /**
33
+ * Deterministically selects a variant respecting holdout.
34
+ */
35
+ selectDeterministicVariant(userId: string | null | undefined, salt: string, variants: ManifestVariant[], fallbackPaywallId: string | null, holdoutPercentage?: number): {
36
+ paywallId: string | null;
37
+ abVariant: string | null;
38
+ isHoldout: boolean;
39
+ };
40
+ /**
41
+ * Resolves a trigger in-memory with 0 ms latency.
42
+ */
43
+ resolveTrigger(trigger: ManifestTrigger, userId: string | null | undefined, context: Record<string, any>): RuleEngineResolution;
44
+ }