Skip to content

Commit 3ba91d0

Browse files
committed
fix(#65): add LoadingState to stores and standardize async error handling
- supportStore: add loadingState field, wrap createTicket with loading/success/failure states - fraudStore: add loadingState field, wrap refreshFraudSignals with loading/success/failure states - settingsStore: add loadingState field, wrap updateExchangeRates with loading/success/failure states; keep isLoading for back-compat - invoiceStore: import LoadingState type ready for store actions
1 parent 03e7e83 commit 3ba91d0

4 files changed

Lines changed: 54 additions & 29 deletions

File tree

src/store/fraudStore.ts

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { create } from 'zustand';
22
import { persist, createJSONStorage } from 'zustand/middleware';
33
import AsyncStorage from '@react-native-async-storage/async-storage';
4+
import { LoadingState, idle, loading, success, failure } from '../types/loadingState';
45
import {
56
FraudAction,
67
FraudAnalytics,
@@ -244,6 +245,7 @@ interface FraudState {
244245
analytics: FraudAnalytics;
245246
loading: boolean;
246247
error: string | null;
248+
loadingState: LoadingState;
247249
refreshFraudSignals: () => void;
248250
assessRisk: (subscriberId: string) => FraudRiskScore[];
249251
flagSubscription: (subscriptionId: string) => void;
@@ -301,26 +303,34 @@ export const useFraudStore = create<FraudState>()(
301303
reviewQueue: hydrateReviewQueue(reviewSeeds),
302304
analytics: computeAnalytics(subscriptionSeeds, reviewSeeds),
303305
loading: false,
306+
loadingState: idle(),
304307
error: null,
305308

306309
refreshFraudSignals: () => {
307-
const { subscriptions, reviewQueue, merchants } = get();
308-
set({
309-
analytics: computeAnalytics(subscriptions, reviewQueue),
310-
assessments: hydrateAssessments(subscriptions),
311-
merchants: merchants.map((merchant) => ({
312-
...merchant,
313-
averageRisk: buildMerchantReport(merchants, subscriptions, reviewQueue, merchant.id).averageRisk,
314-
blockedSubscriptions: buildMerchantReport(merchants, subscriptions, reviewQueue, merchant.id).blockedSubscriptions,
315-
activeSubscriptions: buildMerchantReport(merchants, subscriptions, reviewQueue, merchant.id).totalSubscriptions,
316-
status:
317-
buildMerchantReport(merchants, subscriptions, reviewQueue, merchant.id).averageRisk >= 60
318-
? 'high-risk'
319-
: buildMerchantReport(merchants, subscriptions, reviewQueue, merchant.id).averageRisk >= 35
320-
? 'watch'
321-
: 'healthy',
322-
})),
323-
});
310+
set({ loading: true, loadingState: loading() });
311+
try {
312+
const { subscriptions, reviewQueue, merchants } = get();
313+
set({
314+
analytics: computeAnalytics(subscriptions, reviewQueue),
315+
assessments: hydrateAssessments(subscriptions),
316+
merchants: merchants.map((merchant) => ({
317+
...merchant,
318+
averageRisk: buildMerchantReport(merchants, subscriptions, reviewQueue, merchant.id).averageRisk,
319+
blockedSubscriptions: buildMerchantReport(merchants, subscriptions, reviewQueue, merchant.id).blockedSubscriptions,
320+
activeSubscriptions: buildMerchantReport(merchants, subscriptions, reviewQueue, merchant.id).totalSubscriptions,
321+
status:
322+
buildMerchantReport(merchants, subscriptions, reviewQueue, merchant.id).averageRisk >= 60
323+
? 'high-risk'
324+
: buildMerchantReport(merchants, subscriptions, reviewQueue, merchant.id).averageRisk >= 35
325+
? 'watch'
326+
: 'healthy',
327+
})),
328+
loading: false,
329+
loadingState: success(),
330+
});
331+
} catch (e) {
332+
set({ loading: false, loadingState: failure(e as Error) });
333+
}
324334
},
325335

326336
assessRisk: (subscriberId: string) => {

src/store/invoiceStore.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
import { buildInvoice, calculateInvoiceTotals } from '../utils/invoice';
2424
import { CACHE_CONSTANTS } from '../utils/constants/values';
2525
import { errorHandler, AppError } from '../services/errorHandler';
26+
import { LoadingState, idle, loading, success, failure } from '../types/loadingState';
2627
import { presentLocalNotification } from '../services/notificationService';
2728

2829
const STORAGE_KEY = 'subtrackr-invoices';

src/store/settingsStore.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@ import { create } from 'zustand';
22
import { persist, createJSONStorage } from 'zustand/middleware';
33
import AsyncStorage from '@react-native-async-storage/async-storage';
44
import { currencyService, ExchangeRates } from '../services/currencyService';
5+
import { LoadingState, idle, loading, success, failure } from '../types/loadingState';
56

67
interface SettingsState {
78
preferredCurrency: string;
89
notificationsEnabled: boolean;
910
exchangeRates: ExchangeRates | null;
1011
isLoading: boolean;
11-
12+
loadingState: LoadingState;
13+
1214
// Actions
1315
setPreferredCurrency: (currency: string) => void;
1416
setNotificationsEnabled: (enabled: boolean) => void;
@@ -23,20 +25,23 @@ export const useSettingsStore = create<SettingsState>()(
2325
notificationsEnabled: true,
2426
exchangeRates: null,
2527
isLoading: false,
28+
loadingState: idle(),
2629

2730
setPreferredCurrency: (currency) => {
2831
set({ preferredCurrency: currency });
29-
// Optionally update rates immediately if base changed,
30-
// but here we keep USD as base for rates to simplify conversion
3132
void get().updateExchangeRates();
3233
},
3334

3435
setNotificationsEnabled: (enabled) => set({ notificationsEnabled: enabled }),
3536

3637
updateExchangeRates: async () => {
37-
set({ isLoading: true });
38-
const rates = await currencyService.fetchRates('USD');
39-
set({ exchangeRates: rates, isLoading: false });
38+
set({ isLoading: true, loadingState: loading() });
39+
try {
40+
const rates = await currencyService.fetchRates('USD');
41+
set({ exchangeRates: rates, isLoading: false, loadingState: success() });
42+
} catch (e) {
43+
set({ isLoading: false, loadingState: failure(e as Error, ['Check your internet connection', 'Try again later']) });
44+
}
4045
},
4146

4247
initializeSettings: async () => {

src/store/supportStore.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ import {
1111
TicketingIntegrationConfig,
1212
TicketStatus,
1313
} from '../types/support';
14+
import { LoadingState, idle, loading, success, failure } from '../types/loadingState';
1415

1516
interface SupportState {
1617
tickets: SupportTicket[];
1718
integration: TicketingIntegrationConfig;
19+
loadingState: LoadingState;
1820
createTicket: (event: SubscriptionSupportEvent) => SupportTicket;
1921
assignTicket: (ticketId: string, assignee: string) => void;
2022
updateTicketStatus: (ticketId: string, status: TicketStatus) => void;
@@ -32,14 +34,21 @@ const mapTicket = (
3234
export const useSupportStore = create<SupportState>((set, get) => ({
3335
tickets: [],
3436
integration: { provider: 'internal', enabled: true, defaultAssignee: 'support-team' },
37+
loadingState: idle(),
3538

3639
createTicket: (event) => {
37-
const relatedTicketIds = get()
38-
.tickets.filter((ticket) => ticket.subscriptionId === event.subscriptionId && ticket.status !== 'closed')
39-
.map((ticket) => ticket.id);
40-
const ticket = createTicketFromEvent(event, relatedTicketIds);
41-
set((state) => ({ tickets: [...state.tickets, ticket] }));
42-
return ticket;
40+
set({ loadingState: loading() });
41+
try {
42+
const relatedTicketIds = get()
43+
.tickets.filter((ticket) => ticket.subscriptionId === event.subscriptionId && ticket.status !== 'closed')
44+
.map((ticket) => ticket.id);
45+
const ticket = createTicketFromEvent(event, relatedTicketIds);
46+
set((state) => ({ tickets: [...state.tickets, ticket], loadingState: success() }));
47+
return ticket;
48+
} catch (e) {
49+
set({ loadingState: failure(e as Error) });
50+
throw e;
51+
}
4352
},
4453

4554
assignTicket: (ticketId, assignee) =>

0 commit comments

Comments
 (0)