@umituz/react-native-subscription 2.14.98 → 2.14.99
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/README.md +0 -1
- package/package.json +1 -1
- package/src/domains/README.md +240 -0
- package/src/domains/config/domain/README.md +390 -0
- package/src/domains/config/domain/entities/README.md +350 -0
- package/src/presentation/hooks/useDeductCredit.md +146 -130
- package/src/revenuecat/application/README.md +158 -0
- package/src/revenuecat/application/ports/README.md +169 -0
- package/src/revenuecat/domain/constants/README.md +183 -0
- package/src/revenuecat/domain/entities/README.md +382 -0
- package/src/revenuecat/domain/types/README.md +373 -0
- package/src/revenuecat/domain/value-objects/README.md +441 -0
- package/src/revenuecat/infrastructure/README.md +50 -0
- package/src/revenuecat/infrastructure/handlers/README.md +218 -0
- package/src/revenuecat/infrastructure/services/README.md +325 -0
- package/src/revenuecat/infrastructure/utils/README.md +382 -0
- package/src/revenuecat/presentation/README.md +184 -0
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
# Config Domain Entities
|
|
2
|
+
|
|
3
|
+
Domain entities for configuration management.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
This directory contains entity classes representing configuration concepts like packages, features, and paywalls.
|
|
8
|
+
|
|
9
|
+
## Entities
|
|
10
|
+
|
|
11
|
+
### PackageConfig
|
|
12
|
+
|
|
13
|
+
Represents a subscription package configuration.
|
|
14
|
+
|
|
15
|
+
**File**: `PackageConfig.ts`
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
class PackageConfig {
|
|
19
|
+
readonly identifier: string;
|
|
20
|
+
readonly productId: string;
|
|
21
|
+
readonly period: PackagePeriod;
|
|
22
|
+
readonly price: Money;
|
|
23
|
+
readonly features: string[];
|
|
24
|
+
readonly credits?: number;
|
|
25
|
+
readonly metadata: PackageMetadata;
|
|
26
|
+
|
|
27
|
+
// Methods
|
|
28
|
+
isAnnual(): boolean;
|
|
29
|
+
isMonthly(): boolean;
|
|
30
|
+
isLifetime(): boolean;
|
|
31
|
+
hasCredits(): boolean;
|
|
32
|
+
isRecommended(): boolean;
|
|
33
|
+
isHighlighted(): boolean;
|
|
34
|
+
getPerMonthPrice(): Money | null;
|
|
35
|
+
getDiscountPercentage(): number | null;
|
|
36
|
+
getBadge(): string | null;
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
**Usage:**
|
|
41
|
+
```typescript
|
|
42
|
+
const pkg = new PackageConfig({
|
|
43
|
+
identifier: 'premium_annual',
|
|
44
|
+
productId: 'com.app.premium.annual',
|
|
45
|
+
period: 'annual',
|
|
46
|
+
price: 79.99,
|
|
47
|
+
currency: 'USD',
|
|
48
|
+
features: ['Unlimited Access', 'Ad-Free'],
|
|
49
|
+
credits: 1200,
|
|
50
|
+
metadata: {
|
|
51
|
+
recommended: true,
|
|
52
|
+
badge: 'Best Value',
|
|
53
|
+
discount: { percentage: 20, description: 'Save 20%' },
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
console.log(pkg.isAnnual()); // true
|
|
58
|
+
console.log(pkg.getPerMonthPrice()?.format()); // '$6.67'
|
|
59
|
+
console.log(pkg.getDiscountPercentage()); // 20
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### FeatureConfig
|
|
63
|
+
|
|
64
|
+
Represents a feature configuration with gating rules.
|
|
65
|
+
|
|
66
|
+
**File**: `FeatureConfig.ts`
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
class FeatureConfig {
|
|
70
|
+
readonly id: string;
|
|
71
|
+
readonly name: string;
|
|
72
|
+
readonly description?: string;
|
|
73
|
+
readonly requiresPremium: boolean;
|
|
74
|
+
readonly requiresCredits: boolean;
|
|
75
|
+
readonly creditCost?: number;
|
|
76
|
+
readonly enabled: boolean;
|
|
77
|
+
readonly gateType: 'premium' | 'credits' | 'both';
|
|
78
|
+
|
|
79
|
+
// Methods
|
|
80
|
+
isAccessible(userHasPremium: boolean, userCredits: number): boolean;
|
|
81
|
+
getRequiredCredits(): number;
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Usage:**
|
|
86
|
+
```typescript
|
|
87
|
+
const feature = new FeatureConfig({
|
|
88
|
+
id: 'ai_generation',
|
|
89
|
+
name: 'AI Generation',
|
|
90
|
+
requiresPremium: false,
|
|
91
|
+
requiresCredits: true,
|
|
92
|
+
creditCost: 5,
|
|
93
|
+
enabled: true,
|
|
94
|
+
gateType: 'credits',
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
console.log(feature.isAccessible(false, 10)); // true
|
|
98
|
+
console.log(feature.isAccessible(false, 3)); // false
|
|
99
|
+
console.log(feature.getRequiredCredits()); // 5
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### PaywallConfig
|
|
103
|
+
|
|
104
|
+
Represents a paywall screen configuration.
|
|
105
|
+
|
|
106
|
+
**File**: `PaywallConfig.ts`
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
class PaywallConfig {
|
|
110
|
+
readonly id: string;
|
|
111
|
+
readonly title: string;
|
|
112
|
+
readonly subtitle?: string;
|
|
113
|
+
readonly features: string[];
|
|
114
|
+
readonly packages: PackageConfig[];
|
|
115
|
+
readonly highlightPackage?: string;
|
|
116
|
+
readonly style: PaywallStyle;
|
|
117
|
+
readonly triggers: PaywallTrigger[];
|
|
118
|
+
|
|
119
|
+
// Methods
|
|
120
|
+
getHighlightedPackage(): PackageConfig | null;
|
|
121
|
+
getPackageByIdentifier(identifier: string): PackageConfig | null;
|
|
122
|
+
shouldTrigger(triggerType: string): boolean;
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**Usage:**
|
|
127
|
+
```typescript
|
|
128
|
+
const paywall = new PaywallConfig({
|
|
129
|
+
id: 'premium_upgrade',
|
|
130
|
+
title: 'Upgrade to Premium',
|
|
131
|
+
features: ['Unlimited Access', 'Ad-Free'],
|
|
132
|
+
packages: [monthlyPkg, annualPkg],
|
|
133
|
+
highlightPackage: 'premium_annual',
|
|
134
|
+
style: {
|
|
135
|
+
primaryColor: '#007AFF',
|
|
136
|
+
backgroundColor: '#FFFFFF',
|
|
137
|
+
},
|
|
138
|
+
triggers: [{ type: 'credit_gate', enabled: true }],
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const highlighted = paywall.getHighlightedPackage();
|
|
142
|
+
console.log(highlighted?.identifier); // 'premium_annual'
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### SubscriptionSettingsConfig
|
|
146
|
+
|
|
147
|
+
Represents subscription settings configuration.
|
|
148
|
+
|
|
149
|
+
**File**: `SubscriptionSettingsConfig.ts`
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
class SubscriptionSettingsConfig {
|
|
153
|
+
readonly showSubscriptionDetails: boolean;
|
|
154
|
+
readonly showCreditBalance: boolean;
|
|
155
|
+
readonly allowRestorePurchases: boolean;
|
|
156
|
+
readonly showManageSubscriptionButton: boolean;
|
|
157
|
+
readonly subscriptionManagementURL: string;
|
|
158
|
+
readonly supportEmail: string;
|
|
159
|
+
|
|
160
|
+
// Methods
|
|
161
|
+
getAvailableActions(): string[];
|
|
162
|
+
isRestoreAllowed(): boolean;
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
**Usage:**
|
|
167
|
+
```typescript
|
|
168
|
+
const settings = new SubscriptionSettingsConfig({
|
|
169
|
+
showSubscriptionDetails: true,
|
|
170
|
+
showCreditBalance: true,
|
|
171
|
+
allowRestorePurchases: true,
|
|
172
|
+
showManageSubscriptionButton: true,
|
|
173
|
+
subscriptionManagementURL: 'https://apps.apple.com/account/subscriptions',
|
|
174
|
+
supportEmail: 'support@example.com',
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
console.log(settings.isRestoreAllowed()); // true
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Supporting Types
|
|
181
|
+
|
|
182
|
+
### PackageMetadata
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
interface PackageMetadata {
|
|
186
|
+
highlight?: boolean;
|
|
187
|
+
recommended?: boolean;
|
|
188
|
+
discount?: {
|
|
189
|
+
percentage: number;
|
|
190
|
+
description: string;
|
|
191
|
+
};
|
|
192
|
+
badge?: string;
|
|
193
|
+
}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### PaywallStyle
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
interface PaywallStyle {
|
|
200
|
+
primaryColor: string;
|
|
201
|
+
backgroundColor: string;
|
|
202
|
+
textColor?: string;
|
|
203
|
+
image?: string;
|
|
204
|
+
logo?: string;
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### PaywallTrigger
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
interface PaywallTrigger {
|
|
212
|
+
type: string;
|
|
213
|
+
enabled: boolean;
|
|
214
|
+
conditions?: Record<string, unknown>;
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Factory Functions
|
|
219
|
+
|
|
220
|
+
### createDefaultPackages
|
|
221
|
+
|
|
222
|
+
Create default package configurations.
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
function createDefaultPackages(): PackageConfig[] {
|
|
226
|
+
return [
|
|
227
|
+
new PackageConfig({
|
|
228
|
+
identifier: 'premium_monthly',
|
|
229
|
+
productId: 'com.app.premium.monthly',
|
|
230
|
+
period: 'monthly',
|
|
231
|
+
price: 9.99,
|
|
232
|
+
currency: 'USD',
|
|
233
|
+
features: ['Unlimited Access', 'Ad-Free'],
|
|
234
|
+
credits: 100,
|
|
235
|
+
}),
|
|
236
|
+
new PackageConfig({
|
|
237
|
+
identifier: 'premium_annual',
|
|
238
|
+
productId: 'com.app.premium.annual',
|
|
239
|
+
period: 'annual',
|
|
240
|
+
price: 79.99,
|
|
241
|
+
currency: 'USD',
|
|
242
|
+
features: ['Unlimited Access', 'Ad-Free', 'Save 20%'],
|
|
243
|
+
credits: 1200,
|
|
244
|
+
metadata: {
|
|
245
|
+
recommended: true,
|
|
246
|
+
badge: 'Best Value',
|
|
247
|
+
discount: { percentage: 20, description: 'Save 20%' },
|
|
248
|
+
},
|
|
249
|
+
}),
|
|
250
|
+
];
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
### createDefaultPaywall
|
|
255
|
+
|
|
256
|
+
Create default paywall configuration.
|
|
257
|
+
|
|
258
|
+
```typescript
|
|
259
|
+
function createDefaultPaywall(): PaywallConfig {
|
|
260
|
+
return new PaywallConfig({
|
|
261
|
+
id: 'default_paywall',
|
|
262
|
+
title: 'Upgrade to Premium',
|
|
263
|
+
subtitle: 'Get unlimited access to all features',
|
|
264
|
+
features: [
|
|
265
|
+
'Unlimited Access',
|
|
266
|
+
'Ad-Free Experience',
|
|
267
|
+
'Priority Support',
|
|
268
|
+
'Exclusive Features',
|
|
269
|
+
],
|
|
270
|
+
packages: createDefaultPackages(),
|
|
271
|
+
highlightPackage: 'premium_annual',
|
|
272
|
+
style: {
|
|
273
|
+
primaryColor: '#007AFF',
|
|
274
|
+
backgroundColor: '#FFFFFF',
|
|
275
|
+
},
|
|
276
|
+
triggers: [
|
|
277
|
+
{ type: 'premium_feature', enabled: true },
|
|
278
|
+
{ type: 'credit_gate', enabled: true },
|
|
279
|
+
],
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## Usage Examples
|
|
285
|
+
|
|
286
|
+
### Validating Package Configuration
|
|
287
|
+
|
|
288
|
+
```typescript
|
|
289
|
+
function validatePackage(config: PackageConfigData): boolean {
|
|
290
|
+
try {
|
|
291
|
+
new PackageConfig(config);
|
|
292
|
+
return true;
|
|
293
|
+
} catch (error) {
|
|
294
|
+
console.error('Invalid package config:', error.message);
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### Finding Recommended Package
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
function findRecommendedPackage(packages: PackageConfig[]): PackageConfig | null {
|
|
304
|
+
return packages.find(pkg => pkg.isRecommended()) ?? null;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const recommended = findRecommendedPackage(packages);
|
|
308
|
+
console.log('Recommended:', recommended?.identifier);
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### Filtering Packages by Period
|
|
312
|
+
|
|
313
|
+
```typescript
|
|
314
|
+
function getPackagesByPeriod(packages: PackageConfig[], period: 'monthly' | 'annual'): PackageConfig[] {
|
|
315
|
+
return packages.filter(pkg => {
|
|
316
|
+
if (period === 'monthly') return pkg.isMonthly();
|
|
317
|
+
if (period === 'annual') return pkg.isAnnual();
|
|
318
|
+
return false;
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const monthlyPackages = getPackagesByPeriod(packages, 'monthly');
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### Checking Feature Access
|
|
326
|
+
|
|
327
|
+
```typescript
|
|
328
|
+
function canUserAccessFeature(feature: FeatureConfig, user: User): boolean {
|
|
329
|
+
return feature.isAccessible(user.isPremium, user.credits);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const aiFeature = new FeatureConfig({ /* ... */ });
|
|
333
|
+
const canAccess = canUserAccessFeature(aiFeature, currentUser);
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
## Best Practices
|
|
337
|
+
|
|
338
|
+
1. **Validation**: Always validate configuration in constructor
|
|
339
|
+
2. **Immutability**: Never modify entities after creation
|
|
340
|
+
3. **Business Logic**: Keep business logic in entities
|
|
341
|
+
4. **Type Safety**: Use TypeScript strictly
|
|
342
|
+
5. **Error Messages**: Provide clear error messages
|
|
343
|
+
6. **Defaults**: Provide factory functions for defaults
|
|
344
|
+
7. **Testing**: Test validation logic thoroughly
|
|
345
|
+
|
|
346
|
+
## Related
|
|
347
|
+
|
|
348
|
+
- [Config Domain](../README.md)
|
|
349
|
+
- [Config Value Objects](../value-objects/README.md)
|
|
350
|
+
- [Config Utils](../../utils/README.md)
|
|
@@ -2,159 +2,175 @@
|
|
|
2
2
|
|
|
3
3
|
Hook for deducting credits from user balance with optimistic updates.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Location
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
import { useDeductCredit } from '@umituz/react-native-subscription';
|
|
9
|
-
```
|
|
7
|
+
**Import Path**: `@umituz/react-native-subscription`
|
|
10
8
|
|
|
11
|
-
|
|
9
|
+
**File**: `src/presentation/hooks/useDeductCredit.ts`
|
|
12
10
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
11
|
+
## Strategy
|
|
12
|
+
|
|
13
|
+
### Credit Deduction Flow
|
|
14
|
+
|
|
15
|
+
1. **Pre-deduction Validation**
|
|
16
|
+
- Verify user is authenticated (userId must be defined)
|
|
17
|
+
- Check if sufficient credits exist
|
|
18
|
+
- Validate deduction amount is positive
|
|
19
|
+
|
|
20
|
+
2. **Optimistic Update**
|
|
21
|
+
- Immediately update UI with new balance
|
|
22
|
+
- Store previous state for potential rollback
|
|
23
|
+
- Update TanStack Query cache
|
|
24
|
+
|
|
25
|
+
3. **Server Synchronization**
|
|
26
|
+
- Send deduction request to backend
|
|
27
|
+
- Handle success/failure responses
|
|
28
|
+
- Rollback on failure
|
|
29
|
+
|
|
30
|
+
4. **Post-deduction Handling**
|
|
31
|
+
- Trigger `onCreditsExhausted` callback if balance reaches zero
|
|
32
|
+
- Return success/failure boolean
|
|
33
|
+
- Reset loading state
|
|
34
|
+
|
|
35
|
+
### Integration Points
|
|
36
|
+
|
|
37
|
+
- **Credits Repository**: `src/domains/wallet/infrastructure/repositories/CreditsRepository.ts`
|
|
38
|
+
- **Credits Entity**: `src/domains/wallet/domain/entities/UserCredits.ts`
|
|
39
|
+
- **TanStack Query**: For cache management and optimistic updates
|
|
40
|
+
|
|
41
|
+
## Restrictions
|
|
42
|
+
|
|
43
|
+
### REQUIRED
|
|
23
44
|
|
|
24
|
-
|
|
45
|
+
- **User Authentication**: `userId` parameter MUST be provided and cannot be undefined
|
|
46
|
+
- **Positive Amount**: Credit cost MUST be greater than zero
|
|
47
|
+
- **Callback Implementation**: `onCreditsExhausted` callback SHOULD be implemented to handle zero balance scenarios
|
|
25
48
|
|
|
26
|
-
|
|
27
|
-
|-----------|------|---------|-------------|
|
|
28
|
-
| `userId` | `string \| undefined` | **Required** | User ID for credit deduction |
|
|
29
|
-
| `onCreditsExhausted` | `() => void` | `undefined` | Callback when credits are exhausted |
|
|
49
|
+
### PROHIBITED
|
|
30
50
|
|
|
31
|
-
|
|
51
|
+
- **NEVER** call `deductCredit` or `deductCredits` without checking `isDeducting` state first
|
|
52
|
+
- **NEVER** allow multiple simultaneous deduction calls for the same user
|
|
53
|
+
- **NEVER** deduce credits when balance is insufficient (should check with `useCreditChecker` first)
|
|
54
|
+
- **DO NOT** use this hook for guest users (userId undefined)
|
|
32
55
|
|
|
33
|
-
|
|
34
|
-
|----------|------|-------------|
|
|
35
|
-
| `deductCredit` | `(cost?: number) => Promise<boolean>` | Deduct credits (defaults to 1) |
|
|
36
|
-
| `deductCredits` | `(cost: number) => Promise<boolean>` | Deduct specific amount |
|
|
37
|
-
| `isDeducting` | `boolean` | Mutation is in progress |
|
|
56
|
+
### CRITICAL SAFETY
|
|
38
57
|
|
|
39
|
-
|
|
58
|
+
- **ALWAYS** check return value before proceeding with feature execution
|
|
59
|
+
- **ALWAYS** handle the case where deduction returns `false`
|
|
60
|
+
- **NEVER** assume deduction succeeded without checking return value
|
|
61
|
+
- **MUST** implement error boundaries when using this hook
|
|
62
|
+
|
|
63
|
+
## Rules
|
|
64
|
+
|
|
65
|
+
### Initialization
|
|
40
66
|
|
|
41
67
|
```typescript
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
return (
|
|
59
|
-
<Button onPress={handleUseFeature} disabled={isDeducting}>
|
|
60
|
-
Use Feature (1 Credit)
|
|
61
|
-
</Button>
|
|
62
|
-
);
|
|
63
|
-
}
|
|
68
|
+
// CORRECT
|
|
69
|
+
const { deductCredit, isDeducting } = useDeductCredit({
|
|
70
|
+
userId: user?.uid,
|
|
71
|
+
onCreditsExhausted: () => showPaywall(),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// INCORRECT - Missing callback
|
|
75
|
+
const { deductCredit } = useDeductCredit({
|
|
76
|
+
userId: user?.uid,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// INCORRECT - userId undefined
|
|
80
|
+
const { deductCredit } = useDeductCredit({
|
|
81
|
+
userId: undefined,
|
|
82
|
+
});
|
|
64
83
|
```
|
|
65
84
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
### With Custom Cost
|
|
85
|
+
### Deduction Execution
|
|
69
86
|
|
|
70
87
|
```typescript
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const features = {
|
|
77
|
-
basic: { cost: 1, name: 'Basic Generation' },
|
|
78
|
-
advanced: { cost: 5, name: 'Advanced Generation' },
|
|
79
|
-
premium: { cost: 10, name: 'Premium Generation' },
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
const handleFeature = async (cost: number) => {
|
|
83
|
-
const success = await deductCredit(cost);
|
|
84
|
-
if (success) {
|
|
85
|
-
console.log('Feature used');
|
|
86
|
-
}
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
return (
|
|
90
|
-
<View>
|
|
91
|
-
{Object.entries(features).map(([key, { cost, name }]) => (
|
|
92
|
-
<Button
|
|
93
|
-
key={key}
|
|
94
|
-
onPress={() => handleFeature(cost)}
|
|
95
|
-
title={`${name} (${cost} credits)`}
|
|
96
|
-
/>
|
|
97
|
-
))}
|
|
98
|
-
</View>
|
|
99
|
-
);
|
|
88
|
+
// CORRECT - Check return value
|
|
89
|
+
const success = await deductCredit(5);
|
|
90
|
+
if (success) {
|
|
91
|
+
executeFeature();
|
|
100
92
|
}
|
|
93
|
+
|
|
94
|
+
// INCORRECT - No return value check
|
|
95
|
+
await deductCredit(5);
|
|
96
|
+
executeFeature(); // May execute even if deduction failed
|
|
97
|
+
|
|
98
|
+
// INCORRECT - Multiple simultaneous calls
|
|
99
|
+
Promise.all([
|
|
100
|
+
deductCredit(5),
|
|
101
|
+
deductCredit(3), // Race condition!
|
|
102
|
+
]);
|
|
101
103
|
```
|
|
102
104
|
|
|
103
|
-
###
|
|
105
|
+
### Loading States
|
|
104
106
|
|
|
105
107
|
```typescript
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
[
|
|
116
|
-
{ text: 'Cancel', style: 'cancel' },
|
|
117
|
-
{
|
|
118
|
-
text: 'Confirm',
|
|
119
|
-
onPress: async () => {
|
|
120
|
-
const success = await deductCredit(cost);
|
|
121
|
-
if (success) {
|
|
122
|
-
await performAction(action);
|
|
123
|
-
}
|
|
124
|
-
},
|
|
125
|
-
},
|
|
126
|
-
]
|
|
127
|
-
);
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
return (
|
|
131
|
-
<Button
|
|
132
|
-
onPress={() => handleActionWithConfirmation(5, 'export')}
|
|
133
|
-
title="Export Data (5 Credits)"
|
|
134
|
-
/>
|
|
135
|
-
);
|
|
136
|
-
}
|
|
108
|
+
// CORRECT - Respect loading state
|
|
109
|
+
<Button onPress={handleDeduct} disabled={isDeducting}>
|
|
110
|
+
{isDeducting ? 'Deducting...' : 'Use Feature'}
|
|
111
|
+
</Button>
|
|
112
|
+
|
|
113
|
+
// INCORRECT - Ignore loading state
|
|
114
|
+
<Button onPress={handleDeduct}>
|
|
115
|
+
Use Feature
|
|
116
|
+
</Button>
|
|
137
117
|
```
|
|
138
118
|
|
|
139
|
-
|
|
119
|
+
### Credit Exhaustion Handling
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
// CORRECT - Implement callback
|
|
123
|
+
const { deductCredit } = useDeductCredit({
|
|
124
|
+
userId: user?.uid,
|
|
125
|
+
onCreditsExhausted: () => {
|
|
126
|
+
navigation.navigate('CreditPackages');
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// MINIMUM - Show alert
|
|
131
|
+
const { deductCredit } = useDeductCredit({
|
|
132
|
+
userId: user?.uid,
|
|
133
|
+
onCreditsExhausted: () => {
|
|
134
|
+
Alert.alert('No Credits', 'Please purchase more credits');
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## AI Agent Guidelines
|
|
140
|
+
|
|
141
|
+
### When Implementing Features
|
|
142
|
+
|
|
143
|
+
1. **Always** check if user has sufficient credits BEFORE allowing action
|
|
144
|
+
2. **Always** show the credit cost to user before deducting
|
|
145
|
+
3. **Always** disable buttons while `isDeducting` is true
|
|
146
|
+
4. **Always** handle the case where deduction returns false
|
|
147
|
+
5. **Never** allow zero or negative credit costs
|
|
148
|
+
|
|
149
|
+
### Integration Checklist
|
|
140
150
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
151
|
+
- [ ] Import from correct path: `@umituz/react-native-subscription`
|
|
152
|
+
- [ ] Provide valid `userId` from authentication context
|
|
153
|
+
- [ ] Implement `onCreditsExhausted` callback
|
|
154
|
+
- [ ] Check `isDeducting` before calling deduct functions
|
|
155
|
+
- [ ] Validate return value before proceeding
|
|
156
|
+
- [ ] Show credit cost to user before action
|
|
157
|
+
- [ ] Handle error cases gracefully
|
|
158
|
+
- [ ] Test with insufficient credits
|
|
159
|
+
- [ ] Test with zero balance
|
|
148
160
|
|
|
149
|
-
|
|
161
|
+
### Common Patterns to Implement
|
|
150
162
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
163
|
+
1. **Pre-check**: Use `useCreditChecker` before showing feature button
|
|
164
|
+
2. **Confirmation Dialog**: Ask user before expensive operations (>5 credits)
|
|
165
|
+
3. **Success Feedback**: Show success message after deduction
|
|
166
|
+
4. **Failure Handling**: Show appropriate error message on failure
|
|
167
|
+
5. **Purchase Flow**: Navigate to purchase screen on exhaustion
|
|
155
168
|
|
|
156
|
-
##
|
|
169
|
+
## Related Documentation
|
|
157
170
|
|
|
158
|
-
-
|
|
159
|
-
-
|
|
160
|
-
-
|
|
171
|
+
- **useCredits**: Access current credit balance
|
|
172
|
+
- **useCreditChecker**: Check credit availability before operations
|
|
173
|
+
- **useInitializeCredits**: Initialize credits after purchase
|
|
174
|
+
- **useFeatureGate**: Unified feature gating with credits
|
|
175
|
+
- **Credits Repository**: `src/domains/wallet/infrastructure/repositories/README.md`
|
|
176
|
+
- **Wallet Domain**: `src/domains/wallet/README.md`
|