rn-onboarding-analytics 1.0.0 → 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.
@@ -161,7 +161,7 @@ export interface OnboardingProps {
161
161
  /** Ordered list of steps to render. */
162
162
  steps: OnboardingStep[];
163
163
  /** Called when the user completes the final step. */
164
- onComplete: () => void;
164
+ onComplete: (planId?: string) => void;
165
165
  /** Called when the user skips the onboarding. */
166
166
  onSkip?: () => void;
167
167
  /** Notifies consumers when the active step index changes. */
@@ -184,9 +184,67 @@ export interface OnboardingProps {
184
184
  fonts?: OnboardingFonts | string;
185
185
  /**
186
186
  * API Key for analytics.
187
- * Required to enable analytics tracking.
187
+ * Optional. If not provided, analytics will be disabled.
188
188
  */
189
- apiKey: string;
189
+ apiKey?: string;
190
+ /**
191
+ * If true, analytics events will be logged to console but not sent to the server.
192
+ * Useful for development.
193
+ */
194
+ isDev?: boolean;
195
+ /**
196
+ * Optional Paywall panel content.
197
+ * If provided, it will be shown after the last step.
198
+ */
199
+ paywallPanel?: OnboardingPaywallPanelConfig;
200
+ }
201
+ export interface PaywallPlan {
202
+ id: string;
203
+ title: string;
204
+ price: string;
205
+ interval?: string;
206
+ features?: string[];
190
207
  }
208
+ /**
209
+ * Props for the paywall panel.
210
+ */
211
+ export interface OnboardingPaywallPanelProps {
212
+ /** Callback invoked when the user taps the main action button. */
213
+ onPressContinue: (planId: string) => void;
214
+ /** Title content. */
215
+ title?: string | ReactNode;
216
+ /** Subtitle content. */
217
+ subtitle?: string | ReactNode;
218
+ /** List of plans to display. */
219
+ plans: PaywallPlan[];
220
+ /**
221
+ * Button content. Either a simple string label or a render function.
222
+ */
223
+ button: string | (({ onPress }: {
224
+ onPress: () => void;
225
+ }) => ReactNode);
226
+ /** Optional image shown on the paywall panel. */
227
+ image?: ImageSourcePropType | (() => ReactNode);
228
+ /** Helper text displayed above the continue button. */
229
+ helperTextContinue?: string;
230
+ /** Link for restore purchase action. */
231
+ onRestorePurchase?: {
232
+ text?: string;
233
+ onPress: () => void;
234
+ };
235
+ /** Link for terms of service action. */
236
+ onTerms?: {
237
+ text?: string;
238
+ onPress: () => void;
239
+ };
240
+ /** Link for privacy policy action. */
241
+ onPrivacy?: {
242
+ text?: string;
243
+ onPress: () => void;
244
+ };
245
+ }
246
+ type OnboardingPaywallPanelConfig = Omit<OnboardingPaywallPanelProps, 'onPressContinue'> | (({ onPressContinue, }: {
247
+ onPressContinue: (planId: string) => void;
248
+ }) => ReactNode);
191
249
  export {};
192
250
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/spill-onboarding/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAExD;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;OAEG;IACH,UAAU,EAAE;QACV;;WAEG;QACH,OAAO,EAAE,MAAM,CAAC;QAEhB;;WAEG;QACH,SAAS,EAAE,MAAM,CAAC;QAElB;;WAEG;QACH,KAAK,EAAE,MAAM,CAAC;QAEd;;WAEG;QACH,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;IAEF;;OAEG;IACH,IAAI,EAAE;QACJ;;WAEG;QACH,OAAO,EAAE,MAAM,CAAC;QAEhB;;WAEG;QACH,SAAS,EAAE,MAAM,CAAC;QAElB;;WAEG;QACH,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC;CACH;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,uCAAuC;IACvC,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB,0CAA0C;IAC1C,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB,8CAA8C;IAC9C,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB,yDAAyD;IACzD,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,6BAA6B;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB,wCAAwC;IACxC,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB,4CAA4C;IAC5C,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB,sCAAsC;IACtC,aAAa,CAAC,EAAE,MAAM,CAAC;IAEvB,wCAAwC;IACxC,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,yBAAyB;IACxC,4DAA4D;IAC5D,YAAY,EAAE,MAAM,IAAI,CAAC;IAEzB,kDAAkD;IAClD,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAE3B,qDAAqD;IACrD,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAE9B;;;OAGG;IACH,MAAM,EACF,MAAM,GACN,CAAC,CAAC,EAAE,YAAY,EAAE,EAAE;QAAE,YAAY,EAAE,MAAM,IAAI,CAAA;KAAE,KAAK,SAAS,CAAC,CAAC;IAEpE,+CAA+C;IAC/C,KAAK,CAAC,EAAE,mBAAmB,GAAG,CAAC,MAAM,SAAS,CAAC,CAAC;CACjD;AAED,KAAK,qBAAqB,GAAG;IAC3B,qEAAqE;IACrE,SAAS,CAAC,EAAE,KAAK,CAAC;IAElB,sDAAsD;IACtD,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf,uBAAuB;IACvB,KAAK,EAAE,MAAM,CAAC;IAEd,kCAAkC;IAClC,WAAW,EAAE,MAAM,CAAC;IAEpB,2CAA2C;IAC3C,WAAW,EAAE,MAAM,CAAC;IAEpB,kDAAkD;IAClD,KAAK,EAAE,mBAAmB,CAAC;IAE3B,kDAAkD;IAClD,QAAQ,EAAE,KAAK,GAAG,QAAQ,CAAC;CAC5B,CAAC;AAEF,KAAK,oBAAoB,GAAG;IAC1B;;OAEG;IACH,SAAS,EAAE,CAAC,KAAK,EAAE;QACjB,gCAAgC;QAChC,MAAM,EAAE,MAAM,IAAI,CAAC;QACnB,oCAAoC;QACpC,MAAM,EAAE,MAAM,IAAI,CAAC;QACnB,qCAAqC;QACrC,MAAM,EAAE,OAAO,CAAC;KACjB,KAAK,SAAS,CAAC;IAEhB,iDAAiD;IACjD,KAAK,EAAE,mBAAmB,CAAC;IAE3B,kDAAkD;IAClD,QAAQ,EAAE,KAAK,GAAG,QAAQ,CAAC;CAC5B,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,cAAc,GAAG,qBAAqB,GAAG,oBAAoB,CAAC;AAE1E;;GAEG;AACH,MAAM,WAAW,wBAAwB;IACvC,sDAAsD;IACtD,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf,uBAAuB;IACvB,KAAK,EAAE,MAAM,CAAC;IAEd,6BAA6B;IAC7B,WAAW,EAAE,MAAM,CAAC;IAEpB,2CAA2C;IAC3C,WAAW,EAAE,MAAM,CAAC;IAEpB,mCAAmC;IACnC,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IAEzB,4CAA4C;IAC5C,WAAW,EAAE,MAAM,IAAI,CAAC;IAExB,mEAAmE;IACnE,aAAa,EAAE,OAAO,CAAC;IAEvB,8CAA8C;IAC9C,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,KAAK,oBAAoB,GACrB,IAAI,CAAC,yBAAyB,EAAE,cAAc,CAAC,GAC/C,CAAC,CAAC,EAAE,YAAY,EAAE,EAAE;IAAE,YAAY,EAAE,MAAM,IAAI,CAAA;CAAE,KAAK,SAAS,CAAC,CAAC;AAEpE;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,+DAA+D;IAC/D,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAE3B;;;OAGG;IACH,UAAU,EAAE,oBAAoB,CAAC;IAEjC,uCAAuC;IACvC,KAAK,EAAE,cAAc,EAAE,CAAC;IAExB,qDAAqD;IACrD,UAAU,EAAE,MAAM,IAAI,CAAC;IAEvB,iDAAiD;IACjD,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IAEpB,6DAA6D;IAC7D,YAAY,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAE3C,sDAAsD;IACtD,eAAe,CAAC,EAAE,OAAO,CAAC;IAE1B,6CAA6C;IAC7C,cAAc,CAAC,EAAE,OAAO,CAAC;IAEzB,wDAAwD;IACxD,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAE3B,kEAAkE;IAClE,UAAU,CAAC,EAAE,MAAM,SAAS,CAAC;IAE7B,6CAA6C;IAC7C,UAAU,CAAC,EAAE,CAAC,EAAE,OAAO,EAAE,EAAE;QAAE,OAAO,EAAE,MAAM,IAAI,CAAA;KAAE,KAAK,SAAS,CAAC;IAEjE,uCAAuC;IACvC,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAE1B,yEAAyE;IACzE,KAAK,CAAC,EAAE,eAAe,GAAG,MAAM,CAAC;IAEjC;;;OAGG;IACH,MAAM,EAAE,MAAM,CAAC;CAChB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/spill-onboarding/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rn-onboarding-analytics",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "description": "Onboarding/tutorial flow for React Native with analytics.",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -8,9 +8,9 @@ const API_URL = 'https://api.freesupabase.shop/api/track';
8
8
  export const trackEvent = async (
9
9
  apiKey: string | undefined,
10
10
  eventType: string,
11
- metaData: any = {}
11
+ metaData: any = {},
12
+ isDev: boolean = false
12
13
  ) => {
13
- const token = apiKey;
14
14
  const appName =
15
15
  Constants.expoConfig?.name ||
16
16
  Constants.manifest?.name ||
@@ -25,7 +25,7 @@ export const trackEvent = async (
25
25
  language: primaryLocale?.languageCode,
26
26
  };
27
27
 
28
- if (!token) {
28
+ if (!isDev && !apiKey) {
29
29
  console.error(
30
30
  JSON.stringify({
31
31
  error: 'Go to `https://freesupabase.shop/docs` to get an app token',
@@ -35,13 +35,26 @@ export const trackEvent = async (
35
35
  }
36
36
 
37
37
  const payload = {
38
- app_id: token,
38
+ app_id: apiKey,
39
39
  eventType,
40
40
  userAgent: `React Native (${Platform.OS})`,
41
41
  sourceUrl: appName,
42
42
  metaData: extendedMetaData,
43
43
  };
44
44
 
45
+ if (isDev) {
46
+ console.log(
47
+ '🚧 [Dev Mode] Analytics Event (Not Sent):',
48
+ JSON.stringify(payload, null, 2)
49
+ );
50
+ return;
51
+ }
52
+
53
+ if (!apiKey) {
54
+ console.log('⚠️ Analytics: No API Key provided, event not sent.');
55
+ return;
56
+ }
57
+
45
58
  console.log('📡 Sending Analytics Event:', JSON.stringify(payload, null, 2));
46
59
 
47
60
  try {
@@ -49,7 +62,7 @@ export const trackEvent = async (
49
62
  method: 'POST',
50
63
  headers: {
51
64
  'Content-Type': 'application/json',
52
- 'Authorization': `Bearer ${token}`,
65
+ 'Authorization': `Bearer ${apiKey}`,
53
66
  },
54
67
  body: JSON.stringify(payload),
55
68
  });
@@ -0,0 +1,346 @@
1
+ import { useMemo, useState } from 'react';
2
+ import {
3
+ Image,
4
+ StyleSheet,
5
+ Text,
6
+ View,
7
+ TouchableOpacity,
8
+ ScrollView,
9
+ Dimensions,
10
+ } from 'react-native';
11
+ import { useTheme } from '../../utils/ThemeContext';
12
+ import type { Theme } from '../../utils/theme';
13
+ import PrimaryButton from '../buttons/PrimaryButton';
14
+ import { fontSizes, lineHeights } from '../../utils/fontStyles';
15
+ import type { OnboardingPaywallPanelProps } from '../types';
16
+
17
+ const { height: screenHeight } = Dimensions.get('window');
18
+
19
+ function OnboardingPaywallPanel({
20
+ onPressContinue,
21
+ title,
22
+ subtitle,
23
+ button,
24
+ image,
25
+ plans,
26
+ helperTextContinue,
27
+ onRestorePurchase,
28
+ onTerms,
29
+ onPrivacy,
30
+ }: OnboardingPaywallPanelProps) {
31
+ const { theme } = useTheme();
32
+ const styles = useMemo(() => createStyles(theme), [theme]);
33
+ const [selectedPlanId, setSelectedPlanId] = useState<string>(
34
+ plans[0]?.id || ''
35
+ );
36
+
37
+ const selectedPlan = plans.find((p) => p.id === selectedPlanId);
38
+
39
+ const renderTitle = () => {
40
+ if (!title) return null;
41
+ if (typeof title === 'string') {
42
+ return (
43
+ <Text style={[styles.text, styles.line1, styles.titleText]}>
44
+ {title}
45
+ </Text>
46
+ );
47
+ }
48
+ return title;
49
+ };
50
+
51
+ const renderSubtitle = () => {
52
+ if (!subtitle) return null;
53
+ if (typeof subtitle === 'string') {
54
+ return (
55
+ <Text style={[styles.text, styles.line2, styles.subtitleText]}>
56
+ {subtitle}
57
+ </Text>
58
+ );
59
+ }
60
+ return subtitle;
61
+ };
62
+
63
+ const renderFeatures = () => {
64
+ if (!selectedPlan?.features || selectedPlan.features.length === 0)
65
+ return null;
66
+
67
+ return (
68
+ <View style={styles.featuresContainer}>
69
+ {selectedPlan.features.map((feature, index) => (
70
+ <View key={index} style={styles.featureRow}>
71
+ <Text style={styles.checkIcon}>✓</Text>
72
+ <Text style={styles.featureText}>{feature}</Text>
73
+ </View>
74
+ ))}
75
+ </View>
76
+ );
77
+ };
78
+
79
+ const renderPlans = () => {
80
+ return (
81
+ <View style={styles.plansContainer}>
82
+ {plans.map((plan) => {
83
+ const isSelected = plan.id === selectedPlanId;
84
+ return (
85
+ <TouchableOpacity
86
+ key={plan.id}
87
+ style={[styles.planCard, isSelected && styles.planCardSelected]}
88
+ onPress={() => setSelectedPlanId(plan.id)}
89
+ activeOpacity={0.8}
90
+ >
91
+ <View>
92
+ <Text
93
+ style={[
94
+ styles.planTitle,
95
+ isSelected && styles.planTitleSelected,
96
+ ]}
97
+ >
98
+ {plan.title}
99
+ </Text>
100
+ {plan.interval && (
101
+ <Text
102
+ style={[
103
+ styles.planInterval,
104
+ isSelected && styles.planIntervalSelected,
105
+ ]}
106
+ >
107
+ {plan.interval}
108
+ </Text>
109
+ )}
110
+ </View>
111
+ <Text
112
+ style={[
113
+ styles.planPrice,
114
+ isSelected && styles.planPriceSelected,
115
+ ]}
116
+ >
117
+ {plan.price}
118
+ </Text>
119
+ </TouchableOpacity>
120
+ );
121
+ })}
122
+ </View>
123
+ );
124
+ };
125
+
126
+ const renderButton = () => {
127
+ const handlePress = () => onPressContinue(selectedPlanId);
128
+
129
+ if (typeof button === 'string') {
130
+ return <PrimaryButton text={button} onPress={handlePress} />;
131
+ }
132
+ return button({ onPress: handlePress });
133
+ };
134
+
135
+ const renderFooterLinks = () => {
136
+ if (!onRestorePurchase && !onTerms && !onPrivacy) return null;
137
+
138
+ return (
139
+ <View style={styles.footerLinksContainer}>
140
+ {onRestorePurchase && (
141
+ <TouchableOpacity onPress={onRestorePurchase.onPress}>
142
+ <Text style={styles.footerLinkText}>{onRestorePurchase.text}</Text>
143
+ </TouchableOpacity>
144
+ )}
145
+
146
+ {onRestorePurchase && (onTerms || onPrivacy) && (
147
+ <Text style={styles.footerLinkSeparator}>•</Text>
148
+ )}
149
+
150
+ {onTerms && (
151
+ <TouchableOpacity onPress={onTerms.onPress}>
152
+ <Text style={styles.footerLinkText}>{onTerms.text}</Text>
153
+ </TouchableOpacity>
154
+ )}
155
+
156
+ {onTerms && onPrivacy && (
157
+ <Text style={styles.footerLinkSeparator}>•</Text>
158
+ )}
159
+
160
+ {onPrivacy && (
161
+ <TouchableOpacity onPress={onPrivacy.onPress}>
162
+ <Text style={styles.footerLinkText}>{onPrivacy.text}</Text>
163
+ </TouchableOpacity>
164
+ )}
165
+ </View>
166
+ );
167
+ };
168
+
169
+ return (
170
+ <ScrollView
171
+ style={styles.container}
172
+ contentContainerStyle={styles.contentContainer}
173
+ showsVerticalScrollIndicator={false}
174
+ bounces={false}
175
+ >
176
+ {typeof image === 'function'
177
+ ? image()
178
+ : image && (
179
+ <Image source={image} style={styles.image} resizeMode="cover" />
180
+ )}
181
+
182
+ <View style={styles.contentWrapper}>
183
+ <View style={styles.headerContainer}>
184
+ {renderTitle()}
185
+ {renderSubtitle()}
186
+ </View>
187
+
188
+ {renderFeatures()}
189
+
190
+ {renderPlans()}
191
+
192
+ {helperTextContinue && (
193
+ <Text style={styles.helperText}>{helperTextContinue}</Text>
194
+ )}
195
+
196
+ {renderButton()}
197
+
198
+ {renderFooterLinks()}
199
+ </View>
200
+ </ScrollView>
201
+ );
202
+ }
203
+
204
+ export default OnboardingPaywallPanel;
205
+
206
+ const createStyles = (theme: Theme) =>
207
+ StyleSheet.create({
208
+ container: {
209
+ flex: 1,
210
+ backgroundColor: theme.bg.secondary,
211
+ },
212
+ contentContainer: {
213
+ paddingBottom: 40,
214
+ },
215
+ contentWrapper: {
216
+ paddingHorizontal: 16,
217
+ marginTop: -32,
218
+ paddingTop: 32,
219
+ borderTopLeftRadius: 32,
220
+ borderTopRightRadius: 32,
221
+ backgroundColor: theme.bg.secondary,
222
+ },
223
+ image: {
224
+ alignSelf: 'center',
225
+ width: '100%',
226
+ height: screenHeight * 0.3,
227
+ },
228
+ headerContainer: {
229
+ alignItems: 'center',
230
+ marginBottom: 24,
231
+ marginTop: 8,
232
+ },
233
+ text: {
234
+ fontSize: fontSizes.xxl,
235
+ lineHeight: lineHeights.xxl,
236
+ textAlign: 'center',
237
+ },
238
+ line1: {
239
+ color: theme.text.primary,
240
+ },
241
+ line2: {
242
+ marginTop: 8,
243
+ color: theme.text.secondary,
244
+ fontSize: fontSizes.md,
245
+ lineHeight: lineHeights.md,
246
+ },
247
+ titleText: {
248
+ fontFamily: theme.fonts.introTitle,
249
+ fontWeight: 'bold',
250
+ },
251
+ subtitleText: {
252
+ fontFamily: theme.fonts.introSubtitle,
253
+ },
254
+ featuresContainer: {
255
+ marginBottom: 24,
256
+ paddingHorizontal: 8,
257
+ },
258
+ featureRow: {
259
+ flexDirection: 'row',
260
+ alignItems: 'center',
261
+ marginBottom: 12,
262
+ },
263
+ checkIcon: {
264
+ color: theme.bg.accent,
265
+ fontSize: fontSizes.lg,
266
+ marginRight: 12,
267
+ fontWeight: 'bold',
268
+ },
269
+ featureText: {
270
+ color: theme.text.primary,
271
+ fontSize: fontSizes.md,
272
+ fontWeight: '500',
273
+ },
274
+ plansContainer: {
275
+ gap: 12,
276
+ marginBottom: 24,
277
+ },
278
+ planCard: {
279
+ flexDirection: 'row',
280
+ alignItems: 'center',
281
+ justifyContent: 'space-between',
282
+ padding: 16,
283
+ borderRadius: 16,
284
+ backgroundColor: theme.bg.secondary,
285
+ borderWidth: 1,
286
+ borderColor: theme.bg.label,
287
+ // Shadow for elevation
288
+ shadowColor: '#000',
289
+ shadowOffset: { width: 0, height: 2 },
290
+ shadowOpacity: 0.05,
291
+ shadowRadius: 4,
292
+ elevation: 2,
293
+ },
294
+ planCardSelected: {
295
+ borderColor: theme.bg.accent,
296
+ backgroundColor: theme.bg.secondary,
297
+ borderWidth: 2,
298
+ },
299
+ planTitle: {
300
+ fontSize: fontSizes.md,
301
+ fontWeight: '600',
302
+ color: theme.text.primary,
303
+ },
304
+ planTitleSelected: {
305
+ color: theme.text.primary,
306
+ fontWeight: '700',
307
+ },
308
+ planInterval: {
309
+ fontSize: fontSizes.sm,
310
+ color: theme.text.secondary,
311
+ marginTop: 2,
312
+ },
313
+ planIntervalSelected: {
314
+ color: theme.text.secondary,
315
+ },
316
+ planPrice: {
317
+ fontSize: fontSizes.lg,
318
+ fontWeight: '700',
319
+ color: theme.text.primary,
320
+ },
321
+ planPriceSelected: {
322
+ color: theme.bg.accent,
323
+ },
324
+ helperText: {
325
+ textAlign: 'center',
326
+ color: theme.text.secondary,
327
+ fontSize: fontSizes.sm,
328
+ marginBottom: 12,
329
+ fontWeight: '500',
330
+ },
331
+ footerLinksContainer: {
332
+ flexDirection: 'row',
333
+ justifyContent: 'center',
334
+ alignItems: 'center',
335
+ marginTop: 24,
336
+ gap: 8,
337
+ },
338
+ footerLinkText: {
339
+ fontSize: fontSizes.md,
340
+ color: theme.text.secondary,
341
+ },
342
+ footerLinkSeparator: {
343
+ fontSize: fontSizes.xs,
344
+ color: theme.text.secondary,
345
+ },
346
+ });
@@ -8,6 +8,7 @@ import {
8
8
  } from 'react-native';
9
9
  import { useTheme } from '../utils/ThemeContext';
10
10
  import OnboardingIntroPanel from './components/OnboardingIntroPanel';
11
+ import OnboardingPaywallPanel from './components/OnboardingPaywallPanel';
11
12
  import { useSharedValue, withTiming } from 'react-native-reanimated';
12
13
  import OnboardingStepPanel from './components/OnboardingStepPanel';
13
14
  import OnboardingStepContainer from './components/OnboardingStepContainer';
@@ -31,6 +32,8 @@ function SpillOnboarding({
31
32
  background,
32
33
  skipButton,
33
34
  apiKey,
35
+ isDev,
36
+ paywallPanel: paywallPanelProps,
34
37
  }: OnboardingProps) {
35
38
  const { theme } = useTheme();
36
39
 
@@ -40,6 +43,7 @@ function SpillOnboarding({
40
43
  const [step, setStep] = useState(-1);
41
44
  const currentStep = step >= 0 ? steps[step] : undefined;
42
45
  const firstStep = steps[0];
46
+ const isPaywall = step === steps.length;
43
47
 
44
48
  const onStepChange = useCallback(
45
49
  (stepNumber: number) => {
@@ -47,6 +51,9 @@ function SpillOnboarding({
47
51
  if (index === -1) {
48
52
  return 'Intro';
49
53
  }
54
+ if (index === steps.length) {
55
+ return 'Paywall';
56
+ }
50
57
  const s = steps[index];
51
58
  if (!s) {
52
59
  return 'Unknown';
@@ -69,14 +76,19 @@ function SpillOnboarding({
69
76
  setStep(stepNumber);
70
77
  onStepChangeProps?.(stepNumber);
71
78
 
72
- trackEvent(apiKey, 'step_change', {
73
- from_index: step,
74
- to_index: stepNumber,
75
- from_step: fromStepName,
76
- to_step: toStepName,
77
- });
79
+ trackEvent(
80
+ apiKey,
81
+ 'step_change',
82
+ {
83
+ from_index: step,
84
+ to_index: stepNumber,
85
+ from_step: fromStepName,
86
+ to_step: toStepName,
87
+ },
88
+ isDev
89
+ );
78
90
  },
79
- [onStepChangeProps, apiKey, step, steps]
91
+ [onStepChangeProps, apiKey, step, steps, isDev]
80
92
  );
81
93
 
82
94
  useEffect(() => {
@@ -120,7 +132,11 @@ function SpillOnboarding({
120
132
 
121
133
  const onNextPress = () => {
122
134
  if (step === steps.length - 1) {
123
- trackEvent(apiKey, 'complete');
135
+ if (paywallPanelProps) {
136
+ onStepChange(steps.length);
137
+ return;
138
+ }
139
+ trackEvent(apiKey, 'complete', {}, isDev);
124
140
  return onComplete();
125
141
  }
126
142
 
@@ -158,6 +174,27 @@ function SpillOnboarding({
158
174
  );
159
175
  };
160
176
 
177
+ const onPaywallContinue = (planId: string) => {
178
+ trackEvent(apiKey, 'paywall_select', { plan_id: planId }, isDev);
179
+ trackEvent(apiKey, 'complete', { plan_id: planId }, isDev);
180
+ onComplete(planId);
181
+ };
182
+
183
+ const renderPaywallPanel = () => {
184
+ if (!paywallPanelProps) return null;
185
+
186
+ if (typeof paywallPanelProps === 'function') {
187
+ return paywallPanelProps({ onPressContinue: onPaywallContinue });
188
+ }
189
+
190
+ return (
191
+ <OnboardingPaywallPanel
192
+ onPressContinue={onPaywallContinue}
193
+ {...paywallPanelProps}
194
+ />
195
+ );
196
+ };
197
+
161
198
  const renderStepContent = () => {
162
199
  if (!currentStep) {
163
200
  return null;
@@ -185,12 +222,17 @@ function SpillOnboarding({
185
222
  };
186
223
 
187
224
  const currentStepImage: ImageSourcePropType | undefined = useMemo(() => {
225
+ if (isPaywall) {
226
+ // Paywall image is rendered inside the panel scrollview
227
+ return undefined;
228
+ }
229
+
188
230
  if (!currentStep) {
189
231
  return firstStep?.image;
190
232
  }
191
233
 
192
234
  return currentStep.image;
193
- }, [currentStep, firstStep?.image]);
235
+ }, [currentStep, firstStep?.image, isPaywall]);
194
236
 
195
237
  const onboardingContent = (
196
238
  <View style={styles.container} ref={screen.ref}>
@@ -210,15 +252,19 @@ function SpillOnboarding({
210
252
  background={background}
211
253
  />
212
254
 
213
- <OnboardingStepContainer
214
- currentStep={currentStep}
215
- animationDuration={animationDuration}
216
- showCloseButton={showCloseButton}
217
- renderStepContent={renderStepContent}
218
- onSkip={onSkip}
219
- ref={stepPanel.ref}
220
- skipButton={skipButton}
221
- />
255
+ {isPaywall ? (
256
+ <View style={styles.fullScreenPanel}>{renderPaywallPanel()}</View>
257
+ ) : (
258
+ <OnboardingStepContainer
259
+ currentStep={currentStep}
260
+ animationDuration={animationDuration}
261
+ showCloseButton={showCloseButton}
262
+ renderStepContent={renderStepContent}
263
+ onSkip={onSkip}
264
+ ref={stepPanel.ref}
265
+ skipButton={skipButton}
266
+ />
267
+ )}
222
268
  </View>
223
269
  );
224
270
 
@@ -248,4 +294,8 @@ const createStyles = (theme: Theme) =>
248
294
  left: 0,
249
295
  right: 0,
250
296
  },
297
+ fullScreenPanel: {
298
+ flex: 1,
299
+ zIndex: 10,
300
+ },
251
301
  });