@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.
- package/CHANGELOG.md +10 -0
- package/LICENSE +21 -0
- package/README.md +157 -2
- package/lib/MySuperwall.d.ts +64 -0
- package/lib/MySuperwall.js +249 -0
- package/lib/engine/localRuleEngine.d.ts +44 -0
- package/lib/engine/localRuleEngine.js +318 -0
- package/lib/hooks/usePaywall.d.ts +7 -0
- package/lib/hooks/usePaywall.js +25 -0
- package/lib/index.d.ts +11 -0
- package/lib/index.js +27 -0
- package/lib/models/manifest.d.ts +76 -0
- package/lib/models/manifest.js +2 -0
- package/lib/models/paywall.d.ts +36 -0
- package/lib/models/paywall.js +2 -0
- package/lib/services/anonymousIdentity.d.ts +6 -0
- package/lib/services/anonymousIdentity.js +36 -0
- package/lib/services/followUpService.d.ts +9 -0
- package/lib/services/followUpService.js +26 -0
- package/lib/services/manifestService.d.ts +21 -0
- package/lib/services/manifestService.js +80 -0
- package/lib/services/offlineEventQueue.d.ts +18 -0
- package/lib/services/offlineEventQueue.js +101 -0
- package/lib/ui/PaywallModal.d.ts +8 -0
- package/lib/ui/PaywallModal.js +136 -0
- package/package.json +51 -4
- package/src/MySuperwall.ts +272 -0
- package/src/engine/localRuleEngine.ts +375 -0
- package/src/hooks/usePaywall.ts +27 -0
- package/src/index.ts +12 -0
- package/src/models/manifest.ts +81 -0
- package/src/models/paywall.ts +40 -0
- package/src/services/anonymousIdentity.ts +28 -0
- package/src/services/followUpService.ts +21 -0
- package/src/services/manifestService.ts +63 -0
- package/src/services/offlineEventQueue.ts +88 -0
- package/src/ui/PaywallModal.tsx +122 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
function eventId(): string {
|
|
2
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, char => {
|
|
3
|
+
const value = Math.floor(Math.random() * 16);
|
|
4
|
+
return (char === 'x' ? value : (value & 3) | 8).toString(16);
|
|
5
|
+
});
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class OfflineEventQueue {
|
|
9
|
+
private queueKey = '@mysuperwall_offline_event_queue_v1';
|
|
10
|
+
private asyncStorage: any = null;
|
|
11
|
+
private isFlushing = false;
|
|
12
|
+
private memoryQueue: Record<string, any>[] = [];
|
|
13
|
+
private mutation: Promise<unknown> = Promise.resolve();
|
|
14
|
+
|
|
15
|
+
constructor(scope?: string) {
|
|
16
|
+
if (scope) this.queueKey = '@mysuperwall_event_queue_v2:' + encodeURIComponent(scope);
|
|
17
|
+
try { this.asyncStorage = require('@react-native-async-storage/async-storage').default; }
|
|
18
|
+
catch (_) { this.asyncStorage = null; }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
private serialized<T>(operation: () => Promise<T>): Promise<T> {
|
|
22
|
+
const next = this.mutation.then(operation, operation);
|
|
23
|
+
this.mutation = next.catch(() => undefined);
|
|
24
|
+
return next;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private async read(): Promise<Record<string, any>[]> {
|
|
28
|
+
if (!this.asyncStorage) return [...this.memoryQueue];
|
|
29
|
+
try {
|
|
30
|
+
const raw = await this.asyncStorage.getItem(this.queueKey);
|
|
31
|
+
const parsed = raw ? JSON.parse(raw) : [];
|
|
32
|
+
if (!Array.isArray(parsed)) return [];
|
|
33
|
+
const list = parsed.filter(item => item && typeof item === 'object').map(item =>
|
|
34
|
+
({ ...item, eventId: item.eventId || eventId() }));
|
|
35
|
+
this.memoryQueue = list;
|
|
36
|
+
return list;
|
|
37
|
+
} catch (_) {
|
|
38
|
+
this.asyncStorage = null;
|
|
39
|
+
return [...this.memoryQueue];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private async write(list: Record<string, any>[]): Promise<void> {
|
|
44
|
+
this.memoryQueue = list;
|
|
45
|
+
if (this.asyncStorage) {
|
|
46
|
+
try { await this.asyncStorage.setItem(this.queueKey, JSON.stringify(list)); }
|
|
47
|
+
catch (_) { this.asyncStorage = null; }
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async enqueue(event: Record<string, any>): Promise<void> {
|
|
52
|
+
// IDs and context are captured before asynchronous persistence, once per event.
|
|
53
|
+
const captured = JSON.parse(JSON.stringify({ ...event, eventId: event.eventId || eventId(),
|
|
54
|
+
timestamp: event.timestamp || new Date().toISOString() }));
|
|
55
|
+
await this.serialized(async () => {
|
|
56
|
+
const list = await this.read();
|
|
57
|
+
await this.write([...list, captured].slice(-200));
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async flush(options: { baseUrl: string; appKey?: string; bundleId?: string; platform?: string }): Promise<void> {
|
|
62
|
+
if (this.isFlushing) return;
|
|
63
|
+
this.isFlushing = true;
|
|
64
|
+
try {
|
|
65
|
+
const batch = await this.serialized(async () => {
|
|
66
|
+
const list = await this.read();
|
|
67
|
+
await this.write(list); // Persist upgraded legacy IDs before the request.
|
|
68
|
+
return list.slice(0, 100);
|
|
69
|
+
});
|
|
70
|
+
if (!batch.length) return;
|
|
71
|
+
const base = options.baseUrl.endsWith('/') ? options.baseUrl : `${options.baseUrl}/`;
|
|
72
|
+
const response = await fetch(`${base}paywall/events/batch`, {
|
|
73
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
74
|
+
body: JSON.stringify({ platform: options.platform || 'react-native', appKey: options.appKey,
|
|
75
|
+
bundleId: options.platform === 'android' ? undefined : options.bundleId,
|
|
76
|
+
packageName: options.platform === 'android' ? options.bundleId : undefined, events: batch })
|
|
77
|
+
});
|
|
78
|
+
if (response.ok) {
|
|
79
|
+
const sent = new Set(batch.map(event => event.eventId));
|
|
80
|
+
await this.serialized(async () => {
|
|
81
|
+
const current = await this.read();
|
|
82
|
+
await this.write(current.filter(event => !sent.has(event.eventId)));
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
} catch (_) { /* Retain the original IDs for retry. */ }
|
|
86
|
+
finally { this.isFlushing = false; }
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import React, { FC, useRef } from 'react';
|
|
2
|
+
import { MySuperwall } from '../MySuperwall';
|
|
3
|
+
import { Modal, StyleSheet, SafeAreaView } from 'react-native';
|
|
4
|
+
import { PaywallData, PaywallCallbackActions } from '../models/paywall';
|
|
5
|
+
|
|
6
|
+
export interface PaywallModalProps extends PaywallCallbackActions {
|
|
7
|
+
visible: boolean;
|
|
8
|
+
paywall: PaywallData | null;
|
|
9
|
+
onClose: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const PaywallModal: FC<PaywallModalProps> = props => {
|
|
13
|
+
if (!props.visible || !props.paywall) return null;
|
|
14
|
+
// Remount the displayed session when HTML or attribution changes, even if the
|
|
15
|
+
// paywall ID stays the same. Freeze the payload to the HTML sent to WebView.
|
|
16
|
+
const captured: PaywallData = JSON.parse(JSON.stringify(props.paywall));
|
|
17
|
+
const sessionKey = JSON.stringify(captured);
|
|
18
|
+
return <PaywallModalContent key={sessionKey} {...props} paywall={captured} />;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const PaywallModalContent: FC<PaywallModalProps> = ({
|
|
22
|
+
visible,
|
|
23
|
+
paywall,
|
|
24
|
+
onClose,
|
|
25
|
+
onPurchase,
|
|
26
|
+
onRestore,
|
|
27
|
+
onDismiss,
|
|
28
|
+
onOpenUrl
|
|
29
|
+
}) => {
|
|
30
|
+
const shown = useRef(false);
|
|
31
|
+
const dismissed = useRef(false);
|
|
32
|
+
if (!visible || !paywall) return null;
|
|
33
|
+
const track = (type: string, productId?: string) => {
|
|
34
|
+
void MySuperwall.shared.trackPaywallEvent(type, paywall, productId);
|
|
35
|
+
};
|
|
36
|
+
const dismiss = () => {
|
|
37
|
+
if (dismissed.current) return;
|
|
38
|
+
dismissed.current = true;
|
|
39
|
+
track('paywall_dismissed');
|
|
40
|
+
onClose();
|
|
41
|
+
void onDismiss?.();
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
let WebViewComponent: any = null;
|
|
45
|
+
try {
|
|
46
|
+
WebViewComponent = require('react-native-webview').WebView;
|
|
47
|
+
} catch (_) {
|
|
48
|
+
console.warn('[MySuperwall] react-native-webview is required to render paywalls.');
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const injectedJavaScript = `
|
|
53
|
+
window.MySuperWall = {
|
|
54
|
+
postMessage: function(msg) {
|
|
55
|
+
if (window.ReactNativeWebView && window.ReactNativeWebView.postMessage) {
|
|
56
|
+
window.ReactNativeWebView.postMessage(typeof msg === 'string' ? msg : JSON.stringify(msg));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
true;
|
|
61
|
+
`;
|
|
62
|
+
|
|
63
|
+
const handleMessage = (event: any) => {
|
|
64
|
+
try {
|
|
65
|
+
const data = JSON.parse(event.nativeEvent.data);
|
|
66
|
+
const action = typeof data.action === 'string' ? data.action : data.type;
|
|
67
|
+
const type = typeof action === 'string' ? action.toLowerCase() : '';
|
|
68
|
+
|
|
69
|
+
switch (type) {
|
|
70
|
+
case 'purchase': {
|
|
71
|
+
const productId = data.productId || data.product_id;
|
|
72
|
+
if (typeof productId === 'string' && productId.trim()) { track('purchase_started', productId); if (onPurchase) onPurchase(productId); }
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
case 'restore': {
|
|
76
|
+
track('restore_started');
|
|
77
|
+
if (onRestore) onRestore();
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
case 'close':
|
|
81
|
+
case 'dismiss': {
|
|
82
|
+
dismiss();
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
case 'open_url':
|
|
86
|
+
case 'openurl': {
|
|
87
|
+
if (typeof data.url === 'string' && data.url.trim() && onOpenUrl) onOpenUrl(data.url);
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
} catch (_) {}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
return (
|
|
95
|
+
<Modal visible={visible} animationType="slide" transparent={false} onRequestClose={dismiss}>
|
|
96
|
+
<SafeAreaView style={styles.container}>
|
|
97
|
+
<WebViewComponent
|
|
98
|
+
originWhitelist={['*']}
|
|
99
|
+
source={{ html: paywall.html, baseUrl: 'https://mysuperwall.com' }}
|
|
100
|
+
injectedJavaScriptBeforeContentLoaded={injectedJavaScript}
|
|
101
|
+
onMessage={handleMessage}
|
|
102
|
+
onLoad={() => { if (!shown.current) { shown.current = true; track('paywall_shown'); } }}
|
|
103
|
+
style={styles.webview}
|
|
104
|
+
javaScriptEnabled={true}
|
|
105
|
+
domStorageEnabled={true}
|
|
106
|
+
bounces={false}
|
|
107
|
+
/>
|
|
108
|
+
</SafeAreaView>
|
|
109
|
+
</Modal>
|
|
110
|
+
);
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const styles = StyleSheet.create({
|
|
114
|
+
container: {
|
|
115
|
+
flex: 1,
|
|
116
|
+
backgroundColor: '#000000'
|
|
117
|
+
},
|
|
118
|
+
webview: {
|
|
119
|
+
flex: 1,
|
|
120
|
+
backgroundColor: 'transparent'
|
|
121
|
+
}
|
|
122
|
+
});
|