@umituz/react-native-subscription 2.36.3 → 2.37.0
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/RefundCreditsCommand.ts +76 -0
- package/src/domains/credits/infrastructure/CreditsRepository.ts +6 -0
- package/src/domains/credits/presentation/deduct-credit/types.ts +1 -0
- package/src/domains/credits/presentation/deduct-credit/useDeductCredit.ts +21 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-subscription",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.37.0",
|
|
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",
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { runTransaction, serverTimestamp, type Transaction, type DocumentReference, type Firestore } from "@umituz/react-native-firebase";
|
|
2
|
+
import type { DeductCreditsResult } from "../core/Credits";
|
|
3
|
+
import { CREDIT_ERROR_CODES } from "../core/CreditsConstants";
|
|
4
|
+
import { subscriptionEventBus, SUBSCRIPTION_EVENTS } from "../../../shared/infrastructure/SubscriptionEventBus";
|
|
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
|
+
export async function refundCreditsOperation(
|
|
11
|
+
_db: Firestore,
|
|
12
|
+
creditsRef: DocumentReference,
|
|
13
|
+
amount: number,
|
|
14
|
+
userId: string
|
|
15
|
+
): Promise<DeductCreditsResult> {
|
|
16
|
+
if (!userId || userId.trim().length === 0) {
|
|
17
|
+
return {
|
|
18
|
+
success: false,
|
|
19
|
+
remainingCredits: null,
|
|
20
|
+
error: {
|
|
21
|
+
message: 'Valid userId is required for credit refund',
|
|
22
|
+
code: 'INVALID_USER'
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (amount <= 0) {
|
|
28
|
+
return {
|
|
29
|
+
success: false,
|
|
30
|
+
remainingCredits: null,
|
|
31
|
+
error: {
|
|
32
|
+
message: 'Refund amount must be positive',
|
|
33
|
+
code: 'INVALID_AMOUNT'
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const remaining = await runTransaction(async (tx: Transaction) => {
|
|
40
|
+
const docSnap = await tx.get(creditsRef);
|
|
41
|
+
|
|
42
|
+
if (!docSnap.exists()) {
|
|
43
|
+
throw new Error(CREDIT_ERROR_CODES.NO_CREDITS);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const current = docSnap.data().credits as number;
|
|
47
|
+
const updated = current + amount;
|
|
48
|
+
|
|
49
|
+
tx.update(creditsRef, {
|
|
50
|
+
credits: updated,
|
|
51
|
+
lastUpdatedAt: serverTimestamp()
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
return updated;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
subscriptionEventBus.emit(SUBSCRIPTION_EVENTS.CREDITS_UPDATED, userId);
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
success: true,
|
|
61
|
+
remainingCredits: remaining,
|
|
62
|
+
error: null
|
|
63
|
+
};
|
|
64
|
+
} catch (e: unknown) {
|
|
65
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
66
|
+
const code = message === CREDIT_ERROR_CODES.NO_CREDITS
|
|
67
|
+
? message
|
|
68
|
+
: 'REFUND_ERROR';
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
success: false,
|
|
72
|
+
remainingCredits: null,
|
|
73
|
+
error: { message, code }
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -4,6 +4,7 @@ import type { CreditsConfig, CreditsResult, DeductCreditsResult } from "../core/
|
|
|
4
4
|
import type { PurchaseSource } from "../core/UserCreditsDocument";
|
|
5
5
|
import type { RevenueCatData } from "../../revenuecat/core/types";
|
|
6
6
|
import { deductCreditsOperation } from "../application/DeductCreditsCommand";
|
|
7
|
+
import { refundCreditsOperation } from "../application/RefundCreditsCommand";
|
|
7
8
|
import { PURCHASE_TYPE, type PurchaseType } from "../../subscription/core/SubscriptionConstants";
|
|
8
9
|
import { requireFirestore, buildDocRef, type CollectionConfig } from "../../../shared/infrastructure/firestore";
|
|
9
10
|
import { fetchCredits, checkHasCredits } from "./operations/CreditsFetcher";
|
|
@@ -59,6 +60,11 @@ export class CreditsRepository extends BaseRepository {
|
|
|
59
60
|
return deductCreditsOperation(db, this.getRef(db, userId), cost, userId);
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
async refundCredit(userId: string, amount: number): Promise<DeductCreditsResult> {
|
|
64
|
+
const db = requireFirestore();
|
|
65
|
+
return refundCreditsOperation(db, this.getRef(db, userId), amount, userId);
|
|
66
|
+
}
|
|
67
|
+
|
|
62
68
|
async hasCredits(userId: string, cost: number): Promise<boolean> {
|
|
63
69
|
const db = requireFirestore();
|
|
64
70
|
return checkHasCredits(this.getRef(db, userId), cost);
|
|
@@ -7,5 +7,6 @@ export interface UseDeductCreditResult {
|
|
|
7
7
|
checkCredits: (cost?: number) => Promise<boolean>;
|
|
8
8
|
deductCredit: (cost?: number) => Promise<boolean>;
|
|
9
9
|
deductCredits: (cost: number) => Promise<boolean>;
|
|
10
|
+
refundCredits: (amount: number) => Promise<boolean>;
|
|
10
11
|
isDeducting: boolean;
|
|
11
12
|
}
|
|
@@ -45,10 +45,31 @@ export const useDeductCredit = ({
|
|
|
45
45
|
return repository.hasCredits(userId, cost);
|
|
46
46
|
}, [userId, repository]);
|
|
47
47
|
|
|
48
|
+
const refundCredits = useCallback(async (amount: number): Promise<boolean> => {
|
|
49
|
+
if (!userId) return false;
|
|
50
|
+
try {
|
|
51
|
+
const result = await repository.refundCredit(userId, amount);
|
|
52
|
+
if (result.success) {
|
|
53
|
+
// Invalidate queries to refresh credit balance
|
|
54
|
+
await queryClient.invalidateQueries({ queryKey: ['credits', userId] });
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error('[useDeductCredit] Unexpected error during credit refund', {
|
|
60
|
+
amount,
|
|
61
|
+
userId,
|
|
62
|
+
error
|
|
63
|
+
});
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}, [userId, repository, queryClient]);
|
|
67
|
+
|
|
48
68
|
return {
|
|
49
69
|
checkCredits,
|
|
50
70
|
deductCredit,
|
|
51
71
|
deductCredits,
|
|
72
|
+
refundCredits,
|
|
52
73
|
isDeducting: mutation.isPending
|
|
53
74
|
};
|
|
54
75
|
};
|