@umituz/react-native-subscription 2.27.56 → 2.27.58
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/errors/SubscriptionError.ts +3 -0
- package/src/domain/types/RevenueCatData.ts +3 -1
- package/src/domains/config/domain/entities/Plan.ts +2 -2
- package/src/domains/paywall/components/PaywallModal.tsx +6 -3
- package/src/domains/paywall/components/PlanCard.tsx +1 -1
- package/src/domains/paywall/hooks/usePaywallActions.ts +0 -2
- package/src/domains/wallet/domain/mappers/TransactionMapper.ts +2 -2
- package/src/domains/wallet/infrastructure/services/ProductMetadataService.ts +0 -2
- package/src/domains/wallet/presentation/hooks/useProductMetadata.ts +0 -2
- package/src/domains/wallet/presentation/hooks/useTransactionHistory.ts +0 -2
- package/src/infrastructure/models/UserCreditsDocument.ts +7 -17
- package/src/infrastructure/repositories/CreditsRepository.ts +5 -3
- package/src/infrastructure/services/ActivationHandler.ts +15 -4
- package/src/infrastructure/services/CreditsInitializer.ts +5 -3
- package/src/infrastructure/services/SubscriptionInitializer.ts +0 -1
- package/src/infrastructure/services/SubscriptionService.ts +2 -8
- package/src/infrastructure/services/TrialService.ts +0 -1
- package/src/infrastructure/services/app-service-helpers.ts +0 -2
- package/src/infrastructure/utils/Logger.ts +0 -2
- package/src/init/createSubscriptionInitModule.ts +1 -3
- package/src/presentation/hooks/feedback/useFeedbackSubmit.ts +33 -24
- package/src/presentation/hooks/useAuthAwarePurchase.ts +0 -2
- package/src/presentation/hooks/useAuthSubscriptionSync.ts +9 -5
- package/src/presentation/hooks/useCredits.ts +0 -2
- package/src/presentation/hooks/useDeductCredit.ts +0 -2
- package/src/presentation/hooks/useFeatureGate.ts +0 -2
- package/src/presentation/hooks/usePremium.ts +0 -2
- package/src/presentation/hooks/useSavedPurchaseAutoExecution.ts +5 -5
- package/src/presentation/screens/SubscriptionDetailScreen.tsx +6 -6
- package/src/presentation/stores/purchaseLoadingStore.ts +0 -2
- package/src/revenuecat/application/ports/IRevenueCatService.ts +1 -1
- package/src/revenuecat/infrastructure/handlers/PackageHandler.ts +0 -2
- package/src/revenuecat/infrastructure/managers/SubscriptionManager.ts +0 -2
- package/src/revenuecat/infrastructure/services/CustomerInfoListenerManager.ts +0 -2
- package/src/revenuecat/infrastructure/services/PurchaseHandler.ts +0 -2
- package/src/revenuecat/infrastructure/services/RestoreHandler.ts +1 -1
- package/src/revenuecat/infrastructure/services/RevenueCatInitializer.ts +7 -9
- package/src/revenuecat/infrastructure/services/RevenueCatService.ts +3 -1
- package/src/revenuecat/infrastructure/utils/PremiumStatusSyncer.ts +0 -2
- package/src/revenuecat/infrastructure/utils/RenewalDetector.ts +9 -1
- package/src/revenuecat/presentation/hooks/usePurchasePackage.ts +0 -2
- package/src/revenuecat/presentation/hooks/useRevenueCatTrialEligibility.ts +0 -2
- package/src/utils/premiumStatusUtils.ts +5 -4
- package/src/utils/priceUtils.ts +10 -15
- package/src/utils/tierUtils.ts +3 -2
- package/src/presentation/hooks/useSubscription.utils.ts +0 -79
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.27.
|
|
3
|
+
"version": "2.27.58",
|
|
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",
|
|
@@ -10,6 +10,7 @@ export class SubscriptionError extends Error {
|
|
|
10
10
|
super(message);
|
|
11
11
|
this.name = 'SubscriptionError';
|
|
12
12
|
this.code = code;
|
|
13
|
+
Object.setPrototypeOf(this, SubscriptionError.prototype);
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
static notFound(message: string = 'Subscription not found'): SubscriptionError {
|
|
@@ -40,6 +41,7 @@ export class SubscriptionRepositoryError extends Error {
|
|
|
40
41
|
super(message);
|
|
41
42
|
this.name = 'SubscriptionRepositoryError';
|
|
42
43
|
this.code = code;
|
|
44
|
+
Object.setPrototypeOf(this, SubscriptionRepositoryError.prototype);
|
|
43
45
|
}
|
|
44
46
|
}
|
|
45
47
|
|
|
@@ -50,5 +52,6 @@ export class SubscriptionValidationError extends Error {
|
|
|
50
52
|
super(message);
|
|
51
53
|
this.name = 'SubscriptionValidationError';
|
|
52
54
|
this.code = code;
|
|
55
|
+
Object.setPrototypeOf(this, SubscriptionValidationError.prototype);
|
|
53
56
|
}
|
|
54
57
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { PeriodType } from "../entities/SubscriptionStatus";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* RevenueCat subscription data (Single Source of Truth)
|
|
3
5
|
* Used across the subscription package for storing RevenueCat data in Firestore
|
|
@@ -8,5 +10,5 @@ export interface RevenueCatData {
|
|
|
8
10
|
originalTransactionId?: string;
|
|
9
11
|
isPremium?: boolean;
|
|
10
12
|
/** RevenueCat period type: NORMAL, INTRO, or TRIAL */
|
|
11
|
-
periodType?:
|
|
13
|
+
periodType?: PeriodType;
|
|
12
14
|
}
|
|
@@ -32,8 +32,8 @@ export const calculatePlanMetadata = (
|
|
|
32
32
|
const totalCost = plan.credits * costPerCredit;
|
|
33
33
|
const netRevenue = plan.price * (1 - commissionRate);
|
|
34
34
|
const profit = netRevenue - totalCost;
|
|
35
|
-
const profitMargin = (profit / plan.price) * 100;
|
|
36
|
-
const pricePerCredit = plan.price / plan.credits;
|
|
35
|
+
const profitMargin = plan.price > 0 ? (profit / plan.price) * 100 : 0;
|
|
36
|
+
const pricePerCredit = plan.credits > 0 ? plan.price / plan.credits : 0;
|
|
37
37
|
|
|
38
38
|
return {
|
|
39
39
|
cost: totalCost,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Paywall Modal
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import React, { useState, useCallback } from "react";
|
|
5
|
+
import React, { useState, useCallback, useEffect } from "react";
|
|
6
6
|
import { View, ScrollView, TouchableOpacity, Linking, type ImageSourcePropType } from "react-native";
|
|
7
7
|
import { BaseModal, useAppDesignTokens, AtomicText, AtomicIcon, AtomicSpinner } from "@umituz/react-native-design-system";
|
|
8
8
|
import { Image } from "expo-image";
|
|
@@ -14,8 +14,6 @@ import { PaywallFeatures } from "./PaywallFeatures";
|
|
|
14
14
|
import { PaywallFooter } from "./PaywallFooter";
|
|
15
15
|
import { usePurchaseLoadingStore, selectIsPurchasing } from "../../../presentation/stores";
|
|
16
16
|
|
|
17
|
-
declare const __DEV__: boolean;
|
|
18
|
-
|
|
19
17
|
/** Trial eligibility info per product */
|
|
20
18
|
export interface TrialEligibilityInfo {
|
|
21
19
|
/** Whether eligible for trial */
|
|
@@ -56,6 +54,11 @@ export const PaywallModal: React.FC<PaywallModalProps> = React.memo((props) => {
|
|
|
56
54
|
const isGlobalPurchasing = usePurchaseLoadingStore(selectIsPurchasing);
|
|
57
55
|
const { startPurchase, endPurchase } = usePurchaseLoadingStore();
|
|
58
56
|
|
|
57
|
+
// Reset selected plan when packages change
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
setSelectedPlanId(null);
|
|
60
|
+
}, [packages]);
|
|
61
|
+
|
|
59
62
|
// Combined processing state
|
|
60
63
|
const isProcessing = isLocalProcessing || isGlobalPurchasing;
|
|
61
64
|
|
|
@@ -78,7 +78,7 @@ export const PlanCard: React.FC<PlanCardProps> = React.memo(
|
|
|
78
78
|
</AtomicText>
|
|
79
79
|
|
|
80
80
|
{/* Credits info */}
|
|
81
|
-
{creditAmount && creditsLabel && (
|
|
81
|
+
{creditAmount != null && creditAmount > 0 && creditsLabel && (
|
|
82
82
|
<AtomicText type="bodySmall" style={{ color: tokens.colors.textSecondary }}>
|
|
83
83
|
{creditAmount} {creditsLabel}
|
|
84
84
|
</AtomicText>
|
|
@@ -4,8 +4,6 @@ import { useRestorePurchase } from "../../../revenuecat/presentation/hooks/useRe
|
|
|
4
4
|
import { useAuthAwarePurchase } from "../../../presentation/hooks/useAuthAwarePurchase";
|
|
5
5
|
import type { PurchaseSource } from "../../../domain/entities/Credits";
|
|
6
6
|
|
|
7
|
-
declare const __DEV__: boolean;
|
|
8
|
-
|
|
9
7
|
interface UsePaywallActionsProps {
|
|
10
8
|
source?: PurchaseSource;
|
|
11
9
|
onPurchaseSuccess?: () => void;
|
|
@@ -11,7 +11,7 @@ export class TransactionMapper {
|
|
|
11
11
|
const data = docSnap.data();
|
|
12
12
|
return {
|
|
13
13
|
id: docSnap.id,
|
|
14
|
-
userId: data.userId
|
|
14
|
+
userId: data.userId ?? defaultUserId,
|
|
15
15
|
change: data.change,
|
|
16
16
|
reason: data.reason,
|
|
17
17
|
feature: data.feature,
|
|
@@ -19,7 +19,7 @@ export class TransactionMapper {
|
|
|
19
19
|
packageId: data.packageId,
|
|
20
20
|
subscriptionPlan: data.subscriptionPlan,
|
|
21
21
|
description: data.description,
|
|
22
|
-
createdAt: data.createdAt?.toMillis?.()
|
|
22
|
+
createdAt: data.createdAt?.toMillis?.() ?? Date.now(),
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
25
|
|
|
@@ -14,8 +14,6 @@ import type {
|
|
|
14
14
|
} from "../../domain/types/wallet.types";
|
|
15
15
|
import { ProductMetadataService } from "../../infrastructure/services/ProductMetadataService";
|
|
16
16
|
|
|
17
|
-
declare const __DEV__: boolean;
|
|
18
|
-
|
|
19
17
|
const CACHE_CONFIG = {
|
|
20
18
|
staleTime: 5 * 60 * 1000, // 5 minutes
|
|
21
19
|
gcTime: 30 * 60 * 1000, // 30 minutes
|
|
@@ -17,8 +17,6 @@ import type {
|
|
|
17
17
|
} from "../../domain/types/transaction.types";
|
|
18
18
|
import { TransactionRepository } from "../../infrastructure/repositories/TransactionRepository";
|
|
19
19
|
|
|
20
|
-
declare const __DEV__: boolean;
|
|
21
|
-
|
|
22
20
|
export const transactionQueryKeys = {
|
|
23
21
|
all: ["transactions"] as const,
|
|
24
22
|
user: (userId: string) => ["transactions", userId] as const,
|
|
@@ -1,23 +1,13 @@
|
|
|
1
|
+
import type { PurchaseSource, PurchaseType } from "../../domain/entities/Credits";
|
|
2
|
+
import type { SubscriptionStatusType, PeriodType } from "../../domain/entities/SubscriptionStatus";
|
|
3
|
+
|
|
4
|
+
export type { PurchaseSource, PurchaseType } from "../../domain/entities/Credits";
|
|
5
|
+
export type { SubscriptionStatusType, PeriodType } from "../../domain/entities/SubscriptionStatus";
|
|
6
|
+
|
|
1
7
|
export interface FirestoreTimestamp {
|
|
2
8
|
toDate: () => Date;
|
|
3
9
|
}
|
|
4
10
|
|
|
5
|
-
export type PurchaseSource =
|
|
6
|
-
| "onboarding"
|
|
7
|
-
| "settings"
|
|
8
|
-
| "upgrade_prompt"
|
|
9
|
-
| "home_screen"
|
|
10
|
-
| "feature_gate"
|
|
11
|
-
| "credits_exhausted"
|
|
12
|
-
| "renewal";
|
|
13
|
-
|
|
14
|
-
export type PurchaseType = "initial" | "renewal" | "upgrade" | "downgrade";
|
|
15
|
-
|
|
16
|
-
export type SubscriptionDocStatus = "active" | "trial" | "trial_canceled" | "expired" | "canceled" | "free";
|
|
17
|
-
|
|
18
|
-
/** RevenueCat period types */
|
|
19
|
-
export type PeriodType = "NORMAL" | "INTRO" | "TRIAL";
|
|
20
|
-
|
|
21
11
|
export interface PurchaseMetadata {
|
|
22
12
|
productId: string;
|
|
23
13
|
packageType: "weekly" | "monthly" | "yearly" | "lifetime";
|
|
@@ -33,7 +23,7 @@ export interface PurchaseMetadata {
|
|
|
33
23
|
export interface UserCreditsDocumentRead {
|
|
34
24
|
// Core subscription status
|
|
35
25
|
isPremium?: boolean;
|
|
36
|
-
status?:
|
|
26
|
+
status?: SubscriptionStatusType;
|
|
37
27
|
|
|
38
28
|
// Dates (all from RevenueCat)
|
|
39
29
|
purchasedAt?: FirestoreTimestamp;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Credits Repository
|
|
3
3
|
*/
|
|
4
|
-
declare const __DEV__: boolean;
|
|
5
4
|
|
|
6
5
|
import { doc, getDoc, runTransaction, serverTimestamp, type Firestore, type Transaction } from "firebase/firestore";
|
|
7
6
|
import { BaseRepository, getFirestore } from "@umituz/react-native-firebase";
|
|
@@ -76,10 +75,13 @@ export class CreditsRepository extends BaseRepository {
|
|
|
76
75
|
periodType: revenueCatData?.periodType,
|
|
77
76
|
};
|
|
78
77
|
|
|
79
|
-
|
|
78
|
+
await initializeCreditsTransaction(db, this.getRef(db, userId), cfg, purchaseId, metadata);
|
|
79
|
+
// Re-fetch from Firestore to get the actual stored data with all fields
|
|
80
|
+
const snap = await getDoc(this.getRef(db, userId));
|
|
81
|
+
const fullData = snap.exists() ? snap.data() as UserCreditsDocumentRead : undefined;
|
|
80
82
|
return {
|
|
81
83
|
success: true,
|
|
82
|
-
data: CreditsMapper.toEntity(
|
|
84
|
+
data: fullData ? CreditsMapper.toEntity(fullData) : undefined,
|
|
83
85
|
};
|
|
84
86
|
} catch (e: unknown) {
|
|
85
87
|
const message = e instanceof Error ? e.message : String(e);
|
|
@@ -81,17 +81,28 @@ async function notifyStatusChange(
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Safe error handler - wraps error callbacks to prevent secondary failures
|
|
86
|
+
*/
|
|
87
|
+
export async function safeHandleError(
|
|
88
|
+
onError: ((error: Error, context: string) => Promise<void> | void) | undefined,
|
|
86
89
|
error: unknown,
|
|
87
90
|
context: string
|
|
88
91
|
): Promise<void> {
|
|
89
|
-
if (!
|
|
92
|
+
if (!onError) return;
|
|
90
93
|
|
|
91
94
|
try {
|
|
92
95
|
const err = error instanceof Error ? error : new Error("Unknown error");
|
|
93
|
-
await
|
|
96
|
+
await onError(err, context);
|
|
94
97
|
} catch {
|
|
95
98
|
// Ignore callback errors
|
|
96
99
|
}
|
|
97
100
|
}
|
|
101
|
+
|
|
102
|
+
async function handleError(
|
|
103
|
+
config: ActivationHandlerConfig,
|
|
104
|
+
error: unknown,
|
|
105
|
+
context: string
|
|
106
|
+
): Promise<void> {
|
|
107
|
+
await safeHandleError(config.onError, error, `ActivationHandler.${context}`);
|
|
108
|
+
}
|
|
@@ -74,7 +74,7 @@ export async function initializeCreditsTransaction(
|
|
|
74
74
|
const allocation = packageType && packageType !== "unknown"
|
|
75
75
|
? getCreditAllocation(packageType, config.packageAllocations)
|
|
76
76
|
: null;
|
|
77
|
-
const creditLimit = allocation
|
|
77
|
+
const creditLimit = allocation ?? config.creditLimit;
|
|
78
78
|
|
|
79
79
|
const platform = Platform.OS as "ios" | "android";
|
|
80
80
|
const appVersion = Constants.expoConfig?.version;
|
|
@@ -96,7 +96,7 @@ export async function initializeCreditsTransaction(
|
|
|
96
96
|
type: purchaseType,
|
|
97
97
|
platform,
|
|
98
98
|
appVersion,
|
|
99
|
-
timestamp:
|
|
99
|
+
timestamp: Timestamp.fromDate(new Date()) as unknown as PurchaseMetadata["timestamp"],
|
|
100
100
|
} : undefined;
|
|
101
101
|
|
|
102
102
|
const purchaseHistory = purchaseMetadata
|
|
@@ -107,7 +107,9 @@ export async function initializeCreditsTransaction(
|
|
|
107
107
|
const willRenew = metadata?.willRenew;
|
|
108
108
|
const periodType = metadata?.periodType;
|
|
109
109
|
|
|
110
|
-
const
|
|
110
|
+
const expirationDateStr = metadata?.expirationDate;
|
|
111
|
+
const isExpired = expirationDateStr ? new Date(expirationDateStr).getTime() < Date.now() : false;
|
|
112
|
+
const status = resolveSubscriptionStatus({ isPremium, willRenew, isExpired, periodType });
|
|
111
113
|
|
|
112
114
|
// Determine if this is a status sync (not a new purchase or renewal)
|
|
113
115
|
// Status sync should preserve existing credits, only update metadata
|
|
@@ -6,7 +6,6 @@
|
|
|
6
6
|
* - Relies on CustomerInfoUpdateListener for state updates
|
|
7
7
|
* - No manual timeouts - uses auth state listener with cleanup
|
|
8
8
|
*/
|
|
9
|
-
declare const __DEV__: boolean;
|
|
10
9
|
|
|
11
10
|
import { Platform } from "react-native";
|
|
12
11
|
import type { CustomerInfo } from "react-native-purchases";
|
|
@@ -16,6 +16,7 @@ import type { SubscriptionConfig } from "../../domain/value-objects/Subscription
|
|
|
16
16
|
import {
|
|
17
17
|
activateSubscription,
|
|
18
18
|
deactivateSubscription,
|
|
19
|
+
safeHandleError,
|
|
19
20
|
type ActivationHandlerConfig,
|
|
20
21
|
} from "./ActivationHandler";
|
|
21
22
|
|
|
@@ -108,14 +109,7 @@ export class SubscriptionService implements ISubscriptionService {
|
|
|
108
109
|
}
|
|
109
110
|
|
|
110
111
|
private async handleError(error: unknown, context: string): Promise<void> {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
try {
|
|
114
|
-
const err = error instanceof Error ? error : new Error("Unknown error");
|
|
115
|
-
await this.handlerConfig.onError(err, `SubscriptionService.${context}`);
|
|
116
|
-
} catch {
|
|
117
|
-
// Ignore callback errors
|
|
118
|
-
}
|
|
112
|
+
await safeHandleError(this.handlerConfig.onError, error, `SubscriptionService.${context}`);
|
|
119
113
|
}
|
|
120
114
|
}
|
|
121
115
|
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import type { InitModule } from '@umituz/react-native-design-system';
|
|
2
2
|
import { initializeSubscription, type SubscriptionInitConfig } from '../infrastructure/services/SubscriptionInitializer';
|
|
3
3
|
|
|
4
|
-
declare const __DEV__: boolean;
|
|
5
|
-
|
|
6
4
|
export interface SubscriptionInitModuleConfig extends Omit<SubscriptionInitConfig, 'apiKey'> {
|
|
7
5
|
getApiKey: () => string | undefined;
|
|
8
6
|
critical?: boolean;
|
|
@@ -29,7 +27,7 @@ export function createSubscriptionInitModule(config: SubscriptionInitModuleConfi
|
|
|
29
27
|
return true;
|
|
30
28
|
} catch (error) {
|
|
31
29
|
if (__DEV__) console.error('[SubscriptionInit] Error:', error);
|
|
32
|
-
return
|
|
30
|
+
return false;
|
|
33
31
|
}
|
|
34
32
|
},
|
|
35
33
|
};
|
|
@@ -34,19 +34,23 @@ export function usePaywallFeedbackSubmit(
|
|
|
34
34
|
});
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
37
|
+
try {
|
|
38
|
+
const result = await submitPaywallFeedback(
|
|
39
|
+
user?.uid ?? null,
|
|
40
|
+
user?.email ?? null,
|
|
41
|
+
reason
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
if (result.success) {
|
|
45
|
+
onSuccess?.();
|
|
46
|
+
} else if (result.error) {
|
|
47
|
+
onError?.(result.error);
|
|
48
|
+
}
|
|
49
|
+
} catch (err) {
|
|
50
|
+
onError?.(err instanceof Error ? err : new Error("Feedback submission failed"));
|
|
51
|
+
} finally {
|
|
52
|
+
onComplete?.();
|
|
47
53
|
}
|
|
48
|
-
|
|
49
|
-
onComplete?.();
|
|
50
54
|
},
|
|
51
55
|
[user, onSuccess, onError, onComplete]
|
|
52
56
|
);
|
|
@@ -84,19 +88,24 @@ export function useSettingsFeedbackSubmit(
|
|
|
84
88
|
});
|
|
85
89
|
}
|
|
86
90
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
91
|
+
try {
|
|
92
|
+
const result = await submitSettingsFeedback(
|
|
93
|
+
user?.uid ?? null,
|
|
94
|
+
user?.email ?? null,
|
|
95
|
+
data
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
if (result.success) {
|
|
99
|
+
onSuccess?.();
|
|
100
|
+
} else if (result.error) {
|
|
101
|
+
onError?.(result.error);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return result;
|
|
105
|
+
} catch (err) {
|
|
106
|
+
onError?.(err instanceof Error ? err : new Error("Feedback submission failed"));
|
|
107
|
+
return { success: false, error: err instanceof Error ? err : new Error("Feedback submission failed") };
|
|
97
108
|
}
|
|
98
|
-
|
|
99
|
-
return result;
|
|
100
109
|
},
|
|
101
110
|
[user, onSuccess, onError]
|
|
102
111
|
);
|
|
@@ -8,8 +8,6 @@ import type { PurchasesPackage } from "react-native-purchases";
|
|
|
8
8
|
import { usePremium } from "./usePremium";
|
|
9
9
|
import type { PurchaseSource } from "../../domain/entities/Credits";
|
|
10
10
|
|
|
11
|
-
declare const __DEV__: boolean;
|
|
12
|
-
|
|
13
11
|
export interface PurchaseAuthProvider {
|
|
14
12
|
isAuthenticated: () => boolean;
|
|
15
13
|
showAuthModal: () => void;
|
|
@@ -41,11 +41,15 @@ export function useAuthSubscriptionSync(
|
|
|
41
41
|
return;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
44
|
+
try {
|
|
45
|
+
if (previousUserId && previousUserId !== userId) {
|
|
46
|
+
await initialize(userId);
|
|
47
|
+
} else if (!isInitializedRef.current) {
|
|
48
|
+
await initialize(userId);
|
|
49
|
+
isInitializedRef.current = true;
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
// Prevent unhandled promise rejection from async auth callback
|
|
49
53
|
}
|
|
50
54
|
|
|
51
55
|
previousUserIdRef.current = userId;
|
|
@@ -15,8 +15,6 @@ import {
|
|
|
15
15
|
isCreditsRepositoryConfigured,
|
|
16
16
|
} from "../../infrastructure/repositories/CreditsRepositoryProvider";
|
|
17
17
|
|
|
18
|
-
declare const __DEV__: boolean;
|
|
19
|
-
|
|
20
18
|
export const creditsQueryKeys = {
|
|
21
19
|
all: ["credits"] as const,
|
|
22
20
|
user: (userId: string) => ["credits", userId] as const,
|
|
@@ -11,8 +11,6 @@ import { creditsQueryKeys } from "./useCredits";
|
|
|
11
11
|
|
|
12
12
|
import { timezoneService } from "@umituz/react-native-design-system";
|
|
13
13
|
|
|
14
|
-
declare const __DEV__: boolean;
|
|
15
|
-
|
|
16
14
|
export interface UseDeductCreditParams {
|
|
17
15
|
userId: string | undefined;
|
|
18
16
|
onCreditsExhausted?: () => void;
|
|
@@ -17,8 +17,6 @@ import {
|
|
|
17
17
|
} from '../../revenuecat/presentation/hooks/useSubscriptionQueries';
|
|
18
18
|
import { usePaywallVisibility } from './usePaywallVisibility';
|
|
19
19
|
|
|
20
|
-
declare const __DEV__: boolean;
|
|
21
|
-
|
|
22
20
|
export interface UsePremiumResult {
|
|
23
21
|
isPremium: boolean;
|
|
24
22
|
isLoading: boolean;
|
|
@@ -14,8 +14,6 @@ import { usePremium } from "./usePremium";
|
|
|
14
14
|
import { SubscriptionManager } from "../../revenuecat";
|
|
15
15
|
import { usePurchaseLoadingStore } from "../stores";
|
|
16
16
|
|
|
17
|
-
declare const __DEV__: boolean;
|
|
18
|
-
|
|
19
17
|
export interface UseSavedPurchaseAutoExecutionParams {
|
|
20
18
|
onSuccess?: () => void;
|
|
21
19
|
onError?: (error: Error) => void;
|
|
@@ -118,15 +116,17 @@ export const useSavedPurchaseAutoExecution = (
|
|
|
118
116
|
|
|
119
117
|
if (isReady) {
|
|
120
118
|
const pkg = savedPurchase.pkg;
|
|
121
|
-
clearSavedPurchase();
|
|
122
119
|
|
|
123
120
|
startPurchaseRef.current(pkg.product.identifier, "auto-execution");
|
|
124
121
|
|
|
125
122
|
try {
|
|
126
123
|
const success = await purchasePackageRef.current(pkg);
|
|
127
124
|
|
|
128
|
-
if (success
|
|
129
|
-
|
|
125
|
+
if (success) {
|
|
126
|
+
clearSavedPurchase();
|
|
127
|
+
if (onSuccessRef.current) {
|
|
128
|
+
onSuccessRef.current();
|
|
129
|
+
}
|
|
130
130
|
}
|
|
131
131
|
} catch (error) {
|
|
132
132
|
if (onErrorRef.current && error instanceof Error) {
|
|
@@ -81,9 +81,9 @@ export const SubscriptionDetailScreen: React.FC<
|
|
|
81
81
|
/>
|
|
82
82
|
)}
|
|
83
83
|
|
|
84
|
-
{showCredits && (
|
|
84
|
+
{showCredits && config.credits && (
|
|
85
85
|
<CreditsList
|
|
86
|
-
credits={config.credits
|
|
86
|
+
credits={config.credits}
|
|
87
87
|
title={
|
|
88
88
|
config.translations.usageTitle || config.translations.creditsTitle
|
|
89
89
|
}
|
|
@@ -92,11 +92,11 @@ export const SubscriptionDetailScreen: React.FC<
|
|
|
92
92
|
/>
|
|
93
93
|
)}
|
|
94
94
|
|
|
95
|
-
{showUpgradePrompt && (
|
|
95
|
+
{showUpgradePrompt && config.upgradePrompt && (
|
|
96
96
|
<UpgradePrompt
|
|
97
|
-
title={config.upgradePrompt
|
|
98
|
-
subtitle={config.upgradePrompt
|
|
99
|
-
benefits={config.upgradePrompt
|
|
97
|
+
title={config.upgradePrompt.title}
|
|
98
|
+
subtitle={config.upgradePrompt.subtitle}
|
|
99
|
+
benefits={config.upgradePrompt.benefits}
|
|
100
100
|
upgradeButtonLabel={config.translations.upgradeButton}
|
|
101
101
|
onUpgrade={config.onUpgrade}
|
|
102
102
|
/>
|
|
@@ -8,7 +8,7 @@ import type { PurchasesPackage, PurchasesOffering, CustomerInfo } from "react-na
|
|
|
8
8
|
export interface InitializeResult {
|
|
9
9
|
success: boolean;
|
|
10
10
|
offering: PurchasesOffering | null;
|
|
11
|
-
|
|
11
|
+
isPremium: boolean;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export interface PurchaseResult {
|
|
@@ -7,8 +7,6 @@ import type { PurchasesPackage, CustomerInfo } from "react-native-purchases";
|
|
|
7
7
|
import type { IRevenueCatService } from "../../application/ports/IRevenueCatService";
|
|
8
8
|
import { getPremiumEntitlement } from "../../domain/types/RevenueCatTypes";
|
|
9
9
|
|
|
10
|
-
declare const __DEV__: boolean;
|
|
11
|
-
|
|
12
10
|
export interface PremiumStatus {
|
|
13
11
|
isPremium: boolean;
|
|
14
12
|
expirationDate: Date | null;
|
|
@@ -4,8 +4,6 @@
|
|
|
4
4
|
* Coordinates UserIdProvider, InitializationCache, and PackageHandler
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
declare const __DEV__: boolean;
|
|
8
|
-
|
|
9
7
|
import type { PurchasesPackage } from "react-native-purchases";
|
|
10
8
|
import type { RevenueCatConfig } from "../../domain/value-objects/RevenueCatConfig";
|
|
11
9
|
import type { IRevenueCatService } from "../../application/ports/IRevenueCatService";
|
|
@@ -15,8 +15,6 @@ import {
|
|
|
15
15
|
type RenewalState,
|
|
16
16
|
} from "../utils/RenewalDetector";
|
|
17
17
|
|
|
18
|
-
declare const __DEV__: boolean;
|
|
19
|
-
|
|
20
18
|
export class CustomerInfoListenerManager {
|
|
21
19
|
private listener: CustomerInfoUpdateListener | null = null;
|
|
22
20
|
private currentUserId: string | null = null;
|
|
@@ -6,8 +6,6 @@ import { isUserCancelledError, getErrorMessage } from "../../domain/types/Revenu
|
|
|
6
6
|
import { syncPremiumStatus, notifyPurchaseCompleted } from "../utils/PremiumStatusSyncer";
|
|
7
7
|
import { getSavedPurchase, clearSavedPurchase } from "../../../presentation/hooks/useAuthAwarePurchase";
|
|
8
8
|
|
|
9
|
-
declare const __DEV__: boolean;
|
|
10
|
-
|
|
11
9
|
export interface PurchaseHandlerDeps {
|
|
12
10
|
config: RevenueCatConfig;
|
|
13
11
|
isInitialized: () => boolean;
|
|
@@ -22,7 +22,7 @@ export async function handleRestore(deps: RestoreHandlerDeps, userId: string): P
|
|
|
22
22
|
}
|
|
23
23
|
await notifyRestoreCompleted(deps.config, userId, isPremium, customerInfo);
|
|
24
24
|
|
|
25
|
-
return { success:
|
|
25
|
+
return { success: true, isPremium, customerInfo };
|
|
26
26
|
} catch (error) {
|
|
27
27
|
throw new RevenueCatRestoreError(getErrorMessage(error, "Restore failed"));
|
|
28
28
|
}
|
|
@@ -3,8 +3,6 @@ import type { InitializeResult } from "../../application/ports/IRevenueCatServic
|
|
|
3
3
|
import type { RevenueCatConfig } from "../../domain/value-objects/RevenueCatConfig";
|
|
4
4
|
import { resolveApiKey } from "../utils/ApiKeyResolver";
|
|
5
5
|
|
|
6
|
-
declare const __DEV__: boolean;
|
|
7
|
-
|
|
8
6
|
export interface InitializerDeps {
|
|
9
7
|
config: RevenueCatConfig;
|
|
10
8
|
isInitialized: () => boolean;
|
|
@@ -33,8 +31,8 @@ function configureLogHandler(): void {
|
|
|
33
31
|
}
|
|
34
32
|
|
|
35
33
|
function buildSuccessResult(deps: InitializerDeps, customerInfo: CustomerInfo, offerings: PurchasesOfferings): InitializeResult {
|
|
36
|
-
const
|
|
37
|
-
return { success: true, offering: offerings.current,
|
|
34
|
+
const isPremium = !!customerInfo.entitlements.active[deps.config.entitlementIdentifier];
|
|
35
|
+
return { success: true, offering: offerings.current, isPremium };
|
|
38
36
|
}
|
|
39
37
|
|
|
40
38
|
export async function initializeSDK(
|
|
@@ -50,7 +48,7 @@ export async function initializeSDK(
|
|
|
50
48
|
]);
|
|
51
49
|
return buildSuccessResult(deps, customerInfo, offerings);
|
|
52
50
|
} catch {
|
|
53
|
-
return { success: false, offering: null,
|
|
51
|
+
return { success: false, offering: null, isPremium: false };
|
|
54
52
|
}
|
|
55
53
|
}
|
|
56
54
|
|
|
@@ -69,20 +67,20 @@ export async function initializeSDK(
|
|
|
69
67
|
const offerings = await Purchases.getOfferings();
|
|
70
68
|
return buildSuccessResult(deps, customerInfo, offerings);
|
|
71
69
|
} catch {
|
|
72
|
-
return { success: false, offering: null,
|
|
70
|
+
return { success: false, offering: null, isPremium: false };
|
|
73
71
|
}
|
|
74
72
|
}
|
|
75
73
|
|
|
76
74
|
if (configurationInProgress) {
|
|
77
75
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
78
76
|
if (isPurchasesConfigured) return initializeSDK(deps, userId, apiKey);
|
|
79
|
-
return { success: false, offering: null,
|
|
77
|
+
return { success: false, offering: null, isPremium: false };
|
|
80
78
|
}
|
|
81
79
|
|
|
82
80
|
const key = apiKey || resolveApiKey(deps.config);
|
|
83
81
|
if (!key) {
|
|
84
82
|
if (__DEV__) console.log('[RevenueCat] No API key');
|
|
85
|
-
return { success: false, offering: null,
|
|
83
|
+
return { success: false, offering: null, isPremium: false };
|
|
86
84
|
}
|
|
87
85
|
|
|
88
86
|
configurationInProgress = true;
|
|
@@ -108,7 +106,7 @@ export async function initializeSDK(
|
|
|
108
106
|
return buildSuccessResult(deps, customerInfo, offerings);
|
|
109
107
|
} catch (error) {
|
|
110
108
|
if (__DEV__) console.error('[RevenueCat] Init failed:', error);
|
|
111
|
-
return { success: false, offering: null,
|
|
109
|
+
return { success: false, offering: null, isPremium: false };
|
|
112
110
|
} finally {
|
|
113
111
|
configurationInProgress = false;
|
|
114
112
|
}
|
|
@@ -57,10 +57,12 @@ export class RevenueCatService implements IRevenueCatService {
|
|
|
57
57
|
|
|
58
58
|
async initialize(userId: string, apiKey?: string): Promise<InitializeResult> {
|
|
59
59
|
if (this.isInitialized() && this.getCurrentUserId() === userId) {
|
|
60
|
+
const customerInfo = await Purchases.getCustomerInfo();
|
|
61
|
+
const isPremium = !!customerInfo.entitlements.active[this.stateManager.getConfig().entitlementIdentifier];
|
|
60
62
|
return {
|
|
61
63
|
success: true,
|
|
62
64
|
offering: await this.fetchOfferings(),
|
|
63
|
-
|
|
65
|
+
isPremium,
|
|
64
66
|
};
|
|
65
67
|
}
|
|
66
68
|
|
|
@@ -8,8 +8,6 @@ import type { RevenueCatConfig } from "../../domain/value-objects/RevenueCatConf
|
|
|
8
8
|
import type { PurchaseSource } from "../../../domain/entities/Credits";
|
|
9
9
|
import { getPremiumEntitlement } from "../../domain/types/RevenueCatTypes";
|
|
10
10
|
|
|
11
|
-
declare const __DEV__: boolean;
|
|
12
|
-
|
|
13
11
|
export async function syncPremiumStatus(
|
|
14
12
|
config: RevenueCatConfig,
|
|
15
13
|
userId: string,
|
|
@@ -82,7 +82,15 @@ export function detectRenewal(
|
|
|
82
82
|
};
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
if (!newExpirationDate) {
|
|
86
|
+
// Lifetime subscription (no expiration) - not a renewal
|
|
87
|
+
return {
|
|
88
|
+
...baseResult,
|
|
89
|
+
productId,
|
|
90
|
+
newExpirationDate,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const newExpiration = new Date(newExpirationDate);
|
|
86
94
|
const previousExpiration = new Date(state.previousExpirationDate);
|
|
87
95
|
const productChanged = productId !== state.previousProductId;
|
|
88
96
|
const expirationExtended = newExpiration > previousExpiration;
|
|
@@ -16,8 +16,6 @@ import { SubscriptionManager } from "../../infrastructure/managers/SubscriptionM
|
|
|
16
16
|
import { SUBSCRIPTION_QUERY_KEYS } from "./subscriptionQueryKeys";
|
|
17
17
|
import { creditsQueryKeys } from "../../../presentation/hooks/useCredits";
|
|
18
18
|
|
|
19
|
-
declare const __DEV__: boolean;
|
|
20
|
-
|
|
21
19
|
/** Purchase mutation result - simplified for presentation layer */
|
|
22
20
|
export interface PurchaseMutationResult {
|
|
23
21
|
success: boolean;
|
|
@@ -11,8 +11,6 @@ import Purchases, {
|
|
|
11
11
|
} from "react-native-purchases";
|
|
12
12
|
import { getRevenueCatService } from "../../infrastructure/services/RevenueCatService";
|
|
13
13
|
|
|
14
|
-
declare const __DEV__: boolean;
|
|
15
|
-
|
|
16
14
|
/** Trial eligibility info for a single product */
|
|
17
15
|
export interface ProductTrialEligibility {
|
|
18
16
|
/** Product identifier */
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { PremiumStatusFetcher } from './types';
|
|
8
|
+
import { isGuest } from './authUtils';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Get isPremium value with centralized logic
|
|
@@ -13,17 +14,17 @@ export function getIsPremium(
|
|
|
13
14
|
isGuestFlag: boolean,
|
|
14
15
|
userId: string | null,
|
|
15
16
|
isPremiumOrFetcher: boolean | PremiumStatusFetcher,
|
|
16
|
-
):
|
|
17
|
+
): Promise<boolean> {
|
|
17
18
|
// Guest users NEVER have premium
|
|
18
|
-
if (isGuestFlag
|
|
19
|
+
if (isGuest(isGuestFlag, userId)) return Promise.resolve(false);
|
|
19
20
|
|
|
20
21
|
// Sync mode: return the provided isPremium value
|
|
21
|
-
if (typeof isPremiumOrFetcher === 'boolean') return isPremiumOrFetcher;
|
|
22
|
+
if (typeof isPremiumOrFetcher === 'boolean') return Promise.resolve(isPremiumOrFetcher);
|
|
22
23
|
|
|
23
24
|
// Async mode: fetch premium status
|
|
24
25
|
return (async () => {
|
|
25
26
|
try {
|
|
26
|
-
return await isPremiumOrFetcher.isPremium(userId);
|
|
27
|
+
return await isPremiumOrFetcher.isPremium(userId!);
|
|
27
28
|
} catch (error) {
|
|
28
29
|
throw new Error(
|
|
29
30
|
`Failed to fetch premium status: ${error instanceof Error ? error.message : String(error)}`
|
package/src/utils/priceUtils.ts
CHANGED
|
@@ -20,6 +20,14 @@ export function formatPrice(price: number, currencyCode: string): string {
|
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
import { detectPackageType } from './packageTypeDetector';
|
|
24
|
+
|
|
25
|
+
const PERIOD_SUFFIX_MAP: Record<string, string> = {
|
|
26
|
+
weekly: '/week',
|
|
27
|
+
monthly: '/month',
|
|
28
|
+
yearly: '/year',
|
|
29
|
+
};
|
|
30
|
+
|
|
23
31
|
/**
|
|
24
32
|
* Extract billing period suffix from package identifier
|
|
25
33
|
* Apple App Store Guideline 3.1.2 Compliance:
|
|
@@ -30,21 +38,8 @@ export function formatPrice(price: number, currencyCode: string): string {
|
|
|
30
38
|
* @returns Billing period suffix (e.g., "/week", "/month", "/year") or empty string
|
|
31
39
|
*/
|
|
32
40
|
export function getBillingPeriodSuffix(identifier: string): string {
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
if (lowerIdentifier.includes('weekly') || lowerIdentifier.includes('week')) {
|
|
36
|
-
return '/week';
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if (lowerIdentifier.includes('monthly') || lowerIdentifier.includes('month')) {
|
|
40
|
-
return '/month';
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (lowerIdentifier.includes('annual') || lowerIdentifier.includes('year') || lowerIdentifier.includes('yearly')) {
|
|
44
|
-
return '/year';
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
return '';
|
|
41
|
+
const packageType = detectPackageType(identifier);
|
|
42
|
+
return PERIOD_SUFFIX_MAP[packageType] ?? '';
|
|
48
43
|
}
|
|
49
44
|
|
|
50
45
|
/**
|
package/src/utils/tierUtils.ts
CHANGED
|
@@ -5,13 +5,14 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import type { UserTierInfo } from './types';
|
|
8
|
+
import { isGuest } from './authUtils';
|
|
8
9
|
|
|
9
10
|
export function getUserTierInfo(
|
|
10
11
|
isGuestFlag: boolean,
|
|
11
12
|
userId: string | null,
|
|
12
13
|
isPremium: boolean,
|
|
13
14
|
): UserTierInfo {
|
|
14
|
-
if (isGuestFlag
|
|
15
|
+
if (isGuest(isGuestFlag, userId)) {
|
|
15
16
|
return {
|
|
16
17
|
tier: 'guest',
|
|
17
18
|
isPremium: false,
|
|
@@ -35,6 +36,6 @@ export function checkPremiumAccess(
|
|
|
35
36
|
userId: string | null,
|
|
36
37
|
isPremium: boolean,
|
|
37
38
|
): boolean {
|
|
38
|
-
if (isGuestFlag
|
|
39
|
+
if (isGuest(isGuestFlag, userId)) return false;
|
|
39
40
|
return isPremium;
|
|
40
41
|
}
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* useSubscription Utilities
|
|
3
|
-
* Shared utilities for subscription hook operations
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { getSubscriptionService } from "../../infrastructure/services/SubscriptionService";
|
|
7
|
-
|
|
8
|
-
export type AsyncSubscriptionOperation<T> = () => Promise<T>;
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Result of a subscription service initialization check
|
|
12
|
-
*/
|
|
13
|
-
export interface ServiceCheckResult {
|
|
14
|
-
success: boolean;
|
|
15
|
-
service: ReturnType<typeof getSubscriptionService> | null;
|
|
16
|
-
error?: string;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Checks if subscription service is initialized
|
|
21
|
-
* Returns service instance or error
|
|
22
|
-
*/
|
|
23
|
-
export function checkSubscriptionService(): ServiceCheckResult {
|
|
24
|
-
const service = getSubscriptionService();
|
|
25
|
-
|
|
26
|
-
if (!service) {
|
|
27
|
-
return {
|
|
28
|
-
success: false,
|
|
29
|
-
service: null,
|
|
30
|
-
error: "Subscription service is not initialized",
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return { success: true, service, error: undefined };
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Validates user ID
|
|
39
|
-
*/
|
|
40
|
-
export function validateUserId(userId: string): string | null {
|
|
41
|
-
if (!userId) {
|
|
42
|
-
return "User ID is required";
|
|
43
|
-
}
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Wraps async subscription operations with loading state, error handling, and state updates
|
|
49
|
-
*/
|
|
50
|
-
export async function executeSubscriptionOperation<T>(
|
|
51
|
-
operation: AsyncSubscriptionOperation<T>,
|
|
52
|
-
setLoading: (loading: boolean) => void,
|
|
53
|
-
setError: (error: string | null) => void,
|
|
54
|
-
onSuccess?: (result: T) => void
|
|
55
|
-
): Promise<void> {
|
|
56
|
-
setLoading(true);
|
|
57
|
-
setError(null);
|
|
58
|
-
|
|
59
|
-
try {
|
|
60
|
-
const result = await operation();
|
|
61
|
-
if (onSuccess) {
|
|
62
|
-
onSuccess(result);
|
|
63
|
-
}
|
|
64
|
-
} catch (err) {
|
|
65
|
-
const errorMessage =
|
|
66
|
-
err instanceof Error ? err.message : "Operation failed";
|
|
67
|
-
setError(errorMessage);
|
|
68
|
-
throw err;
|
|
69
|
-
} finally {
|
|
70
|
-
setLoading(false);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Formats error message from unknown error
|
|
76
|
-
*/
|
|
77
|
-
export function formatErrorMessage(err: unknown, fallbackMessage: string): string {
|
|
78
|
-
return err instanceof Error ? err.message : fallbackMessage;
|
|
79
|
-
}
|