@umituz/react-native-subscription 3.1.10 → 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.
Files changed (39) hide show
  1. package/package.json +1 -1
  2. package/src/domains/credits/presentation/useCreditsRealTime.ts +10 -5
  3. package/src/domains/credits/utils/creditValidation.ts +5 -26
  4. package/src/domains/paywall/hooks/usePaywallActions.ts +21 -133
  5. package/src/domains/paywall/hooks/usePaywallActions.types.ts +16 -0
  6. package/src/domains/paywall/hooks/usePaywallPurchase.ts +78 -0
  7. package/src/domains/paywall/hooks/usePaywallRestore.ts +66 -0
  8. package/src/domains/revenuecat/infrastructure/services/userSwitchCore.ts +116 -0
  9. package/src/domains/revenuecat/infrastructure/services/userSwitchHandler.ts +19 -237
  10. package/src/domains/revenuecat/infrastructure/services/userSwitchHelpers.ts +55 -0
  11. package/src/domains/revenuecat/infrastructure/services/userSwitchInitializer.ts +143 -0
  12. package/src/domains/subscription/infrastructure/managers/SubscriptionManager.ts +6 -3
  13. package/src/domains/subscription/infrastructure/managers/initializationHandler.ts +2 -2
  14. package/src/domains/subscription/infrastructure/managers/packageHandlerFactory.ts +2 -2
  15. package/src/domains/subscription/infrastructure/managers/subscriptionManagerUtils.ts +2 -2
  16. package/src/domains/subscription/presentation/components/ManagedSubscriptionFlow.logic.ts +52 -0
  17. package/src/domains/subscription/presentation/components/ManagedSubscriptionFlow.tsx +15 -89
  18. package/src/domains/subscription/presentation/components/ManagedSubscriptionFlow.types.ts +59 -0
  19. package/src/domains/subscription/presentation/components/details/CreditRow.tsx +9 -0
  20. package/src/domains/subscription/presentation/components/details/PremiumDetailsCard.tsx +23 -0
  21. package/src/domains/subscription/presentation/components/states/FeedbackState.tsx +36 -0
  22. package/src/domains/subscription/presentation/components/states/InitializingState.tsx +47 -0
  23. package/src/domains/subscription/presentation/components/states/OnboardingState.tsx +27 -0
  24. package/src/domains/subscription/presentation/components/states/PaywallState.tsx +66 -0
  25. package/src/domains/subscription/presentation/components/states/ReadyState.tsx +51 -0
  26. package/src/domains/subscription/presentation/screens/components/SubscriptionHeaderContent.tsx +119 -103
  27. package/src/domains/wallet/presentation/components/BalanceCard.tsx +7 -0
  28. package/src/domains/wallet/presentation/components/TransactionItem.tsx +11 -0
  29. package/src/index.components.ts +1 -1
  30. package/src/shared/infrastructure/SubscriptionEventBus.ts +4 -2
  31. package/src/shared/presentation/hooks/useFirestoreRealTime.ts +22 -6
  32. package/src/shared/utils/errors/errorAssertions.ts +35 -0
  33. package/src/shared/utils/errors/errorConversion.ts +73 -0
  34. package/src/shared/utils/errors/errorTypeGuards.ts +27 -0
  35. package/src/shared/utils/errors/errorWrappers.ts +54 -0
  36. package/src/shared/utils/errors/index.ts +19 -0
  37. package/src/shared/utils/errors/serviceErrors.ts +36 -0
  38. package/src/domains/subscription/presentation/components/ManagedSubscriptionFlow.states.tsx +0 -187
  39. package/src/shared/utils/errorUtils.ts +0 -195
@@ -2,77 +2,21 @@
2
2
  * ManagedSubscriptionFlow
3
3
  *
4
4
  * Clean state machine-based flow orchestration.
5
- * All state components separated to individual files.
5
+ * State components and logic separated to individual modules.
6
6
  */
7
7
 
8
- import React, { useEffect } from "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 type { PaywallFeedbackTranslations } from "./feedback/PaywallFeedbackScreen.types";
17
- import type { PaywallTranslations, PaywallLegalUrls, SubscriptionFeature } from "../../../paywall/entities/types";
18
- import {
19
- InitializingState,
20
- OnboardingState,
21
- PaywallState,
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
- // STATE TRANSITIONS
108
- // ========================================================================
109
-
110
- useEffect(() => {
111
- if (status === SubscriptionFlowStatus.CHECK_PREMIUM && !isSyncing) {
112
- const paywallShown = useSubscriptionFlowStore.getState().paywallShown;
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
+ };