@umituz/react-native-subscription 2.44.0 → 2.44.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-subscription",
3
- "version": "2.44.0",
3
+ "version": "2.44.1",
4
4
  "description": "Complete subscription management with RevenueCat, paywall UI, and credits system for React Native apps",
5
5
  "main": "./src/index.ts",
6
6
  "types": "./src/index.ts",
@@ -0,0 +1,115 @@
1
+ /**
2
+ * PaywallScreen Render Item Component
3
+ * Separated for better maintainability
4
+ */
5
+
6
+ import React from "react";
7
+ import { View, ListRenderItem } from "react-native";
8
+ import type { ImageSourcePropType } from "react-native";
9
+ import { AtomicText, AtomicIcon } from "@umituz/react-native-design-system/atoms";
10
+ import { Image } from "expo-image";
11
+ import { PlanCard } from "./PlanCard";
12
+ import type { PaywallListItem } from "../utils/paywallLayoutUtils";
13
+ import { paywallScreenStyles as styles } from "./PaywallScreen.styles";
14
+
15
+ interface PaywallRenderItemProps {
16
+ item: PaywallListItem;
17
+ tokens: any;
18
+ translations: any;
19
+ heroImage?: ImageSourcePropType;
20
+ selectedPlanId?: string;
21
+ bestValueIdentifier?: string;
22
+ creditAmounts?: Record<string, number>;
23
+ creditsLabel?: string;
24
+ onSelectPlan?: (planId: string) => void;
25
+ }
26
+
27
+ export const PaywallRenderItem: React.FC<PaywallRenderItemProps> = React.memo(({
28
+ item,
29
+ tokens,
30
+ translations,
31
+ heroImage,
32
+ selectedPlanId,
33
+ bestValueIdentifier,
34
+ creditAmounts,
35
+ creditsLabel,
36
+ onSelectPlan,
37
+ }) => {
38
+ if (!translations) return null;
39
+
40
+ switch (item.type) {
41
+ case 'HEADER':
42
+ return (
43
+ <View key="header">
44
+ {/* Hero Image */}
45
+ {heroImage && (
46
+ <View style={styles.heroContainer}>
47
+ <Image source={heroImage} style={styles.heroImage} contentFit="cover" transition={200} />
48
+ </View>
49
+ )}
50
+
51
+ {/* Header Text */}
52
+ <View style={styles.header}>
53
+ <AtomicText type="headlineLarge" style={[styles.title, { color: tokens.colors.textPrimary }]}>
54
+ {translations.title}
55
+ </AtomicText>
56
+ {translations.subtitle && (
57
+ <AtomicText type="bodyMedium" style={[styles.subtitle, { color: tokens.colors.textSecondary }]}>
58
+ {translations.subtitle}
59
+ </AtomicText>
60
+ )}
61
+ </View>
62
+ </View>
63
+ );
64
+
65
+ case 'FEATURE_HEADER':
66
+ return (
67
+ <View key="feat-header" style={styles.sectionHeader}>
68
+ <AtomicText type="labelLarge" style={[styles.sectionTitle, { color: tokens.colors.textSecondary }]}>
69
+ {translations.featuresTitle || "WHAT'S INCLUDED"}
70
+ </AtomicText>
71
+ </View>
72
+ );
73
+
74
+ case 'FEATURE':
75
+ return (
76
+ <View key={`feat-${item.feature.text}`} style={[styles.featureRow, { marginHorizontal: 24, marginBottom: 16 }]}>
77
+ <View style={[styles.featureIcon, { backgroundColor: tokens.colors.primary }]}>
78
+ <AtomicIcon name={item.feature.icon} customSize={16} customColor={tokens.colors.onPrimary} />
79
+ </View>
80
+ <AtomicText type="bodyMedium" style={[styles.featureText, { color: tokens.colors.textPrimary }]}>
81
+ {item.feature.text}
82
+ </AtomicText>
83
+ </View>
84
+ );
85
+
86
+ case 'PLAN_HEADER':
87
+ return (
88
+ <View key="plan-header" style={styles.sectionHeader}>
89
+ <AtomicText type="labelLarge" style={[styles.sectionTitle, { color: tokens.colors.textSecondary }]}>
90
+ {translations.plansTitle || "CHOOSE YOUR PLAN"}
91
+ </AtomicText>
92
+ </View>
93
+ );
94
+
95
+ case 'PLAN': {
96
+ const pid = item.pkg.product.identifier;
97
+ return (
98
+ <PlanCard
99
+ key={pid}
100
+ pkg={item.pkg}
101
+ isSelected={selectedPlanId === pid}
102
+ badge={bestValueIdentifier === pid ? translations.bestValueBadgeText : undefined}
103
+ creditAmount={creditAmounts?.[pid]}
104
+ creditsLabel={creditsLabel}
105
+ onSelect={() => onSelectPlan?.(pid)}
106
+ />
107
+ );
108
+ }
109
+
110
+ default:
111
+ return null;
112
+ }
113
+ });
114
+
115
+ PaywallRenderItem.displayName = "PaywallRenderItem";
@@ -2,6 +2,7 @@
2
2
  * Paywall Screen Component
3
3
  *
4
4
  * Full-screen paywall with optimized FlatList for performance and modern design.
5
+ * Render logic separated to PaywallScreen.renderItem.tsx for better maintainability.
5
6
  */
6
7
 
7
8
  import React, { useCallback, useEffect, useMemo } from "react";
@@ -14,11 +15,9 @@ import {
14
15
  StatusBar,
15
16
  } from "react-native";
16
17
  import { useNavigation } from "@react-navigation/native";
17
- import { AtomicText, AtomicIcon, AtomicSpinner } from "@umituz/react-native-design-system/atoms";
18
+ import { AtomicIcon, AtomicSpinner } from "@umituz/react-native-design-system/atoms";
18
19
  import { useSafeAreaInsets } from "@umituz/react-native-design-system/safe-area";
19
20
  import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
20
- import { Image } from "expo-image";
21
- import { PlanCard } from "./PlanCard";
22
21
  import { paywallScreenStyles as styles } from "./PaywallScreen.styles";
23
22
  import { PaywallFooter } from "./PaywallFooter";
24
23
  import { usePaywallActions } from "../hooks/usePaywallActions";
@@ -28,6 +27,7 @@ import {
28
27
  type PaywallListItem
29
28
  } from "../utils/paywallLayoutUtils";
30
29
  import { hasItems } from "../../../shared/utils/arrayUtils";
30
+ import { PaywallRenderItem } from "./PaywallScreen.renderItem";
31
31
 
32
32
  export const PaywallScreen: React.FC<PaywallScreenProps> = React.memo((props) => {
33
33
  const navigation = useNavigation();
@@ -115,7 +115,7 @@ export const PaywallScreen: React.FC<PaywallScreenProps> = React.memo((props) =>
115
115
  // Prepare flat data for the list
116
116
  const flatData = useMemo(() => {
117
117
  const data: PaywallListItem[] = [];
118
-
118
+
119
119
  // 1. Header (Hero, Title, Subtitle)
120
120
  data.push({ type: 'HEADER' });
121
121
 
@@ -139,82 +139,20 @@ export const PaywallScreen: React.FC<PaywallScreenProps> = React.memo((props) =>
139
139
  }, [features, packages]);
140
140
 
141
141
  const renderItem: ListRenderItem<PaywallListItem> = useCallback(({ item }) => {
142
- if (!translations) return null;
143
- switch (item.type) {
144
- case 'HEADER':
145
- return (
146
- <View key="header">
147
- {/* Hero Image */}
148
- {heroImage && (
149
- <View style={styles.heroContainer}>
150
- <Image source={heroImage} style={styles.heroImage} contentFit="cover" transition={200} />
151
- </View>
152
- )}
153
-
154
- {/* Header Text */}
155
- <View style={styles.header}>
156
- <AtomicText type="headlineLarge" style={[styles.title, { color: tokens.colors.textPrimary }]}>
157
- {translations.title}
158
- </AtomicText>
159
- {translations.subtitle && (
160
- <AtomicText type="bodyMedium" style={[styles.subtitle, { color: tokens.colors.textSecondary }]}>
161
- {translations.subtitle}
162
- </AtomicText>
163
- )}
164
- </View>
165
- </View>
166
- );
167
-
168
- case 'FEATURE_HEADER':
169
- return (
170
- <View key="feat-header" style={styles.sectionHeader}>
171
- <AtomicText type="labelLarge" style={[styles.sectionTitle, { color: tokens.colors.textSecondary }]}>
172
- {translations.featuresTitle || "WHAT'S INCLUDED"}
173
- </AtomicText>
174
- </View>
175
- );
176
-
177
- case 'FEATURE':
178
- return (
179
- <View key={`feat-${item.feature.text}`} style={[styles.featureRow, { marginHorizontal: 24, marginBottom: 16 }]}>
180
- <View style={[styles.featureIcon, { backgroundColor: tokens.colors.primary }]}>
181
- <AtomicIcon name={item.feature.icon} customSize={16} customColor={tokens.colors.onPrimary} />
182
- </View>
183
- <AtomicText type="bodyMedium" style={[styles.featureText, { color: tokens.colors.textPrimary }]}>
184
- {item.feature.text}
185
- </AtomicText>
186
- </View>
187
- );
188
-
189
- case 'PLAN_HEADER':
190
- return (
191
- <View key="plan-header" style={styles.sectionHeader}>
192
- <AtomicText type="labelLarge" style={[styles.sectionTitle, { color: tokens.colors.textSecondary }]}>
193
- {translations.plansTitle || "CHOOSE YOUR PLAN"}
194
- </AtomicText>
195
- </View>
196
- );
197
-
198
- case 'PLAN': {
199
- const pid = item.pkg.product.identifier;
200
- return (
201
- <PlanCard
202
- key={pid}
203
- pkg={item.pkg}
204
- isSelected={selectedPlanId === pid}
205
- badge={bestValueIdentifier === pid ? translations.bestValueBadgeText : undefined}
206
- creditAmount={creditAmounts?.[pid]}
207
- creditsLabel={creditsLabel}
208
- onSelect={() => setSelectedPlanId(pid)}
209
- />
210
- );
211
- }
212
-
213
- default:
214
- return null;
215
- }
216
- }, [heroImage, translations, tokens, selectedPlanId, bestValueIdentifier, creditAmounts, creditsLabel, setSelectedPlanId]);
217
-
142
+ return (
143
+ <PaywallRenderItem
144
+ item={item}
145
+ tokens={tokens}
146
+ translations={translations}
147
+ heroImage={heroImage}
148
+ selectedPlanId={selectedPlanId}
149
+ bestValueIdentifier={bestValueIdentifier}
150
+ creditAmounts={creditAmounts}
151
+ creditsLabel={creditsLabel}
152
+ onSelectPlan={setSelectedPlanId}
153
+ />
154
+ );
155
+ }, [tokens, translations, heroImage, selectedPlanId, bestValueIdentifier, creditAmounts, creditsLabel, setSelectedPlanId]);
218
156
 
219
157
  // Performance Optimization: getItemLayout for FlatList
220
158
  const getItemLayout = useCallback((_data: ArrayLike<PaywallListItem> | null, index: number) => {
@@ -227,12 +165,13 @@ export const PaywallScreen: React.FC<PaywallScreenProps> = React.memo((props) =>
227
165
  return `${item.type}-${index}`;
228
166
  }, []);
229
167
 
230
- // Defensive check for translations moved to the end of hooks
168
+ // Defensive check for translations
231
169
  if (!translations) {
232
170
  if (__DEV__) console.warn("[PaywallScreen] Translations prop is missing");
233
171
  return null;
234
172
  }
235
173
 
174
+ // Loading state
236
175
  if (isSyncing) {
237
176
  return (
238
177
  <View style={[styles.container, { backgroundColor: tokens.colors.backgroundPrimary, paddingTop: insets.top }]}>
@@ -246,12 +185,12 @@ export const PaywallScreen: React.FC<PaywallScreenProps> = React.memo((props) =>
246
185
  return (
247
186
  <View style={[styles.container, { backgroundColor: tokens.colors.backgroundPrimary }]}>
248
187
  <StatusBar barStyle="light-content" />
249
-
188
+
250
189
  {/* Absolute Close Button */}
251
- <View style={{
252
- position: 'absolute',
253
- top: Math.max(insets.top, 16),
254
- right: 0,
190
+ <View style={{
191
+ position: 'absolute',
192
+ top: Math.max(insets.top, 16),
193
+ right: 0,
255
194
  zIndex: 10,
256
195
  }}>
257
196
  <TouchableOpacity
@@ -275,46 +214,29 @@ export const PaywallScreen: React.FC<PaywallScreenProps> = React.memo((props) =>
275
214
  maxToRenderPerBatch={10}
276
215
  updateCellsBatchingPeriod={50}
277
216
  contentContainerStyle={[
278
- styles.listContent,
279
- {
217
+ styles.listContent,
218
+ {
280
219
  paddingTop: Math.max(insets.top, 20) + 40,
281
- paddingBottom: 220
220
+ paddingBottom: 220
282
221
  }
283
222
  ]}
284
223
  showsVerticalScrollIndicator={false}
285
224
  />
286
225
 
287
226
  {/* Fixed Footer */}
288
- <View style={[
289
- styles.stickyFooter,
290
- {
291
- backgroundColor: tokens.colors.backgroundPrimary,
292
- paddingBottom: Math.max(insets.bottom, 24)
293
- }
294
- ]}>
295
- <TouchableOpacity
296
- onPress={handlePurchase}
297
- disabled={isProcessing || !selectedPlanId}
298
- style={[
299
- styles.cta,
300
- { backgroundColor: tokens.colors.primary },
301
- (isProcessing || !selectedPlanId) && styles.ctaDisabled
302
- ]}
303
- activeOpacity={0.8}
304
- >
305
- <AtomicText type="titleLarge" style={[styles.ctaText, { color: tokens.colors.onPrimary }]}>
306
- {isProcessing ? translations.processingText : translations.purchaseButtonText}
307
- </AtomicText>
308
- </TouchableOpacity>
309
-
310
- <PaywallFooter
311
- translations={translations}
312
- legalUrls={legalUrls}
313
- isProcessing={isProcessing}
314
- onRestore={handleRestore}
315
- onLegalClick={handleLegalUrl}
316
- />
317
- </View>
227
+ <PaywallFooter
228
+ translations={translations}
229
+ legalUrls={legalUrls}
230
+ isProcessing={isProcessing}
231
+ selectedPlanId={selectedPlanId}
232
+ packages={packages}
233
+ onPurchase={handlePurchase}
234
+ onRestore={handleRestore}
235
+ onLegalUrl={handleLegalUrl}
236
+ insets={insets}
237
+ tokens={tokens}
238
+ styles={styles}
239
+ />
318
240
  </View>
319
241
  );
320
242
  });
@@ -0,0 +1,187 @@
1
+ /**
2
+ * State Components for ManagedSubscriptionFlow
3
+ * Separated for better maintainability
4
+ */
5
+
6
+ import React from "react";
7
+ import type { NavigationProp } from "@react-navigation/native";
8
+ import { SplashScreen } from "@umituz/react-native-design-system/molecules";
9
+ import { OnboardingScreen } from "@umituz/react-native-design-system/onboarding";
10
+ import { useSubscriptionFlowStore } from "../useSubscriptionFlow";
11
+ import type { ManagedSubscriptionFlowProps } from "./ManagedSubscriptionFlow";
12
+ import { PaywallScreen } from "../../../paywall/components/PaywallScreen";
13
+ import { PaywallFeedbackScreen } from "./feedback/PaywallFeedbackScreen";
14
+ import { usePaywallFeedbackSubmit } from "../../../../presentation/hooks/feedback/useFeedbackSubmit";
15
+
16
+ // ============================================================================
17
+ // INITIALIZING STATE
18
+ // ============================================================================
19
+
20
+ interface InitializingStateProps {
21
+ tokens: any;
22
+ splash?: ManagedSubscriptionFlowProps["splash"];
23
+ }
24
+
25
+ export const InitializingState: React.FC<InitializingStateProps> = ({ tokens, splash }) => (
26
+ <SplashScreen
27
+ appName={splash?.appName || "Loading..."}
28
+ tagline={splash?.tagline || "Please wait while we set things up"}
29
+ colors={tokens.colors}
30
+ />
31
+ );
32
+
33
+ // ============================================================================
34
+ // ONBOARDING STATE
35
+ // ============================================================================
36
+
37
+ interface OnboardingStateProps {
38
+ config: ManagedSubscriptionFlowProps["onboarding"];
39
+ onComplete: () => void;
40
+ }
41
+
42
+ export const OnboardingState: React.FC<OnboardingStateProps> = ({ config, onComplete }) => (
43
+ <OnboardingScreen
44
+ slides={config.slides}
45
+ onComplete={onComplete}
46
+ showSkipButton={config.showSkipButton ?? true}
47
+ showBackButton={config.showBackButton ?? true}
48
+ showProgressBar={config.showProgressBar ?? true}
49
+ themeColors={config.themeColors}
50
+ translations={config.translations}
51
+ />
52
+ );
53
+
54
+ // ============================================================================
55
+ // PAYWALL STATE
56
+ // ============================================================================
57
+
58
+ interface PaywallStateProps {
59
+ config: ManagedSubscriptionFlowProps["paywall"];
60
+ packages: any[];
61
+ isPremium: boolean;
62
+ credits: number | null;
63
+ isSyncing: boolean;
64
+ onPurchase: (pkgId: string) => Promise<any>;
65
+ onRestore: () => Promise<any>;
66
+ onClose: (purchased: boolean) => void;
67
+ }
68
+
69
+ export const PaywallState: React.FC<PaywallStateProps> = ({
70
+ config,
71
+ packages,
72
+ isPremium,
73
+ credits,
74
+ isSyncing,
75
+ onPurchase,
76
+ onRestore,
77
+ onClose,
78
+ }) => {
79
+ const [purchaseSuccessful, setPurchaseSuccessful] = React.useState(false);
80
+
81
+ const handlePurchase = async (pkgId: string) => {
82
+ const result = await onPurchase(pkgId);
83
+ if (result?.success) {
84
+ setPurchaseSuccessful(true);
85
+ }
86
+ return result;
87
+ };
88
+
89
+ const handleClose = () => {
90
+ onClose(purchaseSuccessful);
91
+ };
92
+
93
+ return (
94
+ <PaywallScreen
95
+ translations={config.translations}
96
+ legalUrls={config.legalUrls}
97
+ features={config.features}
98
+ bestValueIdentifier={config.bestValueIdentifier}
99
+ creditsLabel={config.creditsLabel}
100
+ heroImage={config.heroImage}
101
+ source="onboarding"
102
+ packages={packages}
103
+ isPremium={isPremium}
104
+ credits={credits}
105
+ isSyncing={isSyncing}
106
+ onPurchase={handlePurchase}
107
+ onRestore={onRestore}
108
+ onClose={handleClose}
109
+ />
110
+ );
111
+ };
112
+
113
+ // ============================================================================
114
+ // FEEDBACK STATE
115
+ // ============================================================================
116
+
117
+ interface FeedbackStateProps {
118
+ config: ManagedSubscriptionFlowProps["feedback"];
119
+ onClose: () => void;
120
+ }
121
+
122
+ export const FeedbackState: React.FC<FeedbackStateProps> = ({ config, onClose }) => {
123
+ const { submit: internalSubmit } = usePaywallFeedbackSubmit();
124
+
125
+ const handleSubmit = async (data: { reason: string; otherText?: string }) => {
126
+ if (config.onSubmit) {
127
+ await config.onSubmit(data);
128
+ } else {
129
+ const description = data.otherText ? `${data.reason}: ${data.otherText}` : data.reason;
130
+ await internalSubmit(description);
131
+ }
132
+ };
133
+
134
+ return (
135
+ <PaywallFeedbackScreen
136
+ onClose={onClose}
137
+ onSubmit={handleSubmit}
138
+ translations={config.translations}
139
+ />
140
+ );
141
+ };
142
+
143
+ // ============================================================================
144
+ // READY STATE (APP CONTENT)
145
+ // ============================================================================
146
+
147
+ interface ReadyStateProps {
148
+ children: React.ReactNode;
149
+ offline?: ManagedSubscriptionFlowProps["offline"];
150
+ feedbackConfig: ManagedSubscriptionFlowProps["feedback"];
151
+ showFeedback: boolean;
152
+ tokens: any;
153
+ onFeedbackClose: () => void;
154
+ }
155
+
156
+ export const ReadyState: React.FC<ReadyStateProps> = ({
157
+ children,
158
+ offline,
159
+ feedbackConfig,
160
+ showFeedback,
161
+ tokens,
162
+ onFeedbackClose,
163
+ }) => {
164
+ const { OfflineBanner } = require("@umituz/react-native-design-system/offline");
165
+
166
+ return (
167
+ <>
168
+ {children}
169
+
170
+ {offline && (
171
+ <OfflineBanner
172
+ visible={offline.isOffline}
173
+ message={offline.message}
174
+ backgroundColor={offline.backgroundColor || tokens.colors.error}
175
+ position={offline.position || "top"}
176
+ />
177
+ )}
178
+
179
+ {showFeedback && (
180
+ <FeedbackState
181
+ config={feedbackConfig}
182
+ onClose={onFeedbackClose}
183
+ />
184
+ )}
185
+ </>
186
+ );
187
+ };
@@ -1,27 +1,26 @@
1
1
  /**
2
2
  * ManagedSubscriptionFlow
3
3
  *
4
- * State machine-based flow orchestration.
5
- * No complex if/else - clean state transitions.
4
+ * Clean state machine-based flow orchestration.
5
+ * All state components separated to individual files.
6
6
  */
7
7
 
8
- import React, { useEffect, useCallback } from "react";
8
+ import React, { useEffect } from "react";
9
9
  import type { NavigationProp } from "@react-navigation/native";
10
10
  import type { ImageSourcePropType } from "react-native";
11
- import { View } from "react-native";
12
- import { SplashScreen, useSplashFlow } from "@umituz/react-native-design-system/molecules";
13
- import { OnboardingScreen } from "@umituz/react-native-design-system/onboarding";
14
- import { OfflineBanner } from "@umituz/react-native-design-system/offline";
15
11
  import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
16
12
  import { usePremiumStatus } from "../../presentation/usePremiumStatus";
17
13
  import { usePremiumPackages } from "../../presentation/usePremiumPackages";
18
14
  import { usePremiumActions } from "../../presentation/usePremiumActions";
19
15
  import { useSubscriptionFlowStore, SubscriptionFlowStatus } from "../useSubscriptionFlow";
20
- import { PaywallFeedbackScreen } from "./feedback/PaywallFeedbackScreen";
21
16
  import type { PaywallFeedbackTranslations } from "./feedback/PaywallFeedbackScreen.types";
22
17
  import type { PaywallTranslations, PaywallLegalUrls, SubscriptionFeature } from "../../../paywall/entities/types";
23
- import { usePaywallFeedbackSubmit } from "../../../../presentation/hooks/feedback/useFeedbackSubmit";
24
- import { PaywallScreen } from "../../../paywall/components/PaywallScreen";
18
+ import {
19
+ InitializingState,
20
+ OnboardingState,
21
+ PaywallState,
22
+ ReadyState,
23
+ } from "./ManagedSubscriptionFlow.states";
25
24
 
26
25
  export interface ManagedSubscriptionFlowProps {
27
26
  children: React.ReactNode;
@@ -79,123 +78,8 @@ import {
79
78
  useSubscriptionFlowStatus
80
79
  } from "../providers/SubscriptionFlowProvider";
81
80
 
82
- // ============================================================================
83
- // STATE MACHINE COMPONENTS
84
- // ============================================================================
85
-
86
- interface StateComponentProps {
87
- status: SubscriptionFlowStatus;
88
- tokens: any;
89
- onboardingConfig: ManagedSubscriptionFlowProps["onboarding"];
90
- paywallConfig: ManagedSubscriptionFlowProps["paywall"];
91
- feedbackConfig: ManagedSubscriptionFlowProps["feedback"];
92
- isPremium: boolean;
93
- packages: any[];
94
- credits: number | null;
95
- isSyncing: boolean;
96
- onPurchasePackage: (pkgId: string) => Promise<any>;
97
- onRestorePurchase: () => Promise<any>;
98
- navigation: NavigationProp<any>;
99
- }
100
-
101
- const InitializingState: React.FC<{ tokens: any; splash?: ManagedSubscriptionFlowProps["splash"] }> = ({ tokens, splash }) => (
102
- <SplashScreen
103
- appName={splash?.appName || "Loading..."}
104
- tagline={splash?.tagline || "Please wait while we set things up"}
105
- colors={tokens.colors}
106
- />
107
- );
108
-
109
- const OnboardingState: React.FC<{
110
- config: ManagedSubscriptionFlowProps["onboarding"];
111
- onComplete: () => void;
112
- }> = ({ config, onComplete }) => (
113
- <OnboardingScreen
114
- slides={config.slides}
115
- onComplete={onComplete}
116
- showSkipButton={config.showSkipButton ?? true}
117
- showBackButton={config.showBackButton ?? true}
118
- showProgressBar={config.showProgressBar ?? true}
119
- themeColors={config.themeColors}
120
- translations={config.translations}
121
- />
122
- );
123
-
124
- const PaywallState: React.FC<{
125
- config: ManagedSubscriptionFlowProps["paywall"];
126
- packages: any[];
127
- isPremium: boolean;
128
- credits: number | null;
129
- isSyncing: boolean;
130
- onPurchase: (pkgId: string) => Promise<any>;
131
- onRestore: () => Promise<any>;
132
- onClose: (purchased: boolean) => void;
133
- }> = ({ config, packages, isPremium, credits, isSyncing, onPurchase, onRestore, onClose }) => {
134
- const [purchaseSuccessful, setPurchaseSuccessful] = React.useState(false);
135
-
136
- const handlePurchase = async (pkgId: string) => {
137
- const result = await onPurchase(pkgId);
138
- if (result?.success) {
139
- setPurchaseSuccessful(true);
140
- }
141
- return result;
142
- };
143
-
144
- const handleClose = () => {
145
- onClose(purchaseSuccessful);
146
- };
147
-
148
- return (
149
- <PaywallScreen
150
- translations={config.translations}
151
- legalUrls={config.legalUrls}
152
- features={config.features}
153
- bestValueIdentifier={config.bestValueIdentifier}
154
- creditsLabel={config.creditsLabel}
155
- heroImage={config.heroImage}
156
- source="onboarding"
157
- packages={packages}
158
- isPremium={isPremium}
159
- credits={credits}
160
- isSyncing={isSyncing}
161
- onPurchase={handlePurchase}
162
- onRestore={onRestore}
163
- onClose={handleClose}
164
- />
165
- );
166
- };
167
-
168
- const FeedbackState: React.FC<{
169
- config: ManagedSubscriptionFlowProps["feedback"];
170
- onClose: () => void;
171
- }> = ({ config, onClose }) => {
172
- const { submit: internalSubmit } = usePaywallFeedbackSubmit();
173
-
174
- const handleSubmit = async (data: { reason: string; otherText?: string }) => {
175
- if (config.onSubmit) {
176
- await config.onSubmit(data);
177
- } else {
178
- const description = data.otherText ? `${data.reason}: ${data.otherText}` : data.reason;
179
- await internalSubmit(description);
180
- }
181
- };
182
-
183
- return (
184
- <PaywallFeedbackScreen
185
- onClose={onClose}
186
- onSubmit={handleSubmit}
187
- translations={config.translations}
188
- />
189
- );
190
- };
191
-
192
- // ============================================================================
193
- // MAIN COMPONENT
194
- // ============================================================================
195
-
196
- const ManagedSubscriptionFlowInner = React.memo<ManagedSubscriptionFlowProps>(({
81
+ const ManagedSubscriptionFlowInner: React.FC<ManagedSubscriptionFlowProps> = ({
197
82
  children,
198
- navigation,
199
83
  islocalizationReady,
200
84
  splash,
201
85
  onboarding,
@@ -205,9 +89,6 @@ const ManagedSubscriptionFlowInner = React.memo<ManagedSubscriptionFlowProps>(({
205
89
  }) => {
206
90
  const tokens = useAppDesignTokens();
207
91
  const status = useSubscriptionFlowStatus();
208
- const { isInitialized: isSplashComplete } = useSplashFlow({
209
- duration: splash?.duration || 0,
210
- });
211
92
 
212
93
  // Premium hooks
213
94
  const { isPremium, isSyncing, credits, isLoading: isPremiumLoading } = usePremiumStatus();
@@ -220,32 +101,26 @@ const ManagedSubscriptionFlowInner = React.memo<ManagedSubscriptionFlowProps>(({
220
101
  const completePaywall = useSubscriptionFlowStore((s) => s.completePaywall);
221
102
  const hideFeedback = useSubscriptionFlowStore((s) => s.hideFeedback);
222
103
  const showFeedbackScreen = useSubscriptionFlowStore((s) => s.showFeedbackScreen);
223
-
224
104
  const showFeedback = useSubscriptionFlowStore((s) => s.showFeedback);
225
105
 
226
106
  // ========================================================================
227
107
  // STATE TRANSITIONS
228
108
  // ========================================================================
229
109
 
230
- // CHECK_PREMIUM state transition logic
231
110
  useEffect(() => {
232
111
  if (status === SubscriptionFlowStatus.CHECK_PREMIUM && !isPremiumLoading) {
233
112
  const paywallShown = useSubscriptionFlowStore.getState().paywallShown;
234
113
 
235
114
  if (isPremium) {
236
- // User is premium, go to ready
237
115
  completePaywall(true);
238
116
  } else if (!paywallShown) {
239
- // User not premium and paywall not shown, show paywall
240
117
  showPaywall();
241
118
  } else {
242
- // Paywall already shown, go to ready
243
119
  completePaywall(false);
244
120
  }
245
121
  }
246
122
  }, [status, isPremium, isPremiumLoading, showPaywall, completePaywall]);
247
123
 
248
- // Show feedback when needed
249
124
  useEffect(() => {
250
125
  if (status === SubscriptionFlowStatus.READY && showFeedback) {
251
126
  showFeedbackScreen();
@@ -253,21 +128,18 @@ const ManagedSubscriptionFlowInner = React.memo<ManagedSubscriptionFlowProps>(({
253
128
  }, [status, showFeedback, showFeedbackScreen]);
254
129
 
255
130
  // ========================================================================
256
- // RENDER
131
+ // RENDER BY STATE
257
132
  // ========================================================================
258
133
 
259
- // Wait for localization
260
134
  if (!islocalizationReady || status === SubscriptionFlowStatus.INITIALIZING) {
261
135
  return <InitializingState tokens={tokens} splash={splash} />;
262
136
  }
263
137
 
264
- // Render by state
265
138
  switch (status) {
266
139
  case SubscriptionFlowStatus.ONBOARDING:
267
140
  return <OnboardingState config={onboarding} onComplete={completeOnboarding} />;
268
141
 
269
142
  case SubscriptionFlowStatus.CHECK_PREMIUM:
270
- // Show loading while checking premium
271
143
  return <InitializingState tokens={tokens} splash={splash} />;
272
144
 
273
145
  case SubscriptionFlowStatus.POST_ONBOARDING_PAYWALL:
@@ -286,31 +158,20 @@ const ManagedSubscriptionFlowInner = React.memo<ManagedSubscriptionFlowProps>(({
286
158
 
287
159
  case SubscriptionFlowStatus.READY:
288
160
  return (
289
- <>
290
- {children}
291
-
292
- {offline && (
293
- <OfflineBanner
294
- visible={offline.isOffline}
295
- message={offline.message}
296
- backgroundColor={offline.backgroundColor || tokens.colors.error}
297
- position={offline.position || "top"}
298
- />
299
- )}
300
-
301
- {showFeedback && (
302
- <FeedbackState
303
- config={feedback}
304
- onClose={hideFeedback}
305
- />
306
- )}
307
- </>
161
+ <ReadyState
162
+ children={children}
163
+ offline={offline}
164
+ feedbackConfig={feedback}
165
+ showFeedback={showFeedback}
166
+ tokens={tokens}
167
+ onFeedbackClose={hideFeedback}
168
+ />
308
169
  );
309
170
 
310
171
  default:
311
172
  return <InitializingState tokens={tokens} splash={splash} />;
312
173
  }
313
- });
174
+ };
314
175
 
315
176
  ManagedSubscriptionFlowInner.displayName = "ManagedSubscriptionFlowInner";
316
177