@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umituz/react-native-subscription",
3
- "version": "2.17.10",
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 { CreditType, UserCredits, CreditsConfig, CreditsResult, DeductCreditsResult } from "./domain/entities/Credits";
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(userId: string, purchaseId?: string, productId?: string): Promise<CreditsResult> {
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
- const res = await initializeCreditsTransaction(db, this.getRef(db, userId), cfg, purchaseId);
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 { UserCreditsDocumentRead } from "../models/UserCreditsDocument";
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(userId, `purchase_${productId}_${Date.now()}`, productId);
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(userId, renewalId, productId);
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 = (): UseAuthAwarePurchaseResult => {
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
- }, [premiumEntitlement?.productIdentifier, credits?.credits, creditLimit]);
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
  }