@umituz/react-native-subscription 3.1.9 → 3.1.11
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 +1 -1
- package/src/domains/credits/presentation/useCreditsRealTime.ts +31 -73
- package/src/domains/credits/utils/creditValidation.ts +5 -26
- package/src/domains/paywall/hooks/usePaywallActions.ts +21 -133
- package/src/domains/paywall/hooks/usePaywallActions.types.ts +16 -0
- package/src/domains/paywall/hooks/usePaywallPurchase.ts +78 -0
- package/src/domains/paywall/hooks/usePaywallRestore.ts +66 -0
- package/src/domains/revenuecat/infrastructure/services/userSwitchCore.ts +116 -0
- package/src/domains/revenuecat/infrastructure/services/userSwitchHandler.ts +19 -237
- package/src/domains/revenuecat/infrastructure/services/userSwitchHelpers.ts +55 -0
- package/src/domains/revenuecat/infrastructure/services/userSwitchInitializer.ts +143 -0
- package/src/domains/subscription/infrastructure/managers/SubscriptionManager.ts +6 -3
- package/src/domains/subscription/infrastructure/managers/initializationHandler.ts +2 -2
- package/src/domains/subscription/infrastructure/managers/packageHandlerFactory.ts +2 -2
- package/src/domains/subscription/infrastructure/managers/subscriptionManagerUtils.ts +2 -2
- package/src/domains/subscription/presentation/components/ManagedSubscriptionFlow.logic.ts +52 -0
- package/src/domains/subscription/presentation/components/ManagedSubscriptionFlow.tsx +15 -89
- package/src/domains/subscription/presentation/components/ManagedSubscriptionFlow.types.ts +59 -0
- package/src/domains/subscription/presentation/components/details/CreditRow.tsx +9 -0
- package/src/domains/subscription/presentation/components/details/PremiumDetailsCard.tsx +23 -0
- package/src/domains/subscription/presentation/components/states/FeedbackState.tsx +36 -0
- package/src/domains/subscription/presentation/components/states/InitializingState.tsx +47 -0
- package/src/domains/subscription/presentation/components/states/OnboardingState.tsx +27 -0
- package/src/domains/subscription/presentation/components/states/PaywallState.tsx +66 -0
- package/src/domains/subscription/presentation/components/states/ReadyState.tsx +51 -0
- package/src/domains/subscription/presentation/flowInitialState.ts +22 -0
- package/src/domains/subscription/presentation/flowTypes.ts +106 -0
- package/src/domains/subscription/presentation/screens/components/SubscriptionHeaderContent.tsx +119 -103
- package/src/domains/subscription/presentation/usePremiumActions.ts +5 -6
- package/src/domains/subscription/presentation/useSubscriptionFlow.ts +25 -92
- package/src/domains/wallet/presentation/components/BalanceCard.tsx +7 -0
- package/src/domains/wallet/presentation/components/TransactionItem.tsx +11 -0
- package/src/domains/wallet/presentation/hooks/useTransactionHistory.ts +34 -60
- package/src/index.components.ts +1 -1
- package/src/shared/infrastructure/SubscriptionEventBus.ts +4 -2
- package/src/shared/presentation/hooks/useFirestoreRealTime.ts +230 -0
- package/src/shared/presentation/types/hookState.types.ts +97 -0
- package/src/shared/utils/errors/errorAssertions.ts +35 -0
- package/src/shared/utils/errors/errorConversion.ts +73 -0
- package/src/shared/utils/errors/errorTypeGuards.ts +27 -0
- package/src/shared/utils/errors/errorWrappers.ts +54 -0
- package/src/shared/utils/errors/index.ts +19 -0
- package/src/shared/utils/errors/serviceErrors.ts +36 -0
- package/src/shared/utils/logger.ts +140 -0
- package/src/domains/subscription/presentation/components/ManagedSubscriptionFlow.states.tsx +0 -187
|
@@ -2,77 +2,21 @@
|
|
|
2
2
|
* ManagedSubscriptionFlow
|
|
3
3
|
*
|
|
4
4
|
* Clean state machine-based flow orchestration.
|
|
5
|
-
*
|
|
5
|
+
* State components and logic separated to individual modules.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import React
|
|
9
|
-
import type { NavigationProp } from "@react-navigation/native";
|
|
10
|
-
import type { ImageSourcePropType } from "react-native";
|
|
8
|
+
import React from "react";
|
|
11
9
|
import { useAppDesignTokens } from "@umituz/react-native-design-system/theme";
|
|
12
10
|
import { usePremiumStatus } from "../../presentation/usePremiumStatus";
|
|
13
11
|
import { usePremiumPackages } from "../../presentation/usePremiumPackages";
|
|
14
12
|
import { usePremiumActions } from "../../presentation/usePremiumActions";
|
|
15
13
|
import { useSubscriptionFlowStore, SubscriptionFlowStatus } from "../useSubscriptionFlow";
|
|
16
|
-
import
|
|
17
|
-
import type {
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
ReadyState,
|
|
23
|
-
} from "./ManagedSubscriptionFlow.states";
|
|
24
|
-
|
|
25
|
-
export interface ManagedSubscriptionFlowProps {
|
|
26
|
-
children: React.ReactNode;
|
|
27
|
-
navigation: NavigationProp<any>;
|
|
28
|
-
islocalizationReady: boolean;
|
|
29
|
-
|
|
30
|
-
// Splash Configuration
|
|
31
|
-
splash?: {
|
|
32
|
-
appName: string;
|
|
33
|
-
tagline: string;
|
|
34
|
-
duration?: number;
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
// Onboarding Configuration
|
|
38
|
-
onboarding: {
|
|
39
|
-
slides: any[];
|
|
40
|
-
translations: {
|
|
41
|
-
nextButton: string;
|
|
42
|
-
getStartedButton: string;
|
|
43
|
-
of: string;
|
|
44
|
-
};
|
|
45
|
-
themeColors: any;
|
|
46
|
-
showSkipButton?: boolean;
|
|
47
|
-
showBackButton?: boolean;
|
|
48
|
-
showProgressBar?: boolean;
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
// Paywall Configuration
|
|
52
|
-
paywall: {
|
|
53
|
-
translations: PaywallTranslations;
|
|
54
|
-
features: SubscriptionFeature[];
|
|
55
|
-
legalUrls: PaywallLegalUrls;
|
|
56
|
-
heroImage: ImageSourcePropType;
|
|
57
|
-
bestValueIdentifier?: string;
|
|
58
|
-
creditsLabel?: string;
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
// Feedback Configuration
|
|
62
|
-
feedback: {
|
|
63
|
-
translations: PaywallFeedbackTranslations;
|
|
64
|
-
onSubmit?: (data: { reason: string; otherText?: string }) => void | Promise<void>;
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
// Offline Configuration (optional)
|
|
68
|
-
offline?: {
|
|
69
|
-
isOffline: boolean;
|
|
70
|
-
message: string;
|
|
71
|
-
backgroundColor?: string;
|
|
72
|
-
position?: "top" | "bottom";
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
|
|
14
|
+
import { useStateTransitions } from "./ManagedSubscriptionFlow.logic";
|
|
15
|
+
import type { ManagedSubscriptionFlowProps } from "./ManagedSubscriptionFlow.types";
|
|
16
|
+
import { InitializingState } from "./states/InitializingState";
|
|
17
|
+
import { OnboardingState } from "./states/OnboardingState";
|
|
18
|
+
import { PaywallState } from "./states/PaywallState";
|
|
19
|
+
import { ReadyState } from "./states/ReadyState";
|
|
76
20
|
import {
|
|
77
21
|
SubscriptionFlowProvider,
|
|
78
22
|
useSubscriptionFlowStatus
|
|
@@ -97,35 +41,17 @@ const ManagedSubscriptionFlowInner: React.FC<ManagedSubscriptionFlowProps> = ({
|
|
|
97
41
|
|
|
98
42
|
// Store actions
|
|
99
43
|
const completeOnboarding = useSubscriptionFlowStore((s) => s.completeOnboarding);
|
|
100
|
-
const showPaywall = useSubscriptionFlowStore((s) => s.showPaywall);
|
|
101
44
|
const completePaywall = useSubscriptionFlowStore((s) => s.completePaywall);
|
|
102
45
|
const hideFeedback = useSubscriptionFlowStore((s) => s.hideFeedback);
|
|
103
|
-
const showFeedbackScreen = useSubscriptionFlowStore((s) => s.showFeedbackScreen);
|
|
104
46
|
const showFeedback = useSubscriptionFlowStore((s) => s.showFeedback);
|
|
105
47
|
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
if (isPremium) {
|
|
115
|
-
completePaywall(true);
|
|
116
|
-
} else if (!paywallShown) {
|
|
117
|
-
showPaywall();
|
|
118
|
-
} else {
|
|
119
|
-
completePaywall(false);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
}, [status, isPremium, isSyncing, showPaywall, completePaywall]);
|
|
123
|
-
|
|
124
|
-
useEffect(() => {
|
|
125
|
-
if (status === SubscriptionFlowStatus.READY && showFeedback) {
|
|
126
|
-
showFeedbackScreen();
|
|
127
|
-
}
|
|
128
|
-
}, [status, showFeedback, showFeedbackScreen]);
|
|
48
|
+
// State transitions
|
|
49
|
+
useStateTransitions({
|
|
50
|
+
status,
|
|
51
|
+
isPremium,
|
|
52
|
+
isSyncing,
|
|
53
|
+
showFeedback,
|
|
54
|
+
});
|
|
129
55
|
|
|
130
56
|
// ========================================================================
|
|
131
57
|
// RENDER BY STATE
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ManagedSubscriptionFlow Types
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { NavigationProp } from "@react-navigation/native";
|
|
6
|
+
import type { ImageSourcePropType } from "react-native";
|
|
7
|
+
import type { PaywallFeedbackTranslations } from "./feedback/PaywallFeedbackScreen.types";
|
|
8
|
+
import type { PaywallTranslations, PaywallLegalUrls, SubscriptionFeature } from "../../../paywall/entities/types";
|
|
9
|
+
|
|
10
|
+
export interface ManagedSubscriptionFlowProps {
|
|
11
|
+
children: React.ReactNode;
|
|
12
|
+
navigation: NavigationProp<any>;
|
|
13
|
+
islocalizationReady: boolean;
|
|
14
|
+
|
|
15
|
+
// Splash Configuration
|
|
16
|
+
splash?: {
|
|
17
|
+
appName: string;
|
|
18
|
+
tagline: string;
|
|
19
|
+
duration?: number;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// Onboarding Configuration
|
|
23
|
+
onboarding: {
|
|
24
|
+
slides: any[];
|
|
25
|
+
translations: {
|
|
26
|
+
nextButton: string;
|
|
27
|
+
getStartedButton: string;
|
|
28
|
+
of: string;
|
|
29
|
+
};
|
|
30
|
+
themeColors: any;
|
|
31
|
+
showSkipButton?: boolean;
|
|
32
|
+
showBackButton?: boolean;
|
|
33
|
+
showProgressBar?: boolean;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Paywall Configuration
|
|
37
|
+
paywall: {
|
|
38
|
+
translations: PaywallTranslations;
|
|
39
|
+
features: SubscriptionFeature[];
|
|
40
|
+
legalUrls: PaywallLegalUrls;
|
|
41
|
+
heroImage: ImageSourcePropType;
|
|
42
|
+
bestValueIdentifier?: string;
|
|
43
|
+
creditsLabel?: string;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Feedback Configuration
|
|
47
|
+
feedback: {
|
|
48
|
+
translations: PaywallFeedbackTranslations;
|
|
49
|
+
onSubmit?: (data: { reason: string; otherText?: string }) => void | Promise<void>;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// Offline Configuration (optional)
|
|
53
|
+
offline?: {
|
|
54
|
+
isOffline: boolean;
|
|
55
|
+
message: string;
|
|
56
|
+
backgroundColor?: string;
|
|
57
|
+
position?: "top" | "bottom";
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -55,6 +55,15 @@ export const CreditRow: React.FC<CreditRowProps> = React.memo(({
|
|
|
55
55
|
)}
|
|
56
56
|
</View>
|
|
57
57
|
);
|
|
58
|
+
}, (prevProps, nextProps) => {
|
|
59
|
+
// PERFORMANCE: Custom comparison to prevent unnecessary re-renders
|
|
60
|
+
// Only re-render if these values actually change
|
|
61
|
+
return (
|
|
62
|
+
prevProps.label === nextProps.label &&
|
|
63
|
+
prevProps.current === nextProps.current &&
|
|
64
|
+
prevProps.total === nextProps.total &&
|
|
65
|
+
prevProps.remainingLabel === nextProps.remainingLabel
|
|
66
|
+
);
|
|
58
67
|
});
|
|
59
68
|
|
|
60
69
|
const styles = StyleSheet.create({
|
|
@@ -68,4 +68,27 @@ export const PremiumDetailsCard: React.FC<PremiumDetailsCardProps> = React.memo(
|
|
|
68
68
|
/>
|
|
69
69
|
</View>
|
|
70
70
|
);
|
|
71
|
+
}, (prevProps, nextProps) => {
|
|
72
|
+
// PERFORMANCE: Custom comparison to prevent unnecessary re-renders
|
|
73
|
+
// Deep comparison for credits array since it's frequently updated
|
|
74
|
+
const creditsEqual = prevProps.credits === nextProps.credits ||
|
|
75
|
+
(prevProps.credits?.length === nextProps.credits?.length &&
|
|
76
|
+
prevProps.credits?.every((credit, i) =>
|
|
77
|
+
credit.id === nextProps.credits?.[i]?.id &&
|
|
78
|
+
credit.current === nextProps.credits?.[i]?.current &&
|
|
79
|
+
credit.total === nextProps.credits?.[i]?.total &&
|
|
80
|
+
credit.label === nextProps.credits?.[i]?.label
|
|
81
|
+
));
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
prevProps.statusType === nextProps.statusType &&
|
|
85
|
+
prevProps.isPremium === nextProps.isPremium &&
|
|
86
|
+
prevProps.expirationDate === nextProps.expirationDate &&
|
|
87
|
+
prevProps.purchaseDate === nextProps.purchaseDate &&
|
|
88
|
+
prevProps.daysRemaining === nextProps.daysRemaining &&
|
|
89
|
+
creditsEqual &&
|
|
90
|
+
prevProps.translations === nextProps.translations &&
|
|
91
|
+
prevProps.onManageSubscription === nextProps.onManageSubscription &&
|
|
92
|
+
prevProps.onUpgrade === nextProps.onUpgrade
|
|
93
|
+
);
|
|
71
94
|
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feedback State Component
|
|
3
|
+
*
|
|
4
|
+
* Displays feedback screen to collect user input.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from "react";
|
|
8
|
+
import { PaywallFeedbackScreen } from "../feedback/PaywallFeedbackScreen";
|
|
9
|
+
import { usePaywallFeedbackSubmit } from "../../../../../presentation/hooks/feedback/useFeedbackSubmit";
|
|
10
|
+
import type { ManagedSubscriptionFlowProps } from "../ManagedSubscriptionFlow.types";
|
|
11
|
+
|
|
12
|
+
interface FeedbackStateProps {
|
|
13
|
+
config: ManagedSubscriptionFlowProps["feedback"];
|
|
14
|
+
onClose: () => void;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const FeedbackState: React.FC<FeedbackStateProps> = ({ config, onClose }) => {
|
|
18
|
+
const { submit: internalSubmit } = usePaywallFeedbackSubmit();
|
|
19
|
+
|
|
20
|
+
const handleSubmit = async (data: { reason: string; otherText?: string }) => {
|
|
21
|
+
if (config.onSubmit) {
|
|
22
|
+
await config.onSubmit(data);
|
|
23
|
+
} else {
|
|
24
|
+
const description = data.otherText ? `${data.reason}: ${data.otherText}` : data.reason;
|
|
25
|
+
await internalSubmit(description);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<PaywallFeedbackScreen
|
|
31
|
+
onClose={onClose}
|
|
32
|
+
onSubmit={handleSubmit}
|
|
33
|
+
translations={config.translations}
|
|
34
|
+
/>
|
|
35
|
+
);
|
|
36
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Initializing State Component
|
|
3
|
+
*
|
|
4
|
+
* Displays splash screen during initialization.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from "react";
|
|
8
|
+
import { View, Text, StyleSheet } from "react-native";
|
|
9
|
+
import type { ManagedSubscriptionFlowProps } from "../ManagedSubscriptionFlow.types";
|
|
10
|
+
|
|
11
|
+
interface InitializingStateProps {
|
|
12
|
+
tokens: any;
|
|
13
|
+
splash?: ManagedSubscriptionFlowProps["splash"];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const InitializingState: React.FC<InitializingStateProps> = ({ tokens, splash }) => {
|
|
17
|
+
if (!splash) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<View style={[styles.container, { backgroundColor: tokens.colors.background }]}>
|
|
23
|
+
<Text style={[styles.appName, { color: tokens.colors.text }]}>
|
|
24
|
+
{splash.appName}
|
|
25
|
+
</Text>
|
|
26
|
+
<Text style={[styles.tagline, { color: tokens.colors.textSecondary }]}>
|
|
27
|
+
{splash.tagline}
|
|
28
|
+
</Text>
|
|
29
|
+
</View>
|
|
30
|
+
);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const styles = StyleSheet.create({
|
|
34
|
+
container: {
|
|
35
|
+
flex: 1,
|
|
36
|
+
justifyContent: "center",
|
|
37
|
+
alignItems: "center",
|
|
38
|
+
},
|
|
39
|
+
appName: {
|
|
40
|
+
fontSize: 32,
|
|
41
|
+
fontWeight: "bold",
|
|
42
|
+
marginBottom: 8,
|
|
43
|
+
},
|
|
44
|
+
tagline: {
|
|
45
|
+
fontSize: 16,
|
|
46
|
+
},
|
|
47
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Onboarding State Component
|
|
3
|
+
*
|
|
4
|
+
* Displays onboarding slides to the user.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from "react";
|
|
8
|
+
import { OnboardingScreen } from "@umituz/react-native-design-system/onboarding";
|
|
9
|
+
import type { ManagedSubscriptionFlowProps } from "../ManagedSubscriptionFlow.types";
|
|
10
|
+
|
|
11
|
+
interface OnboardingStateProps {
|
|
12
|
+
config: ManagedSubscriptionFlowProps["onboarding"];
|
|
13
|
+
onComplete: () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const OnboardingState: React.FC<OnboardingStateProps> = ({ config, onComplete }) => {
|
|
17
|
+
return (
|
|
18
|
+
<OnboardingScreen
|
|
19
|
+
slides={config.slides}
|
|
20
|
+
translations={config.translations}
|
|
21
|
+
onComplete={onComplete}
|
|
22
|
+
showSkipButton={config.showSkipButton ?? true}
|
|
23
|
+
showBackButton={config.showBackButton ?? true}
|
|
24
|
+
showProgressBar={config.showProgressBar ?? true}
|
|
25
|
+
/>
|
|
26
|
+
);
|
|
27
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paywall State Component
|
|
3
|
+
*
|
|
4
|
+
* Displays paywall screen for purchase/restore.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React, { useState } from "react";
|
|
8
|
+
import type { PurchasesPackage } from "react-native-purchases";
|
|
9
|
+
import type { UserCredits } from "../../../../credits/core/Credits";
|
|
10
|
+
import { PaywallScreen } from "../../../../paywall/components/PaywallScreen";
|
|
11
|
+
import type { ManagedSubscriptionFlowProps } from "../ManagedSubscriptionFlow.types";
|
|
12
|
+
|
|
13
|
+
interface PaywallStateProps {
|
|
14
|
+
config: ManagedSubscriptionFlowProps["paywall"];
|
|
15
|
+
packages: PurchasesPackage[];
|
|
16
|
+
isPremium: boolean;
|
|
17
|
+
credits: UserCredits | null;
|
|
18
|
+
isSyncing: boolean;
|
|
19
|
+
onPurchase: (pkg: PurchasesPackage) => Promise<boolean>;
|
|
20
|
+
onRestore: () => Promise<boolean>;
|
|
21
|
+
onClose: (purchased: boolean) => void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const PaywallState: React.FC<PaywallStateProps> = ({
|
|
25
|
+
config,
|
|
26
|
+
packages,
|
|
27
|
+
isPremium,
|
|
28
|
+
credits,
|
|
29
|
+
isSyncing,
|
|
30
|
+
onPurchase,
|
|
31
|
+
onRestore,
|
|
32
|
+
onClose,
|
|
33
|
+
}) => {
|
|
34
|
+
const [purchaseSuccessful, setPurchaseSuccessful] = useState(false);
|
|
35
|
+
|
|
36
|
+
const handlePurchase = async (pkg: PurchasesPackage) => {
|
|
37
|
+
const result = await onPurchase(pkg);
|
|
38
|
+
if (result) {
|
|
39
|
+
setPurchaseSuccessful(true);
|
|
40
|
+
}
|
|
41
|
+
return result;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const handleClose = () => {
|
|
45
|
+
onClose(purchaseSuccessful);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<PaywallScreen
|
|
50
|
+
translations={config.translations}
|
|
51
|
+
legalUrls={config.legalUrls}
|
|
52
|
+
features={config.features}
|
|
53
|
+
bestValueIdentifier={config.bestValueIdentifier}
|
|
54
|
+
creditsLabel={config.creditsLabel}
|
|
55
|
+
heroImage={config.heroImage}
|
|
56
|
+
source="onboarding"
|
|
57
|
+
packages={packages}
|
|
58
|
+
isPremium={isPremium}
|
|
59
|
+
credits={credits}
|
|
60
|
+
isSyncing={isSyncing}
|
|
61
|
+
onPurchase={handlePurchase}
|
|
62
|
+
onRestore={onRestore}
|
|
63
|
+
onClose={handleClose}
|
|
64
|
+
/>
|
|
65
|
+
);
|
|
66
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ready State Component
|
|
3
|
+
*
|
|
4
|
+
* Displays app content with optional offline banner and feedback.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React from "react";
|
|
8
|
+
import { FeedbackState } from "./FeedbackState";
|
|
9
|
+
import type { ManagedSubscriptionFlowProps } from "../ManagedSubscriptionFlow.types";
|
|
10
|
+
|
|
11
|
+
interface ReadyStateProps {
|
|
12
|
+
children: React.ReactNode;
|
|
13
|
+
offline?: ManagedSubscriptionFlowProps["offline"];
|
|
14
|
+
feedbackConfig: ManagedSubscriptionFlowProps["feedback"];
|
|
15
|
+
showFeedback: boolean;
|
|
16
|
+
tokens: any;
|
|
17
|
+
onFeedbackClose: () => void;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const ReadyState: React.FC<ReadyStateProps> = ({
|
|
21
|
+
children,
|
|
22
|
+
offline,
|
|
23
|
+
feedbackConfig,
|
|
24
|
+
showFeedback,
|
|
25
|
+
tokens,
|
|
26
|
+
onFeedbackClose,
|
|
27
|
+
}) => {
|
|
28
|
+
const { OfflineBanner } = require("@umituz/react-native-design-system/offline");
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<>
|
|
32
|
+
{children}
|
|
33
|
+
|
|
34
|
+
{offline && (
|
|
35
|
+
<OfflineBanner
|
|
36
|
+
visible={offline.isOffline}
|
|
37
|
+
message={offline.message}
|
|
38
|
+
backgroundColor={offline.backgroundColor || tokens.colors.error}
|
|
39
|
+
position={offline.position || "top"}
|
|
40
|
+
/>
|
|
41
|
+
)}
|
|
42
|
+
|
|
43
|
+
{showFeedback && (
|
|
44
|
+
<FeedbackState
|
|
45
|
+
config={feedbackConfig}
|
|
46
|
+
onClose={onFeedbackClose}
|
|
47
|
+
/>
|
|
48
|
+
)}
|
|
49
|
+
</>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subscription Flow Initial State
|
|
3
|
+
*
|
|
4
|
+
* Default state for the subscription flow state machine.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { SubscriptionFlowStatus, SyncStatus, type SubscriptionFlowState } from "./flowTypes";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Initial state for the subscription flow store.
|
|
11
|
+
* Represents a fresh install scenario.
|
|
12
|
+
*/
|
|
13
|
+
export const initialFlowState: SubscriptionFlowState = {
|
|
14
|
+
status: SubscriptionFlowStatus.INITIALIZING,
|
|
15
|
+
syncStatus: SyncStatus.IDLE,
|
|
16
|
+
syncError: null,
|
|
17
|
+
isOnboardingComplete: false,
|
|
18
|
+
paywallShown: false,
|
|
19
|
+
showFeedback: false,
|
|
20
|
+
isAuthModalOpen: false,
|
|
21
|
+
isInitialized: false,
|
|
22
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subscription Flow State Machine Types
|
|
3
|
+
*
|
|
4
|
+
* Type definitions for the subscription flow state machine.
|
|
5
|
+
* Separated from implementation for better maintainability.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* States in the subscription flow.
|
|
10
|
+
* Represents the user's journey from first launch to premium access.
|
|
11
|
+
*/
|
|
12
|
+
export enum SubscriptionFlowStatus {
|
|
13
|
+
/** App is initializing, determining next state */
|
|
14
|
+
INITIALIZING = "INITIALIZING",
|
|
15
|
+
/** User is seeing onboarding for the first time */
|
|
16
|
+
ONBOARDING = "ONBOARDING",
|
|
17
|
+
/** Checking if user has premium access */
|
|
18
|
+
CHECK_PREMIUM = "CHECK_PREMIUM",
|
|
19
|
+
/** Showing paywall after onboarding */
|
|
20
|
+
POST_ONBOARDING_PAYWALL = "POST_ONBOARDING_PAYWALL",
|
|
21
|
+
/** App is ready to use */
|
|
22
|
+
READY = "READY",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Sync status for subscription/credits data.
|
|
27
|
+
*/
|
|
28
|
+
export enum SyncStatus {
|
|
29
|
+
/** No sync in progress */
|
|
30
|
+
IDLE = "IDLE",
|
|
31
|
+
/** Syncing data from RevenueCat/Firestore */
|
|
32
|
+
SYNCING = "SYNCING",
|
|
33
|
+
/** Sync completed successfully */
|
|
34
|
+
SUCCESS = "SUCCESS",
|
|
35
|
+
/** Sync failed with error */
|
|
36
|
+
ERROR = "ERROR",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Complete state shape for the subscription flow store.
|
|
41
|
+
*/
|
|
42
|
+
export interface SubscriptionFlowState {
|
|
43
|
+
/** Current flow status */
|
|
44
|
+
status: SubscriptionFlowStatus;
|
|
45
|
+
|
|
46
|
+
/** Sync status for subscription/credits data */
|
|
47
|
+
syncStatus: SyncStatus;
|
|
48
|
+
|
|
49
|
+
/** Error message from last sync failure */
|
|
50
|
+
syncError: string | null;
|
|
51
|
+
|
|
52
|
+
/** Whether user has completed onboarding */
|
|
53
|
+
isOnboardingComplete: boolean;
|
|
54
|
+
|
|
55
|
+
/** Whether paywall has been shown at least once */
|
|
56
|
+
paywallShown: boolean;
|
|
57
|
+
|
|
58
|
+
/** Whether to show feedback screen */
|
|
59
|
+
showFeedback: boolean;
|
|
60
|
+
|
|
61
|
+
/** Whether auth modal is currently open */
|
|
62
|
+
isAuthModalOpen: boolean;
|
|
63
|
+
|
|
64
|
+
/** Whether store has been initialized */
|
|
65
|
+
isInitialized: boolean;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Actions available on the subscription flow store.
|
|
70
|
+
*/
|
|
71
|
+
export interface SubscriptionFlowActions {
|
|
72
|
+
/** Mark onboarding as complete and transition to CHECK_PREMIUM */
|
|
73
|
+
completeOnboarding: () => void;
|
|
74
|
+
|
|
75
|
+
/** Show paywall and transition to POST_ONBOARDING_PAYWALL */
|
|
76
|
+
showPaywall: () => void;
|
|
77
|
+
|
|
78
|
+
/** Complete paywall interaction and transition to READY */
|
|
79
|
+
completePaywall: (purchased: boolean) => void;
|
|
80
|
+
|
|
81
|
+
/** Show feedback screen */
|
|
82
|
+
showFeedbackScreen: () => void;
|
|
83
|
+
|
|
84
|
+
/** Hide feedback screen */
|
|
85
|
+
hideFeedback: () => void;
|
|
86
|
+
|
|
87
|
+
/** Open/close auth modal */
|
|
88
|
+
setAuthModalOpen: (open: boolean) => void;
|
|
89
|
+
|
|
90
|
+
/** Update sync status */
|
|
91
|
+
setSyncStatus: (status: SyncStatus, error?: string | null) => void;
|
|
92
|
+
|
|
93
|
+
/** Set initialized flag (internal use) */
|
|
94
|
+
setInitialized: (initialized: boolean) => void;
|
|
95
|
+
|
|
96
|
+
/** Set flow status (internal use) */
|
|
97
|
+
setStatus: (status: SubscriptionFlowStatus) => void;
|
|
98
|
+
|
|
99
|
+
/** Reset flow to initial state */
|
|
100
|
+
resetFlow: () => void;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Combined store type with state and actions.
|
|
105
|
+
*/
|
|
106
|
+
export type SubscriptionFlowStore = SubscriptionFlowState & SubscriptionFlowActions;
|