@umituz/react-native-subscription 2.17.10 → 2.17.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/domain/entities/Credits.ts +17 -0
- package/src/index.ts +17 -1
- package/src/infrastructure/mappers/CreditsMapper.ts +7 -0
- package/src/infrastructure/models/UserCreditsDocument.ts +29 -0
- package/src/infrastructure/repositories/CreditsRepository.ts +22 -4
- package/src/infrastructure/services/CreditsInitializer.ts +74 -2
- package/src/infrastructure/services/SubscriptionInitializer.ts +13 -3
- package/src/infrastructure/stores/PendingPurchaseStore.ts +56 -0
- package/src/presentation/hooks/useAuthAwarePurchase.ts +24 -6
- package/src/presentation/hooks/usePendingPurchaseHandler.ts +78 -0
- package/src/presentation/hooks/useSubscriptionSettingsConfig.ts +9 -1
- package/src/revenuecat/domain/value-objects/RevenueCatConfig.ts +2 -1
- package/src/revenuecat/infrastructure/services/PurchaseHandler.ts +22 -6
- package/src/revenuecat/infrastructure/utils/PremiumStatusSyncer.ts +3 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.17.
|
|
3
|
+
"version": "2.17.11",
|
|
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",
|
|
@@ -9,8 +9,25 @@ import type { SubscriptionPackageType } from "../../utils/packageTypeDetector";
|
|
|
9
9
|
|
|
10
10
|
export type CreditType = "text" | "image";
|
|
11
11
|
|
|
12
|
+
export type PurchaseSource =
|
|
13
|
+
| "onboarding"
|
|
14
|
+
| "settings"
|
|
15
|
+
| "upgrade_prompt"
|
|
16
|
+
| "home_screen"
|
|
17
|
+
| "feature_gate"
|
|
18
|
+
| "credits_exhausted";
|
|
19
|
+
|
|
20
|
+
export type PurchaseType = "initial" | "renewal" | "upgrade" | "downgrade";
|
|
21
|
+
|
|
12
22
|
export interface UserCredits {
|
|
13
23
|
credits: number;
|
|
24
|
+
packageType?: "weekly" | "monthly" | "yearly";
|
|
25
|
+
creditLimit?: number;
|
|
26
|
+
productId?: string;
|
|
27
|
+
purchaseSource?: PurchaseSource;
|
|
28
|
+
purchaseType?: PurchaseType;
|
|
29
|
+
platform?: "ios" | "android";
|
|
30
|
+
appVersion?: string;
|
|
14
31
|
purchasedAt: Date | null;
|
|
15
32
|
lastUpdatedAt: Date | null;
|
|
16
33
|
}
|
package/src/index.ts
CHANGED
|
@@ -17,6 +17,14 @@ export { SubscriptionService, initializeSubscriptionService } from "./infrastruc
|
|
|
17
17
|
export { initializeSubscription, type SubscriptionInitConfig, type CreditPackageConfig } from "./infrastructure/services/SubscriptionInitializer";
|
|
18
18
|
export { CreditsRepository, createCreditsRepository } from "./infrastructure/repositories/CreditsRepository";
|
|
19
19
|
export { configureCreditsRepository, getCreditsRepository, getCreditsConfig, resetCreditsRepository, isCreditsRepositoryConfigured } from "./infrastructure/repositories/CreditsRepositoryProvider";
|
|
20
|
+
export {
|
|
21
|
+
usePendingPurchaseStore,
|
|
22
|
+
type PendingPurchaseData,
|
|
23
|
+
} from "./infrastructure/stores/PendingPurchaseStore";
|
|
24
|
+
export {
|
|
25
|
+
usePendingPurchaseHandler,
|
|
26
|
+
type UsePendingPurchaseHandlerParams,
|
|
27
|
+
} from "./presentation/hooks/usePendingPurchaseHandler";
|
|
20
28
|
|
|
21
29
|
// Presentation Layer - Hooks
|
|
22
30
|
export * from "./presentation/hooks";
|
|
@@ -30,7 +38,15 @@ export * from "./presentation/screens/SubscriptionDetailScreen";
|
|
|
30
38
|
export * from "./presentation/types/SubscriptionDetailTypes";
|
|
31
39
|
|
|
32
40
|
// Credits Domain
|
|
33
|
-
export type {
|
|
41
|
+
export type {
|
|
42
|
+
CreditType,
|
|
43
|
+
UserCredits,
|
|
44
|
+
CreditsConfig,
|
|
45
|
+
CreditsResult,
|
|
46
|
+
DeductCreditsResult,
|
|
47
|
+
PurchaseSource,
|
|
48
|
+
PurchaseType,
|
|
49
|
+
} from "./domain/entities/Credits";
|
|
34
50
|
export { DEFAULT_CREDITS_CONFIG } from "./domain/entities/Credits";
|
|
35
51
|
export { InsufficientCreditsError } from "./domain/errors/InsufficientCreditsError";
|
|
36
52
|
|
|
@@ -5,6 +5,13 @@ export class CreditsMapper {
|
|
|
5
5
|
static toEntity(snapData: UserCreditsDocumentRead): UserCredits {
|
|
6
6
|
return {
|
|
7
7
|
credits: snapData.credits,
|
|
8
|
+
packageType: snapData.packageType,
|
|
9
|
+
creditLimit: snapData.creditLimit,
|
|
10
|
+
productId: snapData.productId,
|
|
11
|
+
purchaseSource: snapData.purchaseSource,
|
|
12
|
+
purchaseType: snapData.purchaseType,
|
|
13
|
+
platform: snapData.platform,
|
|
14
|
+
appVersion: snapData.appVersion,
|
|
8
15
|
purchasedAt: snapData.purchasedAt?.toDate?.() || null,
|
|
9
16
|
lastUpdatedAt: snapData.lastUpdatedAt?.toDate?.() || null,
|
|
10
17
|
};
|
|
@@ -3,11 +3,40 @@ export interface FirestoreTimestamp {
|
|
|
3
3
|
toDate: () => Date;
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
+
export type PurchaseSource =
|
|
7
|
+
| "onboarding"
|
|
8
|
+
| "settings"
|
|
9
|
+
| "upgrade_prompt"
|
|
10
|
+
| "home_screen"
|
|
11
|
+
| "feature_gate"
|
|
12
|
+
| "credits_exhausted";
|
|
13
|
+
|
|
14
|
+
export type PurchaseType = "initial" | "renewal" | "upgrade" | "downgrade";
|
|
15
|
+
|
|
16
|
+
export interface PurchaseMetadata {
|
|
17
|
+
productId: string;
|
|
18
|
+
packageType: "weekly" | "monthly" | "yearly";
|
|
19
|
+
creditLimit: number;
|
|
20
|
+
source: PurchaseSource;
|
|
21
|
+
type: PurchaseType;
|
|
22
|
+
platform: "ios" | "android";
|
|
23
|
+
appVersion?: string;
|
|
24
|
+
timestamp: FirestoreTimestamp;
|
|
25
|
+
}
|
|
26
|
+
|
|
6
27
|
// Document structure when READING from Firestore
|
|
7
28
|
export interface UserCreditsDocumentRead {
|
|
8
29
|
credits: number;
|
|
30
|
+
packageType?: "weekly" | "monthly" | "yearly";
|
|
31
|
+
creditLimit?: number;
|
|
32
|
+
productId?: string;
|
|
33
|
+
purchaseSource?: PurchaseSource;
|
|
34
|
+
purchaseType?: PurchaseType;
|
|
35
|
+
platform?: "ios" | "android";
|
|
36
|
+
appVersion?: string;
|
|
9
37
|
purchasedAt?: FirestoreTimestamp;
|
|
10
38
|
lastUpdatedAt?: FirestoreTimestamp;
|
|
11
39
|
lastPurchaseAt?: FirestoreTimestamp;
|
|
12
40
|
processedPurchases?: string[];
|
|
41
|
+
purchaseHistory?: PurchaseMetadata[];
|
|
13
42
|
}
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
import { doc, getDoc, runTransaction, serverTimestamp, type Firestore, type Transaction } from "firebase/firestore";
|
|
6
6
|
import { BaseRepository, getFirestore } from "@umituz/react-native-firebase";
|
|
7
7
|
import type { CreditsConfig, CreditsResult, DeductCreditsResult } from "../../domain/entities/Credits";
|
|
8
|
-
import type { UserCreditsDocumentRead } from "../models/UserCreditsDocument";
|
|
9
|
-
import { initializeCreditsTransaction } from "../services/CreditsInitializer";
|
|
8
|
+
import type { UserCreditsDocumentRead, PurchaseSource } from "../models/UserCreditsDocument";
|
|
9
|
+
import { initializeCreditsTransaction, type InitializeCreditsMetadata } from "../services/CreditsInitializer";
|
|
10
10
|
import { detectPackageType } from "../../utils/packageTypeDetector";
|
|
11
11
|
import { getCreditAllocation } from "../../utils/creditMapper";
|
|
12
12
|
|
|
@@ -32,7 +32,12 @@ export class CreditsRepository extends BaseRepository {
|
|
|
32
32
|
} catch (e: any) { return { success: false, error: { message: e.message, code: "FETCH_ERR" } }; }
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
async initializeCredits(
|
|
35
|
+
async initializeCredits(
|
|
36
|
+
userId: string,
|
|
37
|
+
purchaseId?: string,
|
|
38
|
+
productId?: string,
|
|
39
|
+
source?: PurchaseSource
|
|
40
|
+
): Promise<CreditsResult> {
|
|
36
41
|
const db = getFirestore();
|
|
37
42
|
if (!db) return { success: false, error: { message: "No DB", code: "INIT_ERR" } };
|
|
38
43
|
try {
|
|
@@ -46,7 +51,20 @@ export class CreditsRepository extends BaseRepository {
|
|
|
46
51
|
if (dynamicLimit !== null) cfg = { ...cfg, creditLimit: dynamicLimit };
|
|
47
52
|
}
|
|
48
53
|
}
|
|
49
|
-
|
|
54
|
+
|
|
55
|
+
const metadata: InitializeCreditsMetadata = {
|
|
56
|
+
productId,
|
|
57
|
+
source,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const res = await initializeCreditsTransaction(
|
|
61
|
+
db,
|
|
62
|
+
this.getRef(db, userId),
|
|
63
|
+
cfg,
|
|
64
|
+
purchaseId,
|
|
65
|
+
metadata
|
|
66
|
+
);
|
|
67
|
+
|
|
50
68
|
return {
|
|
51
69
|
success: true,
|
|
52
70
|
data: CreditsMapper.toEntity({
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { Platform } from "react-native";
|
|
2
|
+
import Constants from "expo-constants";
|
|
1
3
|
import {
|
|
2
4
|
runTransaction,
|
|
3
5
|
serverTimestamp,
|
|
@@ -7,17 +9,31 @@ import {
|
|
|
7
9
|
type DocumentReference,
|
|
8
10
|
} from "firebase/firestore";
|
|
9
11
|
import type { CreditsConfig } from "../../domain/entities/Credits";
|
|
10
|
-
import type {
|
|
12
|
+
import type {
|
|
13
|
+
UserCreditsDocumentRead,
|
|
14
|
+
PurchaseSource,
|
|
15
|
+
PurchaseType,
|
|
16
|
+
PurchaseMetadata,
|
|
17
|
+
} from "../models/UserCreditsDocument";
|
|
18
|
+
import { detectPackageType } from "../../utils/packageTypeDetector";
|
|
19
|
+
import { getCreditAllocation } from "../../utils/creditMapper";
|
|
11
20
|
|
|
12
21
|
interface InitializationResult {
|
|
13
22
|
credits: number;
|
|
14
23
|
}
|
|
15
24
|
|
|
25
|
+
export interface InitializeCreditsMetadata {
|
|
26
|
+
productId?: string;
|
|
27
|
+
source?: PurchaseSource;
|
|
28
|
+
type?: PurchaseType;
|
|
29
|
+
}
|
|
30
|
+
|
|
16
31
|
export async function initializeCreditsTransaction(
|
|
17
32
|
db: Firestore,
|
|
18
33
|
creditsRef: DocumentReference,
|
|
19
34
|
config: CreditsConfig,
|
|
20
|
-
purchaseId?: string
|
|
35
|
+
purchaseId?: string,
|
|
36
|
+
metadata?: InitializeCreditsMetadata
|
|
21
37
|
): Promise<InitializationResult> {
|
|
22
38
|
return runTransaction(db, async (transaction: Transaction) => {
|
|
23
39
|
const creditsDoc = await transaction.get(creditsRef);
|
|
@@ -49,12 +65,68 @@ export async function initializeCreditsTransaction(
|
|
|
49
65
|
processedPurchases = [...processedPurchases, purchaseId].slice(-10);
|
|
50
66
|
}
|
|
51
67
|
|
|
68
|
+
// Detect package type and credit limit from productId
|
|
69
|
+
const productId = metadata?.productId;
|
|
70
|
+
const packageType = productId ? detectPackageType(productId) : undefined;
|
|
71
|
+
const allocation = packageType && packageType !== "unknown"
|
|
72
|
+
? getCreditAllocation(packageType, config.packageAllocations)
|
|
73
|
+
: null;
|
|
74
|
+
const creditLimit = allocation || config.creditLimit;
|
|
75
|
+
|
|
76
|
+
// Platform and app version
|
|
77
|
+
const platform = Platform.OS as "ios" | "android";
|
|
78
|
+
const appVersion = Constants.expoConfig?.version;
|
|
79
|
+
|
|
80
|
+
// Determine purchase type
|
|
81
|
+
let purchaseType: PurchaseType = metadata?.type ?? "initial";
|
|
82
|
+
if (creditsDoc.exists()) {
|
|
83
|
+
const existing = creditsDoc.data() as UserCreditsDocumentRead;
|
|
84
|
+
if (existing.packageType && packageType !== "unknown") {
|
|
85
|
+
const oldLimit = existing.creditLimit || 0;
|
|
86
|
+
const newLimit = creditLimit;
|
|
87
|
+
if (newLimit > oldLimit) {
|
|
88
|
+
purchaseType = "upgrade";
|
|
89
|
+
} else if (newLimit < oldLimit) {
|
|
90
|
+
purchaseType = "downgrade";
|
|
91
|
+
} else if (purchaseId?.startsWith("renewal_")) {
|
|
92
|
+
purchaseType = "renewal";
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Create purchase metadata for history (only if productId and source exists and packageType detected)
|
|
98
|
+
const purchaseMetadata: PurchaseMetadata | undefined =
|
|
99
|
+
productId && metadata?.source && packageType && packageType !== "unknown" ? {
|
|
100
|
+
productId,
|
|
101
|
+
packageType,
|
|
102
|
+
creditLimit,
|
|
103
|
+
source: metadata.source,
|
|
104
|
+
type: purchaseType,
|
|
105
|
+
platform,
|
|
106
|
+
appVersion,
|
|
107
|
+
timestamp: now as any,
|
|
108
|
+
} : undefined;
|
|
109
|
+
|
|
110
|
+
// Update purchase history (keep last 10, only if metadata exists)
|
|
111
|
+
const existing = creditsDoc.exists() ? creditsDoc.data() as UserCreditsDocumentRead : null;
|
|
112
|
+
const purchaseHistory = purchaseMetadata
|
|
113
|
+
? [...(existing?.purchaseHistory || []), purchaseMetadata].slice(-10)
|
|
114
|
+
: existing?.purchaseHistory;
|
|
115
|
+
|
|
52
116
|
const creditsData = {
|
|
53
117
|
credits: newCredits,
|
|
118
|
+
packageType: packageType !== "unknown" ? packageType : undefined,
|
|
119
|
+
creditLimit,
|
|
120
|
+
productId: productId || undefined,
|
|
121
|
+
purchaseSource: metadata?.source,
|
|
122
|
+
purchaseType: metadata?.type ? purchaseType : undefined,
|
|
123
|
+
platform: productId ? platform : undefined,
|
|
124
|
+
appVersion: productId ? appVersion : undefined,
|
|
54
125
|
purchasedAt,
|
|
55
126
|
lastUpdatedAt: now,
|
|
56
127
|
lastPurchaseAt: now,
|
|
57
128
|
processedPurchases,
|
|
129
|
+
purchaseHistory,
|
|
58
130
|
};
|
|
59
131
|
|
|
60
132
|
transaction.set(creditsRef, creditsData, { merge: true });
|
|
@@ -39,16 +39,26 @@ export const initializeSubscription = async (config: SubscriptionInitConfig): Pr
|
|
|
39
39
|
|
|
40
40
|
configureCreditsRepository({ ...credits, creditPackageAmounts: creditPackages?.amounts });
|
|
41
41
|
|
|
42
|
-
const onPurchase = async (userId: string, productId: string) => {
|
|
42
|
+
const onPurchase = async (userId: string, productId: string, _customerInfo: unknown, source?: string) => {
|
|
43
43
|
try {
|
|
44
|
-
await getCreditsRepository().initializeCredits(
|
|
44
|
+
await getCreditsRepository().initializeCredits(
|
|
45
|
+
userId,
|
|
46
|
+
`purchase_${productId}_${Date.now()}`,
|
|
47
|
+
productId,
|
|
48
|
+
source as any
|
|
49
|
+
);
|
|
45
50
|
onCreditsUpdated?.(userId);
|
|
46
51
|
} catch { /* Silent */ }
|
|
47
52
|
};
|
|
48
53
|
|
|
49
54
|
const onRenewal = async (userId: string, productId: string, renewalId: string) => {
|
|
50
55
|
try {
|
|
51
|
-
await getCreditsRepository().initializeCredits(
|
|
56
|
+
await getCreditsRepository().initializeCredits(
|
|
57
|
+
userId,
|
|
58
|
+
renewalId,
|
|
59
|
+
productId,
|
|
60
|
+
undefined
|
|
61
|
+
);
|
|
52
62
|
onCreditsUpdated?.(userId);
|
|
53
63
|
} catch { /* Silent */ }
|
|
54
64
|
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pending Purchase Store
|
|
3
|
+
* Manages pending purchase state for auth-required purchases
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createStore } from "@umituz/react-native-design-system";
|
|
7
|
+
import type { PurchasesPackage } from "react-native-purchases";
|
|
8
|
+
import type { PurchaseSource } from "../../domain/entities/Credits";
|
|
9
|
+
|
|
10
|
+
export interface PendingPurchaseData {
|
|
11
|
+
package: PurchasesPackage;
|
|
12
|
+
source: PurchaseSource;
|
|
13
|
+
selectedAt: number;
|
|
14
|
+
metadata?: Record<string, unknown>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface PendingPurchaseState {
|
|
18
|
+
pending: PendingPurchaseData | null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface PendingPurchaseActions {
|
|
22
|
+
setPendingPurchase: (data: PendingPurchaseData) => void;
|
|
23
|
+
getPendingPurchase: () => PendingPurchaseData | null;
|
|
24
|
+
clearPendingPurchase: () => void;
|
|
25
|
+
hasPendingPurchase: () => boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const initialState: PendingPurchaseState = {
|
|
29
|
+
pending: null,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const usePendingPurchaseStore = createStore<
|
|
33
|
+
PendingPurchaseState,
|
|
34
|
+
PendingPurchaseActions
|
|
35
|
+
>({
|
|
36
|
+
name: "pending-purchase-store",
|
|
37
|
+
initialState,
|
|
38
|
+
persist: false,
|
|
39
|
+
actions: (set, get) => ({
|
|
40
|
+
setPendingPurchase: (data: PendingPurchaseData) => {
|
|
41
|
+
set({ pending: data });
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
getPendingPurchase: () => {
|
|
45
|
+
return get().pending;
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
clearPendingPurchase: () => {
|
|
49
|
+
set({ pending: null });
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
hasPendingPurchase: () => {
|
|
53
|
+
return get().pending !== null;
|
|
54
|
+
},
|
|
55
|
+
}),
|
|
56
|
+
});
|
|
@@ -7,6 +7,10 @@
|
|
|
7
7
|
import { useCallback } from "react";
|
|
8
8
|
import type { PurchasesPackage } from "react-native-purchases";
|
|
9
9
|
import { usePremium } from "./usePremium";
|
|
10
|
+
import { usePendingPurchaseStore } from "../../infrastructure/stores/PendingPurchaseStore";
|
|
11
|
+
import type { PurchaseSource } from "../../domain/entities/Credits";
|
|
12
|
+
|
|
13
|
+
declare const __DEV__: boolean;
|
|
10
14
|
|
|
11
15
|
export interface PurchaseAuthProvider {
|
|
12
16
|
isAuthenticated: () => boolean;
|
|
@@ -24,16 +28,23 @@ export const configureAuthProvider = (provider: PurchaseAuthProvider): void => {
|
|
|
24
28
|
globalAuthProvider = provider;
|
|
25
29
|
};
|
|
26
30
|
|
|
31
|
+
export interface UseAuthAwarePurchaseParams {
|
|
32
|
+
source?: PurchaseSource;
|
|
33
|
+
}
|
|
34
|
+
|
|
27
35
|
export interface UseAuthAwarePurchaseResult {
|
|
28
|
-
handlePurchase: (pkg: PurchasesPackage) => Promise<boolean>;
|
|
36
|
+
handlePurchase: (pkg: PurchasesPackage, source?: PurchaseSource) => Promise<boolean>;
|
|
29
37
|
handleRestore: () => Promise<boolean>;
|
|
30
38
|
}
|
|
31
39
|
|
|
32
|
-
export const useAuthAwarePurchase = (
|
|
40
|
+
export const useAuthAwarePurchase = (
|
|
41
|
+
params?: UseAuthAwarePurchaseParams
|
|
42
|
+
): UseAuthAwarePurchaseResult => {
|
|
33
43
|
const { purchasePackage, restorePurchase, closePaywall } = usePremium();
|
|
44
|
+
const { setPendingPurchase } = usePendingPurchaseStore();
|
|
34
45
|
|
|
35
46
|
const handlePurchase = useCallback(
|
|
36
|
-
async (pkg: PurchasesPackage): Promise<boolean> => {
|
|
47
|
+
async (pkg: PurchasesPackage, source?: PurchaseSource): Promise<boolean> => {
|
|
37
48
|
// SECURITY: Block purchase if auth provider not configured
|
|
38
49
|
if (!globalAuthProvider) {
|
|
39
50
|
if (__DEV__) {
|
|
@@ -42,7 +53,6 @@ export const useAuthAwarePurchase = (): UseAuthAwarePurchaseResult => {
|
|
|
42
53
|
"Call configureAuthProvider() at app start. Purchase blocked for security.",
|
|
43
54
|
);
|
|
44
55
|
}
|
|
45
|
-
// Block purchase - never allow without auth provider
|
|
46
56
|
return false;
|
|
47
57
|
}
|
|
48
58
|
|
|
@@ -50,9 +60,17 @@ export const useAuthAwarePurchase = (): UseAuthAwarePurchaseResult => {
|
|
|
50
60
|
if (!globalAuthProvider.isAuthenticated()) {
|
|
51
61
|
if (__DEV__) {
|
|
52
62
|
console.log(
|
|
53
|
-
"[useAuthAwarePurchase] User not authenticated, opening auth modal",
|
|
63
|
+
"[useAuthAwarePurchase] User not authenticated, saving pending purchase and opening auth modal",
|
|
54
64
|
);
|
|
55
65
|
}
|
|
66
|
+
|
|
67
|
+
// Save pending purchase
|
|
68
|
+
setPendingPurchase({
|
|
69
|
+
package: pkg,
|
|
70
|
+
source: source || params?.source || "settings",
|
|
71
|
+
selectedAt: Date.now(),
|
|
72
|
+
});
|
|
73
|
+
|
|
56
74
|
closePaywall();
|
|
57
75
|
globalAuthProvider.showAuthModal();
|
|
58
76
|
return false;
|
|
@@ -60,7 +78,7 @@ export const useAuthAwarePurchase = (): UseAuthAwarePurchaseResult => {
|
|
|
60
78
|
|
|
61
79
|
return purchasePackage(pkg);
|
|
62
80
|
},
|
|
63
|
-
[purchasePackage, closePaywall],
|
|
81
|
+
[purchasePackage, closePaywall, setPendingPurchase, params?.source],
|
|
64
82
|
);
|
|
65
83
|
|
|
66
84
|
const handleRestore = useCallback(async (): Promise<boolean> => {
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pending Purchase Handler Hook
|
|
3
|
+
* Automatically executes pending purchase after successful authentication
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useEffect } from "react";
|
|
7
|
+
import { usePendingPurchaseStore } from "../../infrastructure/stores/PendingPurchaseStore";
|
|
8
|
+
import { usePremium } from "./usePremium";
|
|
9
|
+
|
|
10
|
+
declare const __DEV__: boolean;
|
|
11
|
+
|
|
12
|
+
export interface UsePendingPurchaseHandlerParams {
|
|
13
|
+
userId: string | undefined;
|
|
14
|
+
isAuthenticated: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Hook to handle pending purchases after authentication
|
|
19
|
+
* Call this in app root after auth initialization
|
|
20
|
+
*/
|
|
21
|
+
export const usePendingPurchaseHandler = ({
|
|
22
|
+
userId,
|
|
23
|
+
isAuthenticated,
|
|
24
|
+
}: UsePendingPurchaseHandlerParams): void => {
|
|
25
|
+
const {
|
|
26
|
+
getPendingPurchase,
|
|
27
|
+
clearPendingPurchase,
|
|
28
|
+
hasPendingPurchase,
|
|
29
|
+
} = usePendingPurchaseStore();
|
|
30
|
+
const { purchasePackage } = usePremium();
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (!isAuthenticated || !userId || !hasPendingPurchase()) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const executePendingPurchase = async () => {
|
|
38
|
+
const pending = getPendingPurchase();
|
|
39
|
+
|
|
40
|
+
if (!pending) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (__DEV__) {
|
|
45
|
+
console.log(
|
|
46
|
+
"[usePendingPurchaseHandler] Executing pending purchase:",
|
|
47
|
+
{
|
|
48
|
+
packageId: pending.package.identifier,
|
|
49
|
+
source: pending.source,
|
|
50
|
+
selectedAt: new Date(pending.selectedAt).toISOString(),
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
await purchasePackage(pending.package);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
if (__DEV__) {
|
|
59
|
+
console.error(
|
|
60
|
+
"[usePendingPurchaseHandler] Failed to execute pending purchase:",
|
|
61
|
+
error
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
} finally {
|
|
65
|
+
clearPendingPurchase();
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
void executePendingPurchase();
|
|
70
|
+
}, [
|
|
71
|
+
isAuthenticated,
|
|
72
|
+
userId,
|
|
73
|
+
hasPendingPurchase,
|
|
74
|
+
getPendingPurchase,
|
|
75
|
+
clearPendingPurchase,
|
|
76
|
+
purchasePackage,
|
|
77
|
+
]);
|
|
78
|
+
};
|
|
@@ -66,12 +66,19 @@ export const useSubscriptionSettingsConfig = (
|
|
|
66
66
|
const dynamicCreditLimit = useMemo(() => {
|
|
67
67
|
const config = getCreditsConfig();
|
|
68
68
|
|
|
69
|
+
// 1. ÖNCE FIRESTORE'DAN OKU (Single Source of Truth)
|
|
70
|
+
if (credits?.creditLimit) {
|
|
71
|
+
return credits.creditLimit;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 2. FALLBACK: RevenueCat'ten detect et
|
|
69
75
|
if (premiumEntitlement?.productIdentifier) {
|
|
70
76
|
const packageType = detectPackageType(premiumEntitlement.productIdentifier);
|
|
71
77
|
const allocation = getCreditAllocation(packageType, config.packageAllocations);
|
|
72
78
|
if (allocation !== null) return allocation;
|
|
73
79
|
}
|
|
74
80
|
|
|
81
|
+
// 3. LAST RESORT: Credit miktarına bakarak tahmin et
|
|
75
82
|
if (credits?.credits && config.packageAllocations) {
|
|
76
83
|
const currentCredits = credits.credits;
|
|
77
84
|
const allocations = Object.values(config.packageAllocations).map(a => a.credits);
|
|
@@ -79,8 +86,9 @@ export const useSubscriptionSettingsConfig = (
|
|
|
79
86
|
return closest;
|
|
80
87
|
}
|
|
81
88
|
|
|
89
|
+
// 4. FINAL FALLBACK: Config'den al
|
|
82
90
|
return creditLimit ?? config.creditLimit;
|
|
83
|
-
}, [
|
|
91
|
+
}, [credits?.creditLimit, credits?.credits, premiumEntitlement?.productIdentifier, creditLimit]);
|
|
84
92
|
|
|
85
93
|
// Get expiration date directly from RevenueCat (source of truth)
|
|
86
94
|
const entitlementExpirationDate = premiumEntitlement?.expirationDate ?? null;
|
|
@@ -25,7 +25,8 @@ export interface RevenueCatConfig {
|
|
|
25
25
|
onPurchaseCompleted?: (
|
|
26
26
|
userId: string,
|
|
27
27
|
productId: string,
|
|
28
|
-
customerInfo: CustomerInfo
|
|
28
|
+
customerInfo: CustomerInfo,
|
|
29
|
+
source?: string
|
|
29
30
|
) => Promise<void> | void;
|
|
30
31
|
/** Callback for restore completion */
|
|
31
32
|
onRestoreCompleted?: (
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
syncPremiumStatus,
|
|
19
19
|
notifyPurchaseCompleted,
|
|
20
20
|
} from "../utils/PremiumStatusSyncer";
|
|
21
|
+
import { usePendingPurchaseStore } from "../../../infrastructure/stores/PendingPurchaseStore";
|
|
21
22
|
|
|
22
23
|
export interface PurchaseHandlerDeps {
|
|
23
24
|
config: RevenueCatConfig;
|
|
@@ -76,16 +77,24 @@ export async function handlePurchase(
|
|
|
76
77
|
});
|
|
77
78
|
}
|
|
78
79
|
|
|
80
|
+
// Get purchase source from pending purchase store
|
|
81
|
+
const pendingPurchaseStore = usePendingPurchaseStore.getState();
|
|
82
|
+
const pending = pendingPurchaseStore.getPendingPurchase();
|
|
83
|
+
const source = pending?.source;
|
|
84
|
+
|
|
79
85
|
if (isConsumable) {
|
|
80
86
|
if (__DEV__) {
|
|
81
|
-
console.log('[DEBUG PurchaseHandler] Consumable purchase SUCCESS');
|
|
87
|
+
console.log('[DEBUG PurchaseHandler] Consumable purchase SUCCESS', { source });
|
|
82
88
|
}
|
|
83
89
|
await notifyPurchaseCompleted(
|
|
84
90
|
deps.config,
|
|
85
91
|
userId,
|
|
86
92
|
pkg.product.identifier,
|
|
87
|
-
customerInfo
|
|
93
|
+
customerInfo,
|
|
94
|
+
source
|
|
88
95
|
);
|
|
96
|
+
// Clear pending purchase after successful purchase
|
|
97
|
+
pendingPurchaseStore.clearPendingPurchase();
|
|
89
98
|
return {
|
|
90
99
|
success: true,
|
|
91
100
|
isPremium: false,
|
|
@@ -103,20 +112,24 @@ export async function handlePurchase(
|
|
|
103
112
|
entitlementIdentifier,
|
|
104
113
|
isPremium,
|
|
105
114
|
allEntitlements: customerInfo.entitlements.active,
|
|
115
|
+
source,
|
|
106
116
|
});
|
|
107
117
|
}
|
|
108
118
|
|
|
109
119
|
if (isPremium) {
|
|
110
120
|
if (__DEV__) {
|
|
111
|
-
console.log('[DEBUG PurchaseHandler] Premium purchase SUCCESS');
|
|
121
|
+
console.log('[DEBUG PurchaseHandler] Premium purchase SUCCESS', { source });
|
|
112
122
|
}
|
|
113
123
|
await syncPremiumStatus(deps.config, userId, customerInfo);
|
|
114
124
|
await notifyPurchaseCompleted(
|
|
115
125
|
deps.config,
|
|
116
126
|
userId,
|
|
117
127
|
pkg.product.identifier,
|
|
118
|
-
customerInfo
|
|
128
|
+
customerInfo,
|
|
129
|
+
source
|
|
119
130
|
);
|
|
131
|
+
// Clear pending purchase after successful purchase
|
|
132
|
+
pendingPurchaseStore.clearPendingPurchase();
|
|
120
133
|
return { success: true, isPremium: true, customerInfo };
|
|
121
134
|
}
|
|
122
135
|
|
|
@@ -124,14 +137,17 @@ export async function handlePurchase(
|
|
|
124
137
|
// Treat the purchase as successful for testing purposes
|
|
125
138
|
if (deps.isUsingTestStore()) {
|
|
126
139
|
if (__DEV__) {
|
|
127
|
-
console.log('[DEBUG PurchaseHandler] Test store purchase SUCCESS');
|
|
140
|
+
console.log('[DEBUG PurchaseHandler] Test store purchase SUCCESS', { source });
|
|
128
141
|
}
|
|
129
142
|
await notifyPurchaseCompleted(
|
|
130
143
|
deps.config,
|
|
131
144
|
userId,
|
|
132
145
|
pkg.product.identifier,
|
|
133
|
-
customerInfo
|
|
146
|
+
customerInfo,
|
|
147
|
+
source
|
|
134
148
|
);
|
|
149
|
+
// Clear pending purchase after successful purchase
|
|
150
|
+
pendingPurchaseStore.clearPendingPurchase();
|
|
135
151
|
return { success: true, isPremium: false, customerInfo };
|
|
136
152
|
}
|
|
137
153
|
|
|
@@ -41,14 +41,15 @@ export async function notifyPurchaseCompleted(
|
|
|
41
41
|
config: RevenueCatConfig,
|
|
42
42
|
userId: string,
|
|
43
43
|
productId: string,
|
|
44
|
-
customerInfo: CustomerInfo
|
|
44
|
+
customerInfo: CustomerInfo,
|
|
45
|
+
source?: string
|
|
45
46
|
): Promise<void> {
|
|
46
47
|
if (!config.onPurchaseCompleted) {
|
|
47
48
|
return;
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
try {
|
|
51
|
-
await config.onPurchaseCompleted(userId, productId, customerInfo);
|
|
52
|
+
await config.onPurchaseCompleted(userId, productId, customerInfo, source);
|
|
52
53
|
} catch {
|
|
53
54
|
// Silent error handling
|
|
54
55
|
}
|