@umituz/react-native-subscription 3.1.8 → 3.1.10
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/application/CreditsInitializer.ts +3 -2
- package/src/domains/credits/application/DeductCreditsCommand.ts +4 -3
- package/src/domains/credits/application/PurchaseMetadataGenerator.ts +1 -1
- package/src/domains/credits/application/RefundCreditsCommand.ts +4 -3
- package/src/domains/credits/application/creditDocumentHelpers.ts +2 -1
- package/src/domains/credits/infrastructure/CreditsRepository.ts +4 -3
- package/src/domains/credits/infrastructure/operations/CreditsFetcher.ts +1 -2
- package/src/domains/credits/infrastructure/operations/CreditsInitializer.ts +2 -1
- package/src/domains/credits/infrastructure/operations/CreditsWriter.ts +6 -5
- package/src/domains/credits/presentation/useCreditsRealTime.ts +21 -68
- package/src/domains/subscription/presentation/flowInitialState.ts +22 -0
- package/src/domains/subscription/presentation/flowTypes.ts +106 -0
- package/src/domains/subscription/presentation/usePremiumActions.ts +5 -6
- package/src/domains/subscription/presentation/useSubscriptionFlow.ts +25 -92
- package/src/domains/wallet/presentation/hooks/useTransactionHistory.ts +34 -60
- package/src/shared/infrastructure/firestore/collectionUtils.ts +2 -7
- package/src/shared/presentation/hooks/useFirestoreRealTime.ts +214 -0
- package/src/shared/presentation/types/hookState.types.ts +97 -0
- package/src/shared/utils/dateConverter.ts +1 -1
- package/src/shared/utils/errorUtils.ts +195 -0
- package/src/shared/utils/logger.ts +140 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.10",
|
|
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",
|
|
@@ -2,7 +2,8 @@ import type { CreditsConfig } from "../core/Credits";
|
|
|
2
2
|
import { getAppVersion, validatePlatform } from "../../../utils/appUtils";
|
|
3
3
|
|
|
4
4
|
import type { InitializeCreditsMetadata, InitializationResult } from "../../subscription/application/SubscriptionInitializerTypes";
|
|
5
|
-
import { runTransaction, type
|
|
5
|
+
import { runTransaction, type DocumentReference } from "firebase/firestore";
|
|
6
|
+
import type { Firestore } from "@umituz/react-native-firebase";
|
|
6
7
|
import { getCreditDocumentOrDefault } from "./creditDocumentHelpers";
|
|
7
8
|
import { calculateNewCredits, buildCreditsData } from "./creditOperationUtils";
|
|
8
9
|
import { CreditLimitService } from "../domain/services/CreditLimitService";
|
|
@@ -39,7 +40,7 @@ export async function initializeCreditsTransaction(
|
|
|
39
40
|
const platform = validatePlatform();
|
|
40
41
|
const appVersion = getAppVersion();
|
|
41
42
|
|
|
42
|
-
return runTransaction(async (transaction
|
|
43
|
+
return runTransaction(_db, async (transaction) => {
|
|
43
44
|
const creditsDoc = await transaction.get(creditsRef);
|
|
44
45
|
|
|
45
46
|
const existingData = getCreditDocumentOrDefault(creditsDoc, platform);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { runTransaction, serverTimestamp, type
|
|
1
|
+
import { runTransaction, serverTimestamp, type DocumentReference } from "firebase/firestore";
|
|
2
|
+
import type { Firestore } from "@umituz/react-native-firebase";
|
|
2
3
|
import type { DeductCreditsResult } from "../core/Credits";
|
|
3
4
|
import { CREDIT_ERROR_CODES, MAX_SINGLE_DEDUCTION } from "../core/CreditsConstants";
|
|
4
5
|
|
|
@@ -33,7 +34,7 @@ export async function deductCreditsOperation(
|
|
|
33
34
|
try {
|
|
34
35
|
if (__DEV__) console.log('[DeductCreditsCommand] >>> starting transaction', { userId, cost, creditsRefPath: creditsRef.path });
|
|
35
36
|
|
|
36
|
-
const remaining = await runTransaction(async (tx
|
|
37
|
+
const remaining = await runTransaction(_db, async (tx) => {
|
|
37
38
|
const docSnap = await tx.get(creditsRef);
|
|
38
39
|
|
|
39
40
|
if (__DEV__) console.log('[DeductCreditsCommand] doc exists:', docSnap.exists());
|
|
@@ -66,7 +67,7 @@ export async function deductCreditsOperation(
|
|
|
66
67
|
|
|
67
68
|
return {
|
|
68
69
|
success: true,
|
|
69
|
-
remainingCredits: remaining,
|
|
70
|
+
remainingCredits: remaining as number,
|
|
70
71
|
error: null
|
|
71
72
|
};
|
|
72
73
|
} catch (e: unknown) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { runTransaction, serverTimestamp, type
|
|
1
|
+
import { runTransaction, serverTimestamp, type DocumentReference } from "firebase/firestore";
|
|
2
|
+
import type { Firestore } from "@umituz/react-native-firebase";
|
|
2
3
|
import type { DeductCreditsResult } from "../core/Credits";
|
|
3
4
|
import { CREDIT_ERROR_CODES } from "../core/CreditsConstants";
|
|
4
5
|
|
|
@@ -31,7 +32,7 @@ export async function refundCreditsOperation(
|
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
try {
|
|
34
|
-
const remaining = await runTransaction(async (tx
|
|
35
|
+
const remaining = await runTransaction(_db, async (tx) => {
|
|
35
36
|
const docSnap = await tx.get(creditsRef);
|
|
36
37
|
|
|
37
38
|
if (!docSnap.exists()) {
|
|
@@ -54,7 +55,7 @@ export async function refundCreditsOperation(
|
|
|
54
55
|
|
|
55
56
|
return {
|
|
56
57
|
success: true,
|
|
57
|
-
remainingCredits: remaining,
|
|
58
|
+
remainingCredits: remaining as number,
|
|
58
59
|
error: null
|
|
59
60
|
};
|
|
60
61
|
} catch (e: unknown) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { UserCreditsDocumentRead, FirestoreTimestamp } from "../core/UserCreditsDocument";
|
|
2
|
-
import { serverTimestamp
|
|
2
|
+
import { serverTimestamp } from "firebase/firestore";
|
|
3
|
+
import type { DocumentSnapshot } from "firebase/firestore";
|
|
3
4
|
import { SUBSCRIPTION_STATUS, type Platform } from "../../subscription/core/SubscriptionConstants";
|
|
4
5
|
|
|
5
6
|
export function getCreditDocumentOrDefault(
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { DocumentReference } from "firebase/firestore";
|
|
2
|
+
import type { Firestore } from "@umituz/react-native-firebase";
|
|
2
3
|
import { BaseRepository } from "@umituz/react-native-firebase";
|
|
3
4
|
import type { CreditsConfig, CreditsResult, DeductCreditsResult } from "../core/Credits";
|
|
4
5
|
import type { PurchaseSource } from "../core/UserCreditsDocument";
|
|
@@ -86,12 +87,12 @@ export class CreditsRepository extends BaseRepository {
|
|
|
86
87
|
|
|
87
88
|
async syncExpiredStatus(userId: string): Promise<void> {
|
|
88
89
|
const db = requireFirestore();
|
|
89
|
-
await syncExpiredStatus(this.getRef(db, userId));
|
|
90
|
+
await syncExpiredStatus(db, this.getRef(db, userId));
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
async syncPremiumMetadata(userId: string, metadata: SubscriptionMetadata): Promise<void> {
|
|
93
94
|
const db = requireFirestore();
|
|
94
|
-
await syncPremiumMetadata(this.getRef(db, userId), metadata);
|
|
95
|
+
await syncPremiumMetadata(db, this.getRef(db, userId), metadata);
|
|
95
96
|
}
|
|
96
97
|
|
|
97
98
|
async ensurePremiumCreditsExist(
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { getDoc } from "firebase/firestore";
|
|
2
|
-
import type { DocumentReference } from "@umituz/react-native-firebase";
|
|
1
|
+
import { getDoc, type DocumentReference } from "firebase/firestore";
|
|
3
2
|
import type { CreditsResult } from "../../core/Credits";
|
|
4
3
|
import type { UserCreditsDocumentRead } from "../../core/UserCreditsDocument";
|
|
5
4
|
import { mapCreditsDocumentToEntity } from "../../core/CreditsMapper";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { DocumentReference } from "firebase/firestore";
|
|
2
|
+
import type { Firestore } from "@umituz/react-native-firebase";
|
|
2
3
|
import type { CreditsConfig, CreditsResult } from "../../core/Credits";
|
|
3
4
|
import type { PurchaseSource } from "../../core/UserCreditsDocument";
|
|
4
5
|
import { initializeCreditsTransaction } from "../../application/CreditsInitializer";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import { runTransaction, serverTimestamp, type DocumentReference } from "firebase/firestore";
|
|
2
|
+
import type { Firestore } from "@umituz/react-native-firebase";
|
|
3
3
|
import { getDoc, setDoc } from "firebase/firestore";
|
|
4
4
|
import { SUBSCRIPTION_STATUS } from "../../../subscription/core/SubscriptionConstants";
|
|
5
5
|
import { resolveSubscriptionStatus } from "../../../subscription/core/SubscriptionStatus";
|
|
@@ -11,8 +11,8 @@ import { getAppVersion, validatePlatform } from "../../../../utils/appUtils";
|
|
|
11
11
|
// Fix: was getDoc+setDoc (non-atomic) — now uses runTransaction so concurrent
|
|
12
12
|
// initializeCreditsTransaction and deductCreditsOperation no longer see stale
|
|
13
13
|
// updateTime preconditions that produce failed-precondition errors.
|
|
14
|
-
export async function syncExpiredStatus(ref: DocumentReference): Promise<void> {
|
|
15
|
-
await runTransaction(async (tx
|
|
14
|
+
export async function syncExpiredStatus(db: Firestore, ref: DocumentReference): Promise<void> {
|
|
15
|
+
await runTransaction(db, async (tx) => {
|
|
16
16
|
const doc = await tx.get(ref);
|
|
17
17
|
if (!doc.exists()) return;
|
|
18
18
|
|
|
@@ -27,10 +27,11 @@ export async function syncExpiredStatus(ref: DocumentReference): Promise<void> {
|
|
|
27
27
|
|
|
28
28
|
// Fix: was getDoc+setDoc (non-atomic) — now uses runTransaction.
|
|
29
29
|
export async function syncPremiumMetadata(
|
|
30
|
+
db: Firestore,
|
|
30
31
|
ref: DocumentReference,
|
|
31
32
|
metadata: SubscriptionMetadata
|
|
32
33
|
): Promise<void> {
|
|
33
|
-
await runTransaction(async (tx
|
|
34
|
+
await runTransaction(db, async (tx) => {
|
|
34
35
|
const doc = await tx.get(ref);
|
|
35
36
|
if (!doc.exists()) return;
|
|
36
37
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import type {
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import type { DocumentReference } from "firebase/firestore";
|
|
3
|
+
import type { UserCreditsDocumentRead } from "../core/UserCreditsDocument";
|
|
4
4
|
import { getCreditsConfig } from "../infrastructure/CreditsRepositoryManager";
|
|
5
5
|
import { mapCreditsDocumentToEntity } from "../core/CreditsMapper";
|
|
6
|
-
import { requireFirestore, buildDocRef
|
|
7
|
-
import
|
|
6
|
+
import { requireFirestore, buildDocRef } from "../../../shared/infrastructure/firestore/collectionUtils";
|
|
7
|
+
import { useFirestoreDocumentRealTime } from "../../../shared/presentation/hooks/useFirestoreRealTime";
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Real-time sync for credits using Firestore onSnapshot.
|
|
@@ -20,76 +20,29 @@ import type { UserCreditsDocumentRead } from "../core/UserCreditsDocument";
|
|
|
20
20
|
* @returns Credits state and loading status
|
|
21
21
|
*/
|
|
22
22
|
export function useCreditsRealTime(userId: string | null | undefined) {
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
useEffect(() => {
|
|
28
|
-
// Reset state when userId changes
|
|
29
|
-
if (!userId) {
|
|
30
|
-
setCredits(null);
|
|
31
|
-
setIsLoading(false);
|
|
32
|
-
setError(null);
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
setIsLoading(true);
|
|
37
|
-
setError(null);
|
|
23
|
+
// Build document reference
|
|
24
|
+
const docRef = useMemo(() => {
|
|
25
|
+
if (!userId) return null;
|
|
38
26
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
// Build doc ref using same logic as repository
|
|
44
|
-
const collectionConfig: CollectionConfig = {
|
|
45
|
-
collectionName: config.collectionName,
|
|
46
|
-
useUserSubcollection: config.useUserSubcollection,
|
|
47
|
-
};
|
|
48
|
-
const docRef = buildDocRef(db, userId, "balance", collectionConfig);
|
|
49
|
-
|
|
50
|
-
// Real-time listener
|
|
51
|
-
const unsubscribe = onSnapshot(
|
|
52
|
-
docRef,
|
|
53
|
-
(snapshot) => {
|
|
54
|
-
if (snapshot.exists()) {
|
|
55
|
-
const entity = mapCreditsDocumentToEntity(snapshot.data() as UserCreditsDocumentRead);
|
|
56
|
-
setCredits(entity);
|
|
57
|
-
} else {
|
|
58
|
-
setCredits(null);
|
|
59
|
-
}
|
|
60
|
-
setIsLoading(false);
|
|
61
|
-
},
|
|
62
|
-
(err) => {
|
|
63
|
-
console.error("[useCreditsRealTime] Snapshot error:", err);
|
|
64
|
-
setError(err as Error);
|
|
65
|
-
setIsLoading(false);
|
|
66
|
-
}
|
|
67
|
-
);
|
|
68
|
-
|
|
69
|
-
return () => {
|
|
70
|
-
unsubscribe();
|
|
71
|
-
};
|
|
72
|
-
} catch (err) {
|
|
73
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
74
|
-
console.error("[useCreditsRealTime] Setup error:", err);
|
|
75
|
-
setError(error);
|
|
76
|
-
setIsLoading(false);
|
|
77
|
-
}
|
|
27
|
+
const db = requireFirestore();
|
|
28
|
+
const config = getCreditsConfig();
|
|
29
|
+
const ref = buildDocRef(db, userId, "balance", config);
|
|
30
|
+
return ref as DocumentReference<UserCreditsDocumentRead>;
|
|
78
31
|
}, [userId]);
|
|
79
32
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
33
|
+
// Use generic real-time sync hook
|
|
34
|
+
const { data, isLoading, error, refetch } = useFirestoreDocumentRealTime(
|
|
35
|
+
userId,
|
|
36
|
+
docRef,
|
|
37
|
+
mapCreditsDocumentToEntity,
|
|
38
|
+
"useCreditsRealTime"
|
|
39
|
+
);
|
|
87
40
|
|
|
88
41
|
return {
|
|
89
|
-
credits,
|
|
42
|
+
credits: data,
|
|
90
43
|
isLoading,
|
|
91
44
|
error,
|
|
92
|
-
refetch,
|
|
45
|
+
refetch,
|
|
93
46
|
};
|
|
94
47
|
}
|
|
95
48
|
|
|
@@ -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;
|
|
@@ -5,6 +5,9 @@ import {
|
|
|
5
5
|
useRestorePurchase,
|
|
6
6
|
} from '../infrastructure/hooks/useSubscriptionQueries';
|
|
7
7
|
import { usePaywallVisibility } from './usePaywallVisibility';
|
|
8
|
+
import { createLogger } from '../../../shared/utils/logger';
|
|
9
|
+
|
|
10
|
+
const logger = createLogger('usePremiumActions');
|
|
8
11
|
|
|
9
12
|
export interface PremiumActions {
|
|
10
13
|
purchasePackage: (pkg: PurchasesPackage) => Promise<boolean>;
|
|
@@ -38,9 +41,7 @@ export function usePremiumActions(): PremiumActions {
|
|
|
38
41
|
const result = await purchaseMutation.mutateAsync(pkg);
|
|
39
42
|
return result.success;
|
|
40
43
|
} catch (error) {
|
|
41
|
-
|
|
42
|
-
console.error('[usePremiumActions] Purchase failed:', error);
|
|
43
|
-
}
|
|
44
|
+
logger.error('Purchase failed', error, { packageId: pkg.identifier });
|
|
44
45
|
return false;
|
|
45
46
|
}
|
|
46
47
|
},
|
|
@@ -52,9 +53,7 @@ export function usePremiumActions(): PremiumActions {
|
|
|
52
53
|
const result = await restoreMutation.mutateAsync();
|
|
53
54
|
return result.success;
|
|
54
55
|
} catch (error) {
|
|
55
|
-
|
|
56
|
-
console.error('[usePremiumActions] Restore failed:', error);
|
|
57
|
-
}
|
|
56
|
+
logger.error('Restore failed', error);
|
|
58
57
|
return false;
|
|
59
58
|
}
|
|
60
59
|
}, [restoreMutation]);
|
|
@@ -3,109 +3,35 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Single source of truth for app flow state.
|
|
5
5
|
* Clean state transitions without complex if/else logic.
|
|
6
|
+
*
|
|
7
|
+
* State transition rules:
|
|
8
|
+
* - INITIALIZING -> ONBOARDING (first launch)
|
|
9
|
+
* - INITIALIZING -> CHECK_PREMIUM (onboarding already done)
|
|
10
|
+
* - ONBOARDING -> CHECK_PREMIUM (onboarding completed)
|
|
11
|
+
* - CHECK_PREMIUM -> READY (user is premium)
|
|
12
|
+
* - CHECK_PREMIUM -> POST_ONBOARDING_PAYWALL (user not premium, paywall not shown)
|
|
13
|
+
* - CHECK_PREMIUM -> READY (user not premium but paywall already shown)
|
|
14
|
+
* - POST_ONBOARDING_PAYWALL -> READY (paywall closed)
|
|
15
|
+
* - READY -> READY (stays ready, shows overlays when needed)
|
|
6
16
|
*/
|
|
7
17
|
|
|
8
18
|
import { createStore } from "@umituz/react-native-design-system/storage";
|
|
9
19
|
import { subscriptionEventBus, FLOW_EVENTS } from "../../../shared/infrastructure/SubscriptionEventBus";
|
|
20
|
+
import {
|
|
21
|
+
SubscriptionFlowStatus,
|
|
22
|
+
SyncStatus,
|
|
23
|
+
type SubscriptionFlowState,
|
|
24
|
+
type SubscriptionFlowActions,
|
|
25
|
+
} from "./flowTypes";
|
|
26
|
+
import { initialFlowState } from "./flowInitialState";
|
|
10
27
|
|
|
11
|
-
export enum SubscriptionFlowStatus {
|
|
12
|
-
INITIALIZING = "INITIALIZING",
|
|
13
|
-
ONBOARDING = "ONBOARDING",
|
|
14
|
-
CHECK_PREMIUM = "CHECK_PREMIUM",
|
|
15
|
-
POST_ONBOARDING_PAYWALL = "POST_ONBOARDING_PAYWALL",
|
|
16
|
-
READY = "READY",
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export enum SyncStatus {
|
|
20
|
-
IDLE = "IDLE",
|
|
21
|
-
SYNCING = "SYNCING",
|
|
22
|
-
SUCCESS = "SUCCESS",
|
|
23
|
-
ERROR = "ERROR",
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface SubscriptionFlowState {
|
|
27
|
-
// Flow state
|
|
28
|
-
status: SubscriptionFlowStatus;
|
|
29
|
-
|
|
30
|
-
// Sync state
|
|
31
|
-
syncStatus: SyncStatus;
|
|
32
|
-
syncError: string | null;
|
|
33
|
-
|
|
34
|
-
// Onboarding state
|
|
35
|
-
isOnboardingComplete: boolean;
|
|
36
|
-
|
|
37
|
-
// Paywall state
|
|
38
|
-
paywallShown: boolean;
|
|
39
|
-
|
|
40
|
-
// Feedback state
|
|
41
|
-
showFeedback: boolean;
|
|
42
|
-
|
|
43
|
-
// Auth modal state
|
|
44
|
-
isAuthModalOpen: boolean;
|
|
45
|
-
|
|
46
|
-
// Initialization flag
|
|
47
|
-
isInitialized: boolean;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export interface SubscriptionFlowActions {
|
|
51
|
-
// Flow actions
|
|
52
|
-
completeOnboarding: () => void;
|
|
53
|
-
showPaywall: () => void;
|
|
54
|
-
completePaywall: (purchased: boolean) => void;
|
|
55
|
-
showFeedbackScreen: () => void;
|
|
56
|
-
hideFeedback: () => void;
|
|
57
|
-
|
|
58
|
-
// Auth actions
|
|
59
|
-
setAuthModalOpen: (open: boolean) => void;
|
|
60
|
-
|
|
61
|
-
// Sync actions
|
|
62
|
-
setSyncStatus: (status: SyncStatus, error?: string | null) => void;
|
|
63
|
-
|
|
64
|
-
// State setters (for internal use)
|
|
65
|
-
setInitialized: (initialized: boolean) => void;
|
|
66
|
-
setStatus: (status: SubscriptionFlowStatus) => void;
|
|
67
|
-
|
|
68
|
-
// Reset
|
|
69
|
-
resetFlow: () => void;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export type SubscriptionFlowStore = SubscriptionFlowState & SubscriptionFlowActions;
|
|
73
|
-
|
|
74
|
-
const initialState: SubscriptionFlowState = {
|
|
75
|
-
status: SubscriptionFlowStatus.INITIALIZING,
|
|
76
|
-
syncStatus: SyncStatus.IDLE,
|
|
77
|
-
syncError: null,
|
|
78
|
-
isOnboardingComplete: false,
|
|
79
|
-
paywallShown: false,
|
|
80
|
-
showFeedback: false,
|
|
81
|
-
isAuthModalOpen: false,
|
|
82
|
-
isInitialized: false,
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* State transition rules:
|
|
87
|
-
*
|
|
88
|
-
* INITIALIZING -> ONBOARDING (first launch)
|
|
89
|
-
* INITIALIZING -> CHECK_PREMIUM (onboarding already done)
|
|
90
|
-
*
|
|
91
|
-
* ONBOARDING -> CHECK_PREMIUM (onboarding completed)
|
|
92
|
-
*
|
|
93
|
-
* CHECK_PREMIUM -> READY (user is premium)
|
|
94
|
-
* CHECK_PREMIUM -> POST_ONBOARDING_PAYWALL (user not premium, paywall not shown)
|
|
95
|
-
* CHECK_PREMIUM -> READY (user not premium but paywall already shown)
|
|
96
|
-
*
|
|
97
|
-
* POST_ONBOARDING_PAYWALL -> READY (paywall closed)
|
|
98
|
-
*
|
|
99
|
-
* READY -> READY (stays ready, shows overlays when needed)
|
|
100
|
-
*/
|
|
101
28
|
export const useSubscriptionFlowStore = createStore<SubscriptionFlowState, SubscriptionFlowActions>({
|
|
102
29
|
name: "subscription-flow-storage",
|
|
103
|
-
initialState,
|
|
30
|
+
initialState: initialFlowState,
|
|
104
31
|
persist: true,
|
|
105
32
|
onRehydrate: (state) => {
|
|
106
33
|
if (!state.isInitialized) {
|
|
107
34
|
state.setInitialized(true);
|
|
108
|
-
|
|
109
35
|
// First time: show onboarding
|
|
110
36
|
state.setStatus(SubscriptionFlowStatus.INITIALIZING);
|
|
111
37
|
} else if (state.isOnboardingComplete) {
|
|
@@ -176,3 +102,10 @@ export const useSubscriptionFlowStore = createStore<SubscriptionFlowState, Subsc
|
|
|
176
102
|
},
|
|
177
103
|
}),
|
|
178
104
|
});
|
|
105
|
+
|
|
106
|
+
// Re-export types for convenience
|
|
107
|
+
export type { SubscriptionFlowState, SubscriptionFlowActions } from "./flowTypes";
|
|
108
|
+
export { SubscriptionFlowStatus, SyncStatus } from "./flowTypes";
|
|
109
|
+
|
|
110
|
+
// Re-export store type inferred from createStore
|
|
111
|
+
export type SubscriptionFlowStore = ReturnType<typeof useSubscriptionFlowStore>;
|