@umituz/react-native-subscription 2.37.39 → 2.37.41
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/CreditLimitCalculator.ts +1 -9
- package/src/domains/credits/application/DeductCreditsCommand.ts +16 -7
- package/src/domains/credits/application/RefundCreditsCommand.ts +1 -5
- package/src/domains/credits/application/credit-strategies/TrialCreditStrategy.ts +1 -5
- package/src/domains/credits/application/creditDocumentHelpers.ts +2 -9
- package/src/domains/credits/core/Credits.ts +0 -23
- package/src/domains/credits/core/CreditsMapper.ts +0 -6
- package/src/domains/credits/core/UserCreditsDocument.ts +0 -12
- package/src/domains/credits/infrastructure/CreditsRepositoryManager.ts +0 -21
- package/src/domains/credits/infrastructure/operations/CreditsWriter.ts +2 -1
- package/src/domains/credits/presentation/deduct-credit/useDeductCredit.ts +2 -2
- package/src/domains/credits/presentation/useCredits.ts +10 -9
- package/src/domains/paywall/components/PaywallContainer.types.ts +0 -28
- package/src/domains/paywall/components/PaywallModal.styles.ts +0 -4
- package/src/domains/paywall/entities/types.ts +0 -5
- package/src/domains/paywall/hooks/usePaywallActions.ts +1 -15
- package/src/domains/revenuecat/core/errors/RevenueCatError.ts +0 -6
- package/src/domains/revenuecat/core/errors/RevenueCatErrorHandler.ts +0 -24
- package/src/domains/revenuecat/core/errors/RevenueCatErrorMessages.ts +0 -18
- package/src/domains/revenuecat/core/errors/index.ts +0 -4
- package/src/domains/revenuecat/core/types/RevenueCatConfig.ts +3 -7
- package/src/domains/revenuecat/core/types/RevenueCatData.ts +4 -9
- package/src/domains/revenuecat/core/types/RevenueCatTypes.ts +5 -65
- package/src/domains/revenuecat/core/types/index.ts +0 -4
- package/src/domains/revenuecat/infrastructure/services/UserSwitchMutex.ts +1 -24
- package/src/domains/subscription/application/SubscriptionAuthListener.ts +5 -21
- package/src/domains/subscription/application/SubscriptionInitializerTypes.ts +1 -5
- package/src/domains/subscription/application/SubscriptionSyncProcessor.ts +6 -2
- package/src/domains/subscription/application/SubscriptionSyncService.ts +11 -2
- package/src/domains/subscription/application/SubscriptionSyncUtils.ts +1 -1
- package/src/domains/subscription/application/initializer/BackgroundInitializer.ts +15 -2
- package/src/domains/subscription/application/initializer/ServiceConfigurator.ts +9 -2
- package/src/domains/subscription/constants/thresholds.ts +0 -9
- package/src/domains/subscription/core/SubscriptionConstants.ts +0 -4
- package/src/domains/subscription/core/SubscriptionStatus.ts +11 -21
- package/src/domains/subscription/core/SubscriptionStatusHandlers.ts +4 -7
- package/src/domains/subscription/infrastructure/handlers/PurchaseStatusResolver.ts +1 -1
- package/src/domains/subscription/infrastructure/hooks/subscriptionQueryKeys.ts +0 -13
- package/src/domains/subscription/infrastructure/hooks/usePurchasePackage.ts +0 -18
- package/src/domains/subscription/infrastructure/hooks/useRestorePurchase.ts +3 -17
- package/src/domains/subscription/infrastructure/hooks/useRevenueCatTrialEligibility.ts +0 -17
- package/src/domains/subscription/infrastructure/hooks/useSubscriptionPackages.ts +0 -19
- package/src/domains/subscription/infrastructure/hooks/useSubscriptionQueries.ts +0 -6
- package/src/domains/subscription/infrastructure/managers/subscriptionManagerUtils.ts +0 -17
- package/src/domains/subscription/infrastructure/state/initializationState.ts +0 -25
- package/src/domains/subscription/infrastructure/utils/InitializationCache.ts +0 -21
- package/src/domains/subscription/infrastructure/utils/PremiumStatusSyncer.ts +2 -7
- package/src/domains/subscription/infrastructure/utils/authPurchaseState.ts +19 -6
- package/src/domains/subscription/infrastructure/utils/renewal/PackageTierComparator.ts +1 -0
- package/src/domains/subscription/infrastructure/utils/trialEligibilityUtils.ts +0 -18
- package/src/domains/subscription/presentation/components/details/PremiumDetailsCard.styles.ts +0 -5
- package/src/domains/subscription/presentation/components/details/PremiumDetailsCardTypes.ts +0 -5
- package/src/domains/subscription/presentation/components/feedback/paywallFeedbackStyles.ts +0 -5
- package/src/domains/subscription/presentation/stores/index.ts +0 -4
- package/src/domains/subscription/presentation/stores/purchaseLoadingStore.ts +0 -13
- package/src/domains/subscription/presentation/useAuthAwarePurchase.ts +35 -21
- package/src/domains/subscription/presentation/usePaywallVisibility.ts +0 -9
- package/src/domains/subscription/presentation/useSubscriptionStatus.ts +8 -11
- package/src/domains/subscription/utils/authGuards.ts +3 -0
- package/src/domains/trial/application/TrialService.ts +0 -9
- package/src/domains/trial/core/TrialTypes.ts +0 -8
- package/src/domains/wallet/domain/mappers/TransactionMapper.ts +0 -5
- package/src/domains/wallet/domain/types/transaction.types.ts +0 -7
- package/src/domains/wallet/index.ts +0 -7
- package/src/domains/wallet/infrastructure/config/walletConfig.ts +0 -11
- package/src/domains/wallet/presentation/hooks/useTransactionHistory.ts +6 -3
- package/src/domains/wallet/presentation/hooks/useWallet.ts +0 -7
- package/src/domains/wallet/utils/transactionIconMap.ts +0 -10
- package/src/global.d.ts +0 -6
- package/src/index.ts +1 -4
- package/src/init/createSubscriptionInitModule.ts +12 -2
- package/src/init/index.ts +1 -5
- package/src/presentation/hooks/feedback/useFeedbackSubmit.ts +0 -11
- package/src/shared/application/FeedbackService.ts +3 -21
- package/src/shared/application/ports/ISubscriptionRepository.ts +0 -4
- package/src/shared/infrastructure/SubscriptionEventBus.ts +0 -13
- package/src/shared/infrastructure/firestore/collectionUtils.ts +1 -17
- package/src/shared/infrastructure/firestore/index.ts +0 -4
- package/src/shared/infrastructure/firestore/resultUtils.ts +0 -12
- package/src/shared/infrastructure/react-query/hooks/usePreviousUserCleanup.ts +0 -17
- package/src/shared/infrastructure/react-query/queryConfig.ts +0 -15
- package/src/shared/utils/BaseError.ts +0 -5
- package/src/shared/utils/Result.ts +0 -20
- package/src/shared/utils/dateConverter.ts +6 -46
- package/src/utils/appUtils.ts +0 -16
- package/src/utils/creditMapper.ts +0 -7
- package/src/utils/dateUtils.compare.ts +0 -24
- package/src/utils/dateUtils.core.ts +0 -39
- package/src/utils/dateUtils.format.ts +0 -41
- package/src/utils/dateUtils.math.ts +0 -41
- package/src/utils/dateUtils.ts +0 -5
- package/src/utils/packagePeriodUtils.ts +0 -20
- package/src/utils/packageTypeDetector.ts +1 -21
- package/src/utils/premiumStatusUtils.ts +1 -14
- package/src/utils/priceUtils.ts +0 -35
- package/src/utils/tierUtils.ts +1 -8
- package/src/utils/types.ts +1 -25
- package/src/utils/validation.ts +1 -7
- package/src/domains/README.md +0 -52
- package/src/domains/config/domain/README.md +0 -37
- package/src/domains/config/domain/entities/README.md +0 -41
- package/src/domains/paywall/README.md +0 -101
- package/src/domains/paywall/entities/README.md +0 -40
- package/src/domains/paywall/hooks/README.md +0 -41
- package/src/domains/subscription/application/syncConstants.ts +0 -1
- package/src/domains/subscription/infrastructure/README.md +0 -41
- package/src/domains/subscription/infrastructure/config/README.md +0 -49
- package/src/domains/subscription/infrastructure/handlers/README.md +0 -41
- package/src/domains/subscription/infrastructure/hooks/README.md +0 -50
- package/src/domains/subscription/infrastructure/managers/README.md +0 -41
- package/src/domains/subscription/infrastructure/services/README.md +0 -42
- package/src/domains/subscription/infrastructure/utils/README.md +0 -41
- package/src/domains/subscription/presentation/components/README.md +0 -155
- package/src/domains/subscription/presentation/components/details/CreditRow.md +0 -92
- package/src/domains/subscription/presentation/components/details/DetailRow.md +0 -91
- package/src/domains/subscription/presentation/components/details/PremiumDetailsCard.md +0 -93
- package/src/domains/subscription/presentation/components/details/PremiumStatusBadge.md +0 -91
- package/src/domains/subscription/presentation/components/details/README.md +0 -99
- package/src/domains/subscription/presentation/components/feedback/PaywallFeedbackModal.md +0 -90
- package/src/domains/subscription/presentation/components/feedback/README.md +0 -99
- package/src/domains/subscription/presentation/components/paywall/PaywallModal.md +0 -94
- package/src/domains/subscription/presentation/components/paywall/README.md +0 -54
- package/src/domains/subscription/presentation/components/sections/README.md +0 -99
- package/src/domains/subscription/presentation/components/sections/SubscriptionSection.md +0 -94
- package/src/domains/subscription/presentation/utils/README.md +0 -31
- package/src/domains/wallet/README.md +0 -51
- package/src/domains/wallet/domain/README.md +0 -41
- package/src/domains/wallet/infrastructure/README.md +0 -41
- package/src/domains/wallet/presentation/components/README.md +0 -41
- package/src/domains/wallet/presentation/hooks/README.md +0 -41
- package/src/shared/application/ports/README.md +0 -48
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.37.
|
|
3
|
+
"version": "2.37.41",
|
|
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",
|
|
@@ -1,20 +1,14 @@
|
|
|
1
1
|
import type { CreditsConfig } from "../core/Credits";
|
|
2
2
|
import { detectPackageType } from "../../../utils/packageTypeDetector";
|
|
3
3
|
import { getCreditAllocation } from "../../../utils/creditMapper";
|
|
4
|
-
import { NO_SUBSCRIPTION_PRODUCT_ID } from "../../subscription/application/syncConstants";
|
|
5
4
|
|
|
6
5
|
export function calculateCreditLimit(productId: string | undefined, config: CreditsConfig): number {
|
|
7
6
|
if (!productId) {
|
|
8
7
|
throw new Error("[CreditLimitCalculator] Cannot calculate credit limit without productId");
|
|
9
8
|
}
|
|
10
9
|
|
|
11
|
-
// Free tier users (no subscription) get 0 credits - strict paywall
|
|
12
|
-
if (productId === NO_SUBSCRIPTION_PRODUCT_ID) {
|
|
13
|
-
return 0;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
10
|
const explicitAmount = config.creditPackageAmounts?.[productId];
|
|
17
|
-
if (explicitAmount) return explicitAmount;
|
|
11
|
+
if (explicitAmount !== undefined && explicitAmount !== null) return explicitAmount;
|
|
18
12
|
|
|
19
13
|
const packageType = detectPackageType(productId);
|
|
20
14
|
const dynamicLimit = getCreditAllocation(packageType, config.packageAllocations);
|
|
@@ -25,5 +19,3 @@ export function calculateCreditLimit(productId: string | undefined, config: Cred
|
|
|
25
19
|
|
|
26
20
|
return dynamicLimit;
|
|
27
21
|
}
|
|
28
|
-
|
|
29
|
-
|
|
@@ -3,10 +3,6 @@ import type { DeductCreditsResult } from "../core/Credits";
|
|
|
3
3
|
import { CREDIT_ERROR_CODES } from "../core/CreditsConstants";
|
|
4
4
|
import { subscriptionEventBus, SUBSCRIPTION_EVENTS } from "../../../shared/infrastructure/SubscriptionEventBus";
|
|
5
5
|
|
|
6
|
-
/**
|
|
7
|
-
* Deducts credits from a user's balance.
|
|
8
|
-
* Encapsulates the domain rules and transaction logic for credit usage.
|
|
9
|
-
*/
|
|
10
6
|
export async function deductCreditsOperation(
|
|
11
7
|
_db: Firestore,
|
|
12
8
|
creditsRef: DocumentReference,
|
|
@@ -24,6 +20,18 @@ export async function deductCreditsOperation(
|
|
|
24
20
|
};
|
|
25
21
|
}
|
|
26
22
|
|
|
23
|
+
const MAX_SINGLE_DEDUCTION = 10000;
|
|
24
|
+
if (cost <= 0 || !Number.isFinite(cost) || cost > MAX_SINGLE_DEDUCTION) {
|
|
25
|
+
return {
|
|
26
|
+
success: false,
|
|
27
|
+
remainingCredits: null,
|
|
28
|
+
error: {
|
|
29
|
+
message: `Cost must be a positive finite number not exceeding ${MAX_SINGLE_DEDUCTION}`,
|
|
30
|
+
code: 'INVALID_AMOUNT'
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
27
35
|
try {
|
|
28
36
|
const remaining = await runTransaction(async (tx: Transaction) => {
|
|
29
37
|
const docSnap = await tx.get(creditsRef);
|
|
@@ -32,7 +40,8 @@ export async function deductCreditsOperation(
|
|
|
32
40
|
throw new Error(CREDIT_ERROR_CODES.NO_CREDITS);
|
|
33
41
|
}
|
|
34
42
|
|
|
35
|
-
const
|
|
43
|
+
const rawCredits = docSnap.data().credits;
|
|
44
|
+
const current = typeof rawCredits === "number" && Number.isFinite(rawCredits) ? rawCredits : 0;
|
|
36
45
|
if (current < cost) {
|
|
37
46
|
throw new Error(CREDIT_ERROR_CODES.CREDITS_EXHAUSTED);
|
|
38
47
|
}
|
|
@@ -55,8 +64,8 @@ export async function deductCreditsOperation(
|
|
|
55
64
|
};
|
|
56
65
|
} catch (e: unknown) {
|
|
57
66
|
const message = e instanceof Error ? e.message : String(e);
|
|
58
|
-
const code = (message === CREDIT_ERROR_CODES.NO_CREDITS || message === CREDIT_ERROR_CODES.CREDITS_EXHAUSTED)
|
|
59
|
-
? message
|
|
67
|
+
const code = (message === CREDIT_ERROR_CODES.NO_CREDITS || message === CREDIT_ERROR_CODES.CREDITS_EXHAUSTED)
|
|
68
|
+
? message
|
|
60
69
|
: CREDIT_ERROR_CODES.DEDUCT_ERR;
|
|
61
70
|
|
|
62
71
|
return {
|
|
@@ -3,10 +3,6 @@ import type { DeductCreditsResult } from "../core/Credits";
|
|
|
3
3
|
import { CREDIT_ERROR_CODES } from "../core/CreditsConstants";
|
|
4
4
|
import { subscriptionEventBus, SUBSCRIPTION_EVENTS } from "../../../shared/infrastructure/SubscriptionEventBus";
|
|
5
5
|
|
|
6
|
-
/**
|
|
7
|
-
* Refunds credits to a user's balance.
|
|
8
|
-
* Used for optimistic billing rollbacks when generation fails due to transient errors.
|
|
9
|
-
*/
|
|
10
6
|
export async function refundCreditsOperation(
|
|
11
7
|
_db: Firestore,
|
|
12
8
|
creditsRef: DocumentReference,
|
|
@@ -24,7 +20,7 @@ export async function refundCreditsOperation(
|
|
|
24
20
|
};
|
|
25
21
|
}
|
|
26
22
|
|
|
27
|
-
if (amount <= 0) {
|
|
23
|
+
if (amount <= 0 || !Number.isFinite(amount)) {
|
|
28
24
|
return {
|
|
29
25
|
success: false,
|
|
30
26
|
remainingCredits: null,
|
|
@@ -2,13 +2,9 @@ import { ICreditStrategy, type CreditAllocationParams } from "./ICreditStrategy"
|
|
|
2
2
|
import { SUBSCRIPTION_STATUS } from "../../../subscription/core/SubscriptionConstants";
|
|
3
3
|
import { TRIAL_CONFIG } from "../../../trial/core/TrialTypes";
|
|
4
4
|
|
|
5
|
-
/**
|
|
6
|
-
* Strategy for Trial and Trial Canceled users.
|
|
7
|
-
* Allocates credits based on trial configuration.
|
|
8
|
-
*/
|
|
9
5
|
export class TrialCreditStrategy implements ICreditStrategy {
|
|
10
6
|
canHandle(params: CreditAllocationParams): boolean {
|
|
11
|
-
return params.status === SUBSCRIPTION_STATUS.TRIAL ||
|
|
7
|
+
return params.status === SUBSCRIPTION_STATUS.TRIAL ||
|
|
12
8
|
params.status === SUBSCRIPTION_STATUS.TRIAL_CANCELED;
|
|
13
9
|
}
|
|
14
10
|
|
|
@@ -1,14 +1,7 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Credit Document Helpers
|
|
3
|
-
* Utilities for getting and creating credit documents
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
1
|
import type { UserCreditsDocumentRead } from "../core/UserCreditsDocument";
|
|
7
2
|
import { serverTimestamp, type DocumentSnapshot } from "@umituz/react-native-firebase";
|
|
8
3
|
import { SUBSCRIPTION_STATUS, type Platform } from "../../subscription/core/SubscriptionConstants";
|
|
9
|
-
|
|
10
|
-
* Get existing credit document or create default
|
|
11
|
-
*/
|
|
4
|
+
|
|
12
5
|
export function getCreditDocumentOrDefault(
|
|
13
6
|
creditsDoc: DocumentSnapshot,
|
|
14
7
|
platform: Platform
|
|
@@ -17,7 +10,7 @@ export function getCreditDocumentOrDefault(
|
|
|
17
10
|
return creditsDoc.data() as UserCreditsDocumentRead;
|
|
18
11
|
}
|
|
19
12
|
|
|
20
|
-
const now = serverTimestamp() as any;
|
|
13
|
+
const now = serverTimestamp() as any;
|
|
21
14
|
|
|
22
15
|
const defaultDocument: UserCreditsDocumentRead = {
|
|
23
16
|
credits: 0,
|
|
@@ -1,12 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Credits Domain Entities
|
|
3
|
-
*
|
|
4
|
-
* Generic credit system types for subscription-based apps.
|
|
5
|
-
* Designed to be used across hundreds of apps with configurable limits.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
1
|
import type { SubscriptionPackageType } from "../../../utils/packageTypeDetector";
|
|
9
|
-
// Types imported from SubscriptionConstants are used directly in UserCredits interface
|
|
10
2
|
import type {
|
|
11
3
|
SubscriptionStatusType,
|
|
12
4
|
PackageType,
|
|
@@ -17,37 +9,25 @@ import type {
|
|
|
17
9
|
|
|
18
10
|
export type CreditType = "text" | "image";
|
|
19
11
|
|
|
20
|
-
/** Single Source of Truth for user subscription + credits data */
|
|
21
12
|
export interface UserCredits {
|
|
22
|
-
// Core subscription
|
|
23
13
|
isPremium: boolean;
|
|
24
14
|
status: SubscriptionStatusType;
|
|
25
|
-
|
|
26
|
-
// Dates
|
|
27
15
|
purchasedAt: Date | null;
|
|
28
16
|
expirationDate: Date | null;
|
|
29
17
|
lastUpdatedAt: Date | null;
|
|
30
18
|
lastPurchaseAt: Date | null;
|
|
31
|
-
|
|
32
|
-
// RevenueCat subscription details
|
|
33
19
|
willRenew: boolean | null;
|
|
34
20
|
productId: string | null;
|
|
35
21
|
packageType: PackageType | null;
|
|
36
22
|
originalTransactionId: string | null;
|
|
37
|
-
|
|
38
|
-
// Trial fields - periodType comes from RevenueCat SDK
|
|
39
23
|
periodType: string | null;
|
|
40
24
|
isTrialing: boolean | null;
|
|
41
25
|
trialStartDate: Date | null;
|
|
42
26
|
trialEndDate: Date | null;
|
|
43
27
|
trialCredits: number | null;
|
|
44
28
|
convertedFromTrial: boolean | null;
|
|
45
|
-
|
|
46
|
-
// Credits
|
|
47
29
|
credits: number;
|
|
48
30
|
creditLimit: number;
|
|
49
|
-
|
|
50
|
-
// Metadata
|
|
51
31
|
purchaseSource: PurchaseSource | null;
|
|
52
32
|
purchaseType: PurchaseType | null;
|
|
53
33
|
platform: Platform;
|
|
@@ -66,11 +46,8 @@ export type PackageAllocationMap = Partial<Record<
|
|
|
66
46
|
export interface CreditsConfig {
|
|
67
47
|
collectionName: string;
|
|
68
48
|
creditLimit: number;
|
|
69
|
-
/** When true, stores credits at users/{userId}/credits instead of {collectionName}/{userId} */
|
|
70
49
|
useUserSubcollection: boolean;
|
|
71
|
-
/** Credit amounts per product ID for consumable credit packages */
|
|
72
50
|
creditPackageAmounts: Record<string, number>;
|
|
73
|
-
/** Credit allocations for different subscription types (weekly, monthly, yearly) */
|
|
74
51
|
packageAllocations: PackageAllocationMap;
|
|
75
52
|
}
|
|
76
53
|
|
|
@@ -3,9 +3,6 @@ import { resolveSubscriptionStatus, type SubscriptionStatusType } from "../../su
|
|
|
3
3
|
import type { UserCreditsDocumentRead } from "./UserCreditsDocument";
|
|
4
4
|
import { toSafeDate } from "../../../utils/dateUtils";
|
|
5
5
|
|
|
6
|
-
/**
|
|
7
|
-
* Validate subscription status against expirationDate and periodType
|
|
8
|
-
*/
|
|
9
6
|
function validateSubscription(
|
|
10
7
|
doc: UserCreditsDocumentRead,
|
|
11
8
|
expirationDate: Date | null,
|
|
@@ -28,9 +25,6 @@ function validateSubscription(
|
|
|
28
25
|
};
|
|
29
26
|
}
|
|
30
27
|
|
|
31
|
-
/**
|
|
32
|
-
* Maps Firestore document to domain entity with expiration validation
|
|
33
|
-
*/
|
|
34
28
|
export function mapCreditsDocumentToEntity(doc: UserCreditsDocumentRead): UserCredits {
|
|
35
29
|
const expirationDate = toSafeDate(doc.expirationDate);
|
|
36
30
|
const periodType = doc.periodType;
|
|
@@ -30,41 +30,29 @@ export interface PurchaseMetadata {
|
|
|
30
30
|
timestamp: FirestoreTimestamp;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
/** Single Source of Truth for user subscription data */
|
|
34
33
|
export interface UserCreditsDocumentRead {
|
|
35
|
-
// Core subscription status
|
|
36
34
|
isPremium: boolean;
|
|
37
35
|
status: SubscriptionStatusType;
|
|
38
|
-
|
|
39
|
-
// Dates (all from RevenueCat)
|
|
40
36
|
purchasedAt: FirestoreTimestamp;
|
|
41
37
|
expirationDate: FirestoreTimestamp | null;
|
|
42
38
|
lastUpdatedAt: FirestoreTimestamp;
|
|
43
39
|
lastPurchaseAt: FirestoreTimestamp | null;
|
|
44
40
|
canceledAt: FirestoreTimestamp | null;
|
|
45
41
|
billingIssueDetectedAt: FirestoreTimestamp | null;
|
|
46
|
-
|
|
47
|
-
// RevenueCat subscription details
|
|
48
42
|
willRenew: boolean | null;
|
|
49
43
|
productId: string | null;
|
|
50
44
|
packageType: PackageType | null;
|
|
51
45
|
originalTransactionId: string | null;
|
|
52
46
|
store: Store | null;
|
|
53
47
|
ownershipType: OwnershipType | null;
|
|
54
|
-
|
|
55
|
-
// Trial fields
|
|
56
48
|
periodType: string | null;
|
|
57
49
|
isTrialing: boolean | null;
|
|
58
50
|
trialStartDate: FirestoreTimestamp | null;
|
|
59
51
|
trialEndDate: FirestoreTimestamp | null;
|
|
60
52
|
trialCredits: number | null;
|
|
61
53
|
convertedFromTrial: boolean | null;
|
|
62
|
-
|
|
63
|
-
// Credits
|
|
64
54
|
credits: number;
|
|
65
55
|
creditLimit: number;
|
|
66
|
-
|
|
67
|
-
// Metadata
|
|
68
56
|
purchaseSource: PurchaseSource | null;
|
|
69
57
|
purchaseType: PurchaseType | null;
|
|
70
58
|
platform: Platform;
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Credits Repository Manager
|
|
3
|
-
* Module-level singleton for credits repository configuration
|
|
4
|
-
* Provides a clean, testable approach for repository access
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
1
|
import type { CreditsConfig } from "../core/Credits";
|
|
8
2
|
import type { CreditsRepository } from "./CreditsRepository";
|
|
9
3
|
import { createCreditsRepository } from "./CreditsRepository";
|
|
@@ -11,26 +5,15 @@ import { createCreditsRepository } from "./CreditsRepository";
|
|
|
11
5
|
let globalRepository: CreditsRepository | null = null;
|
|
12
6
|
let globalConfig: CreditsConfig | null = null;
|
|
13
7
|
|
|
14
|
-
/**
|
|
15
|
-
* Configure credits repository for the application
|
|
16
|
-
* Must be called once during app initialization
|
|
17
|
-
*/
|
|
18
8
|
export function configureCreditsRepository(config: CreditsConfig): void {
|
|
19
9
|
globalConfig = config;
|
|
20
10
|
globalRepository = createCreditsRepository(globalConfig);
|
|
21
11
|
}
|
|
22
12
|
|
|
23
|
-
/**
|
|
24
|
-
* Check if credits repository is configured
|
|
25
|
-
*/
|
|
26
13
|
export function isCreditsRepositoryConfigured(): boolean {
|
|
27
14
|
return globalRepository !== null;
|
|
28
15
|
}
|
|
29
16
|
|
|
30
|
-
/**
|
|
31
|
-
* Get the configured credits repository
|
|
32
|
-
* Throws if repository not configured
|
|
33
|
-
*/
|
|
34
17
|
export function getCreditsRepository(): CreditsRepository {
|
|
35
18
|
if (!globalRepository) {
|
|
36
19
|
throw new Error(
|
|
@@ -40,9 +23,6 @@ export function getCreditsRepository(): CreditsRepository {
|
|
|
40
23
|
return globalRepository;
|
|
41
24
|
}
|
|
42
25
|
|
|
43
|
-
/**
|
|
44
|
-
* Get the current credits configuration
|
|
45
|
-
*/
|
|
46
26
|
export function getCreditsConfig(): CreditsConfig {
|
|
47
27
|
if (!globalConfig) {
|
|
48
28
|
throw new Error(
|
|
@@ -51,4 +31,3 @@ export function getCreditsConfig(): CreditsConfig {
|
|
|
51
31
|
}
|
|
52
32
|
return globalConfig;
|
|
53
33
|
}
|
|
54
|
-
|
|
@@ -4,6 +4,7 @@ import { serverTimestamp } from "@umituz/react-native-firebase";
|
|
|
4
4
|
import { SUBSCRIPTION_STATUS } from "../../../subscription/core/SubscriptionConstants";
|
|
5
5
|
import { resolveSubscriptionStatus } from "../../../subscription/core/SubscriptionStatus";
|
|
6
6
|
import { toTimestamp } from "../../../../shared/utils/dateConverter";
|
|
7
|
+
import { isPast } from "../../../../utils/dateUtils";
|
|
7
8
|
|
|
8
9
|
export async function syncExpiredStatus(ref: DocumentReference): Promise<void> {
|
|
9
10
|
const doc = await getDoc(ref);
|
|
@@ -36,7 +37,7 @@ export async function syncPremiumMetadata(
|
|
|
36
37
|
const doc = await getDoc(ref);
|
|
37
38
|
if (!doc.exists()) return;
|
|
38
39
|
|
|
39
|
-
const isExpired = false;
|
|
40
|
+
const isExpired = metadata.expirationDate ? isPast(metadata.expirationDate) : false;
|
|
40
41
|
const status = resolveSubscriptionStatus({
|
|
41
42
|
isPremium: metadata.isPremium,
|
|
42
43
|
willRenew: metadata.willRenew,
|
|
@@ -35,9 +35,9 @@ export const useDeductCredit = ({
|
|
|
35
35
|
console.error('[useDeductCredit] Unexpected error during credit deduction', {
|
|
36
36
|
cost,
|
|
37
37
|
userId,
|
|
38
|
-
error
|
|
38
|
+
error: error instanceof Error ? error.message : String(error)
|
|
39
39
|
});
|
|
40
|
-
|
|
40
|
+
return false;
|
|
41
41
|
}
|
|
42
42
|
}, [onCreditsExhausted, userId]);
|
|
43
43
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useQuery, useQueryClient } from "@umituz/react-native-design-system";
|
|
2
2
|
import { useCallback, useMemo, useEffect } from "react";
|
|
3
|
-
import { useAuthStore, selectUserId } from "@umituz/react-native-auth";
|
|
3
|
+
import { useAuthStore, selectUserId, selectIsAnonymous } from "@umituz/react-native-auth";
|
|
4
4
|
import { subscriptionEventBus, SUBSCRIPTION_EVENTS } from "../../../shared/infrastructure/SubscriptionEventBus";
|
|
5
5
|
import { NO_CACHE_QUERY_CONFIG } from "../../../shared/infrastructure/react-query/queryConfig";
|
|
6
6
|
import { usePreviousUserCleanup } from "../../../shared/infrastructure/react-query/hooks/usePreviousUserCleanup";
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
isCreditsRepositoryConfigured,
|
|
11
11
|
} from "../infrastructure/CreditsRepositoryManager";
|
|
12
12
|
import { calculateSafePercentage, canAffordAmount } from "../utils/creditValidation";
|
|
13
|
-
import {
|
|
13
|
+
import { isRegisteredUser } from "../../subscription/utils/authGuards";
|
|
14
14
|
import { creditsQueryKeys } from "./creditsQueryKeys";
|
|
15
15
|
import type { UseCreditsResult, CreditsLoadStatus } from "./useCredits.types";
|
|
16
16
|
|
|
@@ -26,15 +26,17 @@ const deriveLoadStatus = (
|
|
|
26
26
|
|
|
27
27
|
export const useCredits = (): UseCreditsResult => {
|
|
28
28
|
const userId = useAuthStore(selectUserId);
|
|
29
|
+
const isAnonymous = useAuthStore(selectIsAnonymous);
|
|
29
30
|
const isConfigured = isCreditsRepositoryConfigured();
|
|
30
31
|
|
|
31
32
|
const config = isConfigured ? getCreditsConfig() : null;
|
|
32
|
-
const
|
|
33
|
+
const isUserRegistered = isRegisteredUser(userId, isAnonymous);
|
|
34
|
+
const queryEnabled = isUserRegistered && isConfigured;
|
|
33
35
|
|
|
34
36
|
const { data, status, error, refetch } = useQuery({
|
|
35
37
|
queryKey: creditsQueryKeys.user(userId),
|
|
36
38
|
queryFn: async () => {
|
|
37
|
-
if (!
|
|
39
|
+
if (!isUserRegistered || !isConfigured) return null;
|
|
38
40
|
|
|
39
41
|
const repository = getCreditsRepository();
|
|
40
42
|
const result = await repository.getCredits(userId);
|
|
@@ -51,11 +53,10 @@ export const useCredits = (): UseCreditsResult => {
|
|
|
51
53
|
|
|
52
54
|
const queryClient = useQueryClient();
|
|
53
55
|
|
|
54
|
-
// Clean up previous user's cache on logout/user switch
|
|
55
56
|
usePreviousUserCleanup(userId, queryClient, creditsQueryKeys.user);
|
|
56
57
|
|
|
57
58
|
useEffect(() => {
|
|
58
|
-
if (!
|
|
59
|
+
if (!isUserRegistered) return undefined;
|
|
59
60
|
|
|
60
61
|
const unsubscribe = subscriptionEventBus.on(SUBSCRIPTION_EVENTS.CREDITS_UPDATED, (updatedUserId) => {
|
|
61
62
|
if (updatedUserId === userId) {
|
|
@@ -64,13 +65,13 @@ export const useCredits = (): UseCreditsResult => {
|
|
|
64
65
|
});
|
|
65
66
|
|
|
66
67
|
return unsubscribe;
|
|
67
|
-
}, [userId, queryClient]);
|
|
68
|
+
}, [userId, isUserRegistered, queryClient]);
|
|
68
69
|
|
|
69
70
|
const credits = data ?? null;
|
|
70
71
|
|
|
71
72
|
const derivedValues = useMemo(() => {
|
|
72
73
|
const has = (credits?.credits ?? 0) > 0;
|
|
73
|
-
const limit = config?.creditLimit ?? 0;
|
|
74
|
+
const limit = credits?.creditLimit ?? config?.creditLimit ?? 0;
|
|
74
75
|
const percent = calculateSafePercentage(credits?.credits, limit);
|
|
75
76
|
return { hasCredits: has, creditsPercent: percent };
|
|
76
77
|
}, [credits, config?.creditLimit]);
|
|
@@ -95,4 +96,4 @@ export const useCredits = (): UseCreditsResult => {
|
|
|
95
96
|
refetch,
|
|
96
97
|
canAfford,
|
|
97
98
|
};
|
|
98
|
-
};
|
|
99
|
+
};
|
|
@@ -1,57 +1,29 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* PaywallContainer Types
|
|
3
|
-
* Props for subscription paywall
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
1
|
import type { ImageSourcePropType } from "react-native";
|
|
7
2
|
import type { PaywallTranslations, PaywallLegalUrls, SubscriptionFeature } from "../entities/types";
|
|
8
3
|
import type { PurchaseSource } from "../../subscription/core/SubscriptionConstants";
|
|
9
4
|
import type { PackageAllocationMap } from "../../credits/core/Credits";
|
|
10
5
|
|
|
11
|
-
/**
|
|
12
|
-
* Trial display configuration
|
|
13
|
-
* Controls how free trial info is displayed (Apple-compliant)
|
|
14
|
-
*/
|
|
15
6
|
export interface TrialConfig {
|
|
16
|
-
/** Enable trial display (default: false) */
|
|
17
7
|
readonly enabled: boolean;
|
|
18
|
-
/** Product IDs that have trial offers (if empty, checks all via RevenueCat) */
|
|
19
8
|
readonly eligibleProductIds?: readonly string[];
|
|
20
|
-
/** Trial duration in days (default: 7) */
|
|
21
9
|
readonly durationDays?: number;
|
|
22
|
-
/** Text to show for trial (e.g., "7 days free, then billed") */
|
|
23
10
|
readonly trialText?: string;
|
|
24
11
|
}
|
|
25
12
|
|
|
26
13
|
export interface PaywallContainerProps {
|
|
27
|
-
/** Paywall translations - no defaults, must be provided */
|
|
28
14
|
readonly translations: PaywallTranslations;
|
|
29
|
-
/** Legal URLs for privacy and terms */
|
|
30
15
|
readonly legalUrls?: PaywallLegalUrls;
|
|
31
|
-
/** Feature list to display */
|
|
32
16
|
readonly features?: readonly SubscriptionFeature[];
|
|
33
|
-
/** Hero image for paywall header */
|
|
34
17
|
readonly heroImage?: ImageSourcePropType;
|
|
35
|
-
/** Best value package identifier for badge */
|
|
36
18
|
readonly bestValueIdentifier?: string;
|
|
37
|
-
/** Credit amounts per product identifier (takes precedence over packageAllocations) */
|
|
38
19
|
readonly creditAmounts?: Record<string, number>;
|
|
39
|
-
/** Credits label text (e.g., "credits") */
|
|
40
20
|
readonly creditsLabel?: string;
|
|
41
|
-
/** Package allocations for auto-computing creditAmounts (used when creditAmounts not provided) */
|
|
42
21
|
readonly packageAllocations?: PackageAllocationMap;
|
|
43
|
-
/** Source of the paywall - affects pending purchase handling */
|
|
44
22
|
readonly source?: PurchaseSource;
|
|
45
|
-
/** Callback when purchase succeeds */
|
|
46
23
|
readonly onPurchaseSuccess?: () => void;
|
|
47
|
-
/** Callback when purchase fails */
|
|
48
24
|
readonly onPurchaseError?: (error: Error | string) => void;
|
|
49
|
-
/** Callback when auth is required (for anonymous users) */
|
|
50
25
|
readonly onAuthRequired?: () => void;
|
|
51
|
-
/** Visibility override */
|
|
52
26
|
readonly visible?: boolean;
|
|
53
|
-
/** Callback when paywall is closed */
|
|
54
27
|
readonly onClose?: () => void;
|
|
55
|
-
/** Trial display configuration (Apple-compliant) */
|
|
56
28
|
readonly trialConfig?: TrialConfig;
|
|
57
29
|
}
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* usePaywallActions Hook
|
|
3
|
-
* Encapsulates purchase and restore flow for the paywall.
|
|
4
|
-
*/
|
|
5
1
|
import { useState, useCallback, useRef, useEffect } from "react";
|
|
6
2
|
import type { PurchasesPackage } from "react-native-purchases";
|
|
7
3
|
import { usePurchaseLoadingStore } from "../../subscription/presentation/stores";
|
|
@@ -52,7 +48,6 @@ export function usePaywallActions({
|
|
|
52
48
|
});
|
|
53
49
|
|
|
54
50
|
const handlePurchase = useCallback(async () => {
|
|
55
|
-
|
|
56
51
|
if (!selectedPlanId) {
|
|
57
52
|
return;
|
|
58
53
|
}
|
|
@@ -71,7 +66,7 @@ export function usePaywallActions({
|
|
|
71
66
|
|
|
72
67
|
if (!pkg) {
|
|
73
68
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
74
|
-
console.error("[usePaywallActions]
|
|
69
|
+
console.error("[usePaywallActions] Package not found", {
|
|
75
70
|
selectedPlanId,
|
|
76
71
|
availablePackages: packages.map(p => p.product.identifier),
|
|
77
72
|
});
|
|
@@ -85,18 +80,12 @@ export function usePaywallActions({
|
|
|
85
80
|
startPurchase(selectedPlanId, "manual");
|
|
86
81
|
|
|
87
82
|
try {
|
|
88
|
-
|
|
89
83
|
const success = await onPurchaseRef.current(pkg);
|
|
90
84
|
|
|
91
85
|
if (success === true) {
|
|
92
86
|
onPurchaseSuccessRef.current?.();
|
|
93
87
|
onCloseRef.current?.();
|
|
94
|
-
} else if (success === false) {
|
|
95
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
96
|
-
console.warn("[usePaywallActions] ⚠️ Purchase returned false (user cancelled or failed)");
|
|
97
|
-
}
|
|
98
88
|
}
|
|
99
|
-
// else: success is undefined/null - no action needed
|
|
100
89
|
} catch (error) {
|
|
101
90
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
102
91
|
onPurchaseErrorRef.current?.(err);
|
|
@@ -107,7 +96,6 @@ export function usePaywallActions({
|
|
|
107
96
|
}, [selectedPlanId, packages, isProcessing, startPurchase, endPurchase]);
|
|
108
97
|
|
|
109
98
|
const handleRestore = useCallback(async () => {
|
|
110
|
-
|
|
111
99
|
if (!onRestoreRef.current) {
|
|
112
100
|
const err = new Error("Restore handler not configured");
|
|
113
101
|
onPurchaseErrorRef.current?.(err);
|
|
@@ -120,13 +108,11 @@ export function usePaywallActions({
|
|
|
120
108
|
|
|
121
109
|
setIsLocalProcessing(true);
|
|
122
110
|
try {
|
|
123
|
-
|
|
124
111
|
const success = await onRestoreRef.current();
|
|
125
112
|
|
|
126
113
|
if (success === true) {
|
|
127
114
|
onPurchaseSuccessRef.current?.();
|
|
128
115
|
}
|
|
129
|
-
// else: success is false/undefined - restore failed or user cancelled, no action needed
|
|
130
116
|
} catch (error) {
|
|
131
117
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
132
118
|
onPurchaseErrorRef.current?.(err);
|
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* RevenueCat Error Classes
|
|
3
|
-
* Domain-specific error types for RevenueCat operations
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
1
|
import { BaseError } from "../../../../shared/utils/BaseError";
|
|
7
2
|
|
|
8
3
|
class RevenueCatError extends BaseError {
|
|
@@ -49,4 +44,3 @@ export class RevenueCatNetworkError extends RevenueCatError {
|
|
|
49
44
|
this.name = "RevenueCatNetworkError";
|
|
50
45
|
}
|
|
51
46
|
}
|
|
52
|
-
|
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* RevenueCat Error Handler
|
|
3
|
-
* Error code mapping and message resolution utilities
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
1
|
import Purchases from "react-native-purchases";
|
|
7
2
|
import {
|
|
8
3
|
ERROR_MESSAGES_MAP,
|
|
@@ -11,12 +6,7 @@ import {
|
|
|
11
6
|
type PurchasesErrorCode,
|
|
12
7
|
} from "./RevenueCatErrorMessages";
|
|
13
8
|
|
|
14
|
-
/**
|
|
15
|
-
* Error Code to Enum Mapping
|
|
16
|
-
* Maps both string keys and numeric codes to Purchases error enum values
|
|
17
|
-
*/
|
|
18
9
|
const ERROR_CODE_MAP = new Map<string, PurchasesErrorCode>([
|
|
19
|
-
// String error codes
|
|
20
10
|
["PURCHASE_CANCELLED_ERROR", Purchases.PURCHASES_ERROR_CODE.PURCHASE_CANCELLED_ERROR],
|
|
21
11
|
["PURCHASE_NOT_ALLOWED_ERROR", Purchases.PURCHASES_ERROR_CODE.PURCHASE_NOT_ALLOWED_ERROR],
|
|
22
12
|
["PURCHASE_INVALID_ERROR", Purchases.PURCHASES_ERROR_CODE.PURCHASE_INVALID_ERROR],
|
|
@@ -31,7 +21,6 @@ const ERROR_CODE_MAP = new Map<string, PurchasesErrorCode>([
|
|
|
31
21
|
["STORE_PROBLEM_ERROR", Purchases.PURCHASES_ERROR_CODE.STORE_PROBLEM_ERROR],
|
|
32
22
|
["PAYMENT_PENDING_ERROR", Purchases.PURCHASES_ERROR_CODE.PAYMENT_PENDING_ERROR],
|
|
33
23
|
|
|
34
|
-
// Numeric error codes as fallback
|
|
35
24
|
["1", Purchases.PURCHASES_ERROR_CODE.PURCHASE_CANCELLED_ERROR],
|
|
36
25
|
["2", Purchases.PURCHASES_ERROR_CODE.STORE_PROBLEM_ERROR],
|
|
37
26
|
["3", Purchases.PURCHASES_ERROR_CODE.PURCHASE_NOT_ALLOWED_ERROR],
|
|
@@ -47,13 +36,6 @@ const ERROR_CODE_MAP = new Map<string, PurchasesErrorCode>([
|
|
|
47
36
|
["0", Purchases.PURCHASES_ERROR_CODE.UNKNOWN_ERROR],
|
|
48
37
|
]);
|
|
49
38
|
|
|
50
|
-
/**
|
|
51
|
-
* Get error message configuration for a given error code
|
|
52
|
-
* Strategy Pattern with Map lookup - O(1) complexity
|
|
53
|
-
*
|
|
54
|
-
* @param errorCode - Error code string from RevenueCat error
|
|
55
|
-
* @returns ErrorMessage configuration
|
|
56
|
-
*/
|
|
57
39
|
function getErrorMessageForCode(errorCode: string | null | undefined): ErrorMessage {
|
|
58
40
|
if (!errorCode) {
|
|
59
41
|
return DEFAULT_ERROR_MESSAGE;
|
|
@@ -70,12 +52,6 @@ function getErrorMessageForCode(errorCode: string | null | undefined): ErrorMess
|
|
|
70
52
|
return DEFAULT_ERROR_MESSAGE;
|
|
71
53
|
}
|
|
72
54
|
|
|
73
|
-
/**
|
|
74
|
-
* Get error message from RevenueCat error object
|
|
75
|
-
*
|
|
76
|
-
* @param error - RevenueCat error object
|
|
77
|
-
* @returns ErrorMessage configuration
|
|
78
|
-
*/
|
|
79
55
|
export function getErrorMessage(error: unknown): ErrorMessage {
|
|
80
56
|
if (!error || typeof error !== "object") {
|
|
81
57
|
return DEFAULT_ERROR_MESSAGE;
|