@react-vault/create-app 0.1.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/LICENSE +12 -0
- package/README.md +16 -0
- package/bin/create-app.js +8 -0
- package/claude-toolkit/README.md +131 -0
- package/claude-toolkit/agents/bfsi-accessibility-auditor.md +132 -0
- package/claude-toolkit/agents/bfsi-architect.md +156 -0
- package/claude-toolkit/agents/bfsi-code-reviewer.md +137 -0
- package/claude-toolkit/agents/bfsi-compliance-auditor.md +161 -0
- package/claude-toolkit/agents/bfsi-pii-scanner.md +142 -0
- package/claude-toolkit/agents/bfsi-pr-reviewer.md +114 -0
- package/claude-toolkit/agents/bfsi-security-reviewer.md +136 -0
- package/claude-toolkit/commands/bfsi-audit.md +46 -0
- package/claude-toolkit/commands/bfsi-doctor.md +97 -0
- package/claude-toolkit/commands/bfsi-review.md +46 -0
- package/claude-toolkit/commands/bfsi-scaffold.md +47 -0
- package/claude-toolkit/hooks/hooks.json +181 -0
- package/claude-toolkit/hooks/scripts/a11y-check.sh +63 -0
- package/claude-toolkit/hooks/scripts/audit-prompt.sh +36 -0
- package/claude-toolkit/hooks/scripts/block-destructive.sh +41 -0
- package/claude-toolkit/hooks/scripts/block-force-push.sh +30 -0
- package/claude-toolkit/hooks/scripts/format.sh +42 -0
- package/claude-toolkit/hooks/scripts/inject-context.sh +44 -0
- package/claude-toolkit/hooks/scripts/lint.sh +45 -0
- package/claude-toolkit/hooks/scripts/protect-files.sh +53 -0
- package/claude-toolkit/hooks/scripts/save-compliance-context.sh +35 -0
- package/claude-toolkit/hooks/scripts/scan-pii.sh +87 -0
- package/claude-toolkit/hooks/scripts/scan-secrets.sh +67 -0
- package/claude-toolkit/hooks/scripts/verify-clean.sh +50 -0
- package/claude-toolkit/package.json +22 -0
- package/claude-toolkit/plugin.json +31 -0
- package/claude-toolkit/skills/bfsi-api-endpoint/SKILL.md +105 -0
- package/claude-toolkit/skills/bfsi-commit/SKILL.md +102 -0
- package/claude-toolkit/skills/bfsi-compliance-check/SKILL.md +107 -0
- package/claude-toolkit/skills/bfsi-encrypt-helper/SKILL.md +127 -0
- package/claude-toolkit/skills/bfsi-error-message/SKILL.md +162 -0
- package/claude-toolkit/skills/bfsi-feature/SKILL.md +120 -0
- package/claude-toolkit/skills/bfsi-feature/references/architecture.md +69 -0
- package/claude-toolkit/skills/bfsi-feature/references/audit-events.md +70 -0
- package/claude-toolkit/skills/bfsi-feature/scripts/scaffold.mjs +136 -0
- package/claude-toolkit/skills/bfsi-form/SKILL.md +73 -0
- package/claude-toolkit/skills/bfsi-form/references/validation-regex.md +50 -0
- package/claude-toolkit/skills/bfsi-onboarding/SKILL.md +110 -0
- package/claude-toolkit/skills/bfsi-pii-field/SKILL.md +90 -0
- package/claude-toolkit/skills/bfsi-test-pattern/SKILL.md +179 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +339 -0
- package/dist/index.js.map +1 -0
- package/package.json +69 -0
- package/templates/_shared/.claude/settings.json +31 -0
- package/templates/_shared/.env.local.sample +25 -0
- package/templates/_shared/.github/workflows/ci.yml +49 -0
- package/templates/_shared/CLAUDE.md +89 -0
- package/templates/_shared/README.md +50 -0
- package/templates/_shared/index.html +16 -0
- package/templates/_shared/package.json +73 -0
- package/templates/_shared/postcss.config.cjs +6 -0
- package/templates/_shared/src/app/App.tsx +13 -0
- package/templates/_shared/src/app/globals.css +64 -0
- package/templates/_shared/src/env.ts +33 -0
- package/templates/_shared/src/i18n/i18n.ts +18 -0
- package/templates/_shared/src/i18n/translations/en.json +54 -0
- package/templates/_shared/src/i18n/translations/hi.json +30 -0
- package/templates/_shared/src/main.tsx +16 -0
- package/templates/_shared/src/routes/ProtectedRoute.tsx +28 -0
- package/templates/_shared/src/routes/index.tsx +67 -0
- package/templates/_shared/src/shared/ErrorBoundary.tsx +60 -0
- package/templates/_shared/tailwind.config.ts +68 -0
- package/templates/_shared/tests/setup.ts +7 -0
- package/templates/_shared/tsconfig.json +33 -0
- package/templates/_shared/tsconfig.node.json +13 -0
- package/templates/_shared/vite.config.ts +47 -0
- package/templates/rtk-query/.claude/skills/axios-auth/SKILL.md +103 -0
- package/templates/rtk-query/.claude/skills/axios-auth/references/error-shape.md +84 -0
- package/templates/rtk-query/.claude/skills/axios-auth/references/full-code-walkthrough.md +146 -0
- package/templates/rtk-query/.claude/skills/axios-auth/references/notification-wiring.md +141 -0
- package/templates/rtk-query/.claude/skills/constants-organization/SKILL.md +112 -0
- package/templates/rtk-query/.claude/skills/constants-organization/references/example-files.md +134 -0
- package/templates/rtk-query/.claude/skills/constants-organization/references/tag-types-catalog.md +53 -0
- package/templates/rtk-query/.claude/skills/redux-store-integration/SKILL.md +159 -0
- package/templates/rtk-query/.claude/skills/redux-store-integration/references/localStorage-persistence.md +70 -0
- package/templates/rtk-query/.claude/skills/redux-store-integration/references/middleware-patterns.md +82 -0
- package/templates/rtk-query/.claude/skills/rtk-query-api/SKILL.md +148 -0
- package/templates/rtk-query/.claude/skills/rtk-query-api/references/cache-strategies.md +96 -0
- package/templates/rtk-query/.claude/skills/rtk-query-api/references/endpoint-cookbook.md +145 -0
- package/templates/rtk-query/.claude/skills/rtk-query-api/references/optimistic-update.md +53 -0
- package/templates/rtk-query/README.md +84 -0
- package/templates/rtk-query/package.partial.json +7 -0
- package/templates/rtk-query/src/app/App.tsx +23 -0
- package/templates/rtk-query/src/axiosconfig/axiosInstance.ts +26 -0
- package/templates/rtk-query/src/axiosconfig/baseQuery.ts +72 -0
- package/templates/rtk-query/src/axiosconfig/interceptor.ts +42 -0
- package/templates/rtk-query/src/redux/invalidateCacheMiddleware.ts +20 -0
- package/templates/rtk-query/src/redux/reduxHooks.ts +10 -0
- package/templates/rtk-query/src/redux/rootReducer.ts +18 -0
- package/templates/rtk-query/src/redux/store.ts +36 -0
- package/templates/tanstack-query/.claude/skills/axios-auth/SKILL.md +109 -0
- package/templates/tanstack-query/.claude/skills/axios-auth/references/error-shape.md +89 -0
- package/templates/tanstack-query/.claude/skills/axios-auth/references/full-code-walkthrough.md +121 -0
- package/templates/tanstack-query/.claude/skills/axios-auth/references/notification-pattern.md +109 -0
- package/templates/tanstack-query/.claude/skills/constants-organization/SKILL.md +144 -0
- package/templates/tanstack-query/.claude/skills/constants-organization/references/example-files.md +111 -0
- package/templates/tanstack-query/.claude/skills/constants-organization/references/query-key-factories.md +129 -0
- package/templates/tanstack-query/.claude/skills/query-client-setup/SKILL.md +165 -0
- package/templates/tanstack-query/.claude/skills/query-client-setup/references/devtools.md +67 -0
- package/templates/tanstack-query/.claude/skills/query-client-setup/references/global-handlers.md +94 -0
- package/templates/tanstack-query/.claude/skills/tanstack-services/SKILL.md +142 -0
- package/templates/tanstack-query/.claude/skills/tanstack-services/references/audited-mutation.md +144 -0
- package/templates/tanstack-query/.claude/skills/tanstack-services/references/optimistic-update.md +102 -0
- package/templates/tanstack-query/.claude/skills/tanstack-services/references/service-cookbook.md +151 -0
- package/templates/tanstack-query/README.md +63 -0
- package/templates/tanstack-query/package.partial.json +8 -0
- package/templates/tanstack-query/src/api/axiosInstance.ts +20 -0
- package/templates/tanstack-query/src/api/http.ts +62 -0
- package/templates/tanstack-query/src/api/queryClient.ts +28 -0
- package/templates/tanstack-query/src/app/App.tsx +20 -0
- package/templates/tanstack-query/src/services/example.ts +32 -0
package/templates/tanstack-query/.claude/skills/tanstack-services/references/audited-mutation.md
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# Audited mutation wrapper
|
|
2
|
+
|
|
3
|
+
Every state-changing API call in a BFSI app should emit an audit event (RBI Annexure I §8, SOC2 CC7.3). Without an `axiosBaseQuery`-style abstraction, we wrap `useMutation` with a custom hook.
|
|
4
|
+
|
|
5
|
+
## The wrapper
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
// src/services/useAuditedMutation.ts
|
|
9
|
+
import {
|
|
10
|
+
useMutation,
|
|
11
|
+
type UseMutationOptions,
|
|
12
|
+
type UseMutationResult,
|
|
13
|
+
} from '@tanstack/react-query';
|
|
14
|
+
import type { ApiError } from '@<scope>/core/http';
|
|
15
|
+
import { AuditClient, generateEventId } from '@<scope>/core/audit';
|
|
16
|
+
import { env } from '@/env';
|
|
17
|
+
|
|
18
|
+
const auditClient = new AuditClient({
|
|
19
|
+
endpoint: env.VITE_AUDIT_ENDPOINT,
|
|
20
|
+
batchSize: env.VITE_AUDIT_BATCH_SIZE,
|
|
21
|
+
flushIntervalMs: env.VITE_AUDIT_FLUSH_INTERVAL_MS,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export interface UseAuditedMutationOptions<TData, TVars>
|
|
25
|
+
extends UseMutationOptions<TData, ApiError, TVars> {
|
|
26
|
+
/** Audit event name — convention: <feature>.<entity>.<action> */
|
|
27
|
+
eventName: string;
|
|
28
|
+
/** Optional toast on success */
|
|
29
|
+
successMessage?: string;
|
|
30
|
+
/** Function to extract audit target from variables */
|
|
31
|
+
getTarget?: (vars: TVars) => { type: string; id?: string };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function useAuditedMutation<TData, TVars>(
|
|
35
|
+
options: UseAuditedMutationOptions<TData, TVars>,
|
|
36
|
+
): UseMutationResult<TData, ApiError, TVars> {
|
|
37
|
+
return useMutation({
|
|
38
|
+
...options,
|
|
39
|
+
onMutate: async (vars) => {
|
|
40
|
+
// Record audit event (pending)
|
|
41
|
+
const eventId = generateEventId();
|
|
42
|
+
const target = options.getTarget?.(vars);
|
|
43
|
+
auditClient.record({
|
|
44
|
+
event_id: eventId,
|
|
45
|
+
event_name: options.eventName,
|
|
46
|
+
actor_id: getCurrentUserId(), // your auth context
|
|
47
|
+
target_type: target?.type,
|
|
48
|
+
target_id: target?.id,
|
|
49
|
+
timestamp: new Date().toISOString(),
|
|
50
|
+
outcome: 'pending',
|
|
51
|
+
});
|
|
52
|
+
// Pass eventId through context for outcome update on success/error
|
|
53
|
+
const inherited = await options.onMutate?.(vars);
|
|
54
|
+
return { eventId, inherited } as never;
|
|
55
|
+
},
|
|
56
|
+
onSuccess: (data, vars, ctx) => {
|
|
57
|
+
const eventId = (ctx as { eventId: string } | undefined)?.eventId;
|
|
58
|
+
if (eventId) {
|
|
59
|
+
auditClient.record({
|
|
60
|
+
event_id: eventId,
|
|
61
|
+
event_name: options.eventName,
|
|
62
|
+
actor_id: getCurrentUserId(),
|
|
63
|
+
timestamp: new Date().toISOString(),
|
|
64
|
+
outcome: 'success',
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
options.onSuccess?.(data, vars, ctx);
|
|
68
|
+
},
|
|
69
|
+
onError: (err, vars, ctx) => {
|
|
70
|
+
const eventId = (ctx as { eventId: string } | undefined)?.eventId;
|
|
71
|
+
if (eventId) {
|
|
72
|
+
auditClient.record({
|
|
73
|
+
event_id: eventId,
|
|
74
|
+
event_name: options.eventName,
|
|
75
|
+
actor_id: getCurrentUserId(),
|
|
76
|
+
timestamp: new Date().toISOString(),
|
|
77
|
+
outcome: 'failure',
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
options.onError?.(err, vars, ctx);
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function getCurrentUserId(): string {
|
|
86
|
+
// Wire to your auth context
|
|
87
|
+
return 'TODO';
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Usage
|
|
92
|
+
|
|
93
|
+
```tsx
|
|
94
|
+
const submit = useAuditedMutation({
|
|
95
|
+
mutationFn: submitKyc,
|
|
96
|
+
eventName: 'kyc.verification.submitted',
|
|
97
|
+
successMessage: 'KYC submitted successfully',
|
|
98
|
+
getTarget: (payload) => ({ type: 'kyc_record', id: undefined }), // id assigned on success
|
|
99
|
+
onSuccess: (record) => {
|
|
100
|
+
queryClient.invalidateQueries({ queryKey: kycKeys.all });
|
|
101
|
+
navigate(`/kyc/${record.id}`);
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Audit event naming
|
|
107
|
+
|
|
108
|
+
`<feature>.<entity>.<action>` — see `bfsi-feature` skill in the toolkit for full naming convention. Examples:
|
|
109
|
+
|
|
110
|
+
- `kyc.verification.submitted`
|
|
111
|
+
- `kyc.verification.approved`
|
|
112
|
+
- `transaction.transfer.initiated`
|
|
113
|
+
- `user.session.logged_in`
|
|
114
|
+
- `data.pan.revealed` (when PII is unmasked)
|
|
115
|
+
|
|
116
|
+
## Why this wraps useMutation
|
|
117
|
+
|
|
118
|
+
In the RTK variant, audit events flow through the `axiosBaseQuery` because that's the single chokepoint every API call goes through. In TanStack, there's no equivalent — each service call is direct. The wrapper hook brings back the chokepoint for state-changing operations.
|
|
119
|
+
|
|
120
|
+
You CAN skip the wrapper for read-only queries (no audit needed for reads, by default). Reads via `useQuery` don't get the wrapper.
|
|
121
|
+
|
|
122
|
+
## Reveal events (separate from mutations)
|
|
123
|
+
|
|
124
|
+
When a user clicks "reveal PAN" in `<PIIMaskedDisplay>`, you fire an audit event directly:
|
|
125
|
+
|
|
126
|
+
```tsx
|
|
127
|
+
<PIIMaskedDisplay
|
|
128
|
+
type="pan"
|
|
129
|
+
value={user.pan}
|
|
130
|
+
onReveal={() =>
|
|
131
|
+
auditClient.record({
|
|
132
|
+
event_id: generateEventId(),
|
|
133
|
+
event_name: 'data.pan.revealed',
|
|
134
|
+
actor_id: getCurrentUserId(),
|
|
135
|
+
target_type: 'user',
|
|
136
|
+
target_id: user.id,
|
|
137
|
+
timestamp: new Date().toISOString(),
|
|
138
|
+
outcome: 'success',
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
/>
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
No mutation involved — just a direct audit call.
|
package/templates/tanstack-query/.claude/skills/tanstack-services/references/optimistic-update.md
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# Optimistic updates (TanStack)
|
|
2
|
+
|
|
3
|
+
Use sparingly. Optimistic updates risk showing stale state to the user; only use when success rate is very high AND latency is meaningful to the user.
|
|
4
|
+
|
|
5
|
+
## When to use
|
|
6
|
+
|
|
7
|
+
✅ Good fit:
|
|
8
|
+
|
|
9
|
+
- Toggling a boolean (favourited, archived, read/unread)
|
|
10
|
+
- Reordering a list (drag-and-drop)
|
|
11
|
+
- Inline editing of a single field
|
|
12
|
+
|
|
13
|
+
❌ Bad fit:
|
|
14
|
+
|
|
15
|
+
- Financial transactions (NEVER show success before backend confirms)
|
|
16
|
+
- KYC submissions (regulatory + may be rejected by ML)
|
|
17
|
+
- Anything where failure is plausible
|
|
18
|
+
|
|
19
|
+
## Pattern — `onMutate` + rollback
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
const queryClient = useQueryClient();
|
|
23
|
+
|
|
24
|
+
const toggleFavorite = useMutation({
|
|
25
|
+
mutationFn: async (args: { id: string; isFavorite: boolean }) => {
|
|
26
|
+
return PATCH<IKycRecord, { isFavorite: boolean }>(KYC_URLS.FAVORITE(args.id), {
|
|
27
|
+
isFavorite: args.isFavorite,
|
|
28
|
+
});
|
|
29
|
+
},
|
|
30
|
+
onMutate: async ({ id, isFavorite }) => {
|
|
31
|
+
// Cancel any outgoing refetches so they don't overwrite our optimistic update
|
|
32
|
+
await queryClient.cancelQueries({ queryKey: kycKeys.detail(id) });
|
|
33
|
+
|
|
34
|
+
// Snapshot the previous value
|
|
35
|
+
const previous = queryClient.getQueryData<IKycRecord>(kycKeys.detail(id));
|
|
36
|
+
|
|
37
|
+
// Optimistic update
|
|
38
|
+
if (previous) {
|
|
39
|
+
queryClient.setQueryData<IKycRecord>(kycKeys.detail(id), {
|
|
40
|
+
...previous,
|
|
41
|
+
isFavorite,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Return rollback context
|
|
46
|
+
return { previous };
|
|
47
|
+
},
|
|
48
|
+
onError: (_err, { id }, context) => {
|
|
49
|
+
if (context?.previous) {
|
|
50
|
+
queryClient.setQueryData(kycKeys.detail(id), context.previous);
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
onSettled: (_data, _err, { id }) => {
|
|
54
|
+
// Always refetch to reconcile with server truth
|
|
55
|
+
queryClient.invalidateQueries({ queryKey: kycKeys.detail(id) });
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Rules
|
|
61
|
+
|
|
62
|
+
1. ALWAYS `cancelQueries` first — otherwise an in-flight fetch overwrites the optimistic update on settle.
|
|
63
|
+
2. ALWAYS snapshot the previous value before patching — needed for rollback.
|
|
64
|
+
3. ALWAYS rollback in `onError` using the context returned by `onMutate`.
|
|
65
|
+
4. ALWAYS reconcile in `onSettled` (invalidate or refetch).
|
|
66
|
+
5. NEVER toast "success" optimistically — wait for `onSuccess`.
|
|
67
|
+
6. NEVER optimistically create new entries that need a server-assigned ID — wait for `onSuccess` to know the real ID.
|
|
68
|
+
|
|
69
|
+
## Optimistic across multiple queries
|
|
70
|
+
|
|
71
|
+
If a mutation should optimistically update both detail AND a list:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
onMutate: async ({ id, isFavorite }) => {
|
|
75
|
+
await queryClient.cancelQueries({ queryKey: kycKeys.detail(id) });
|
|
76
|
+
await queryClient.cancelQueries({ queryKey: kycKeys.lists() });
|
|
77
|
+
|
|
78
|
+
const previousDetail = queryClient.getQueryData<IKycRecord>(kycKeys.detail(id));
|
|
79
|
+
const previousLists = queryClient.getQueriesData<IKycListResponse>({ queryKey: kycKeys.lists() });
|
|
80
|
+
|
|
81
|
+
// Patch detail
|
|
82
|
+
if (previousDetail) {
|
|
83
|
+
queryClient.setQueryData<IKycRecord>(kycKeys.detail(id), {
|
|
84
|
+
...previousDetail,
|
|
85
|
+
isFavorite,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Patch every cached list page
|
|
90
|
+
for (const [key, list] of previousLists) {
|
|
91
|
+
if (!list) continue;
|
|
92
|
+
queryClient.setQueryData<IKycListResponse>(key, {
|
|
93
|
+
...list,
|
|
94
|
+
items: list.items.map((item) =>
|
|
95
|
+
item.id === id ? { ...item, isFavorite } : item,
|
|
96
|
+
),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return { previousDetail, previousLists };
|
|
101
|
+
},
|
|
102
|
+
```
|
package/templates/tanstack-query/.claude/skills/tanstack-services/references/service-cookbook.md
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# Service cookbook (TanStack)
|
|
2
|
+
|
|
3
|
+
Templates for common shapes. Copy + adapt.
|
|
4
|
+
|
|
5
|
+
## List
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
export const getKycList = async (): Promise<IKycListResponse> => {
|
|
9
|
+
const raw = await GET<unknown>(KYC_URLS.LIST);
|
|
10
|
+
return kycListResponseSchema.parse(raw);
|
|
11
|
+
};
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Paginated list
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
export const getKycList = async (params: {
|
|
18
|
+
page: number;
|
|
19
|
+
pageSize: number;
|
|
20
|
+
status?: KycStatus;
|
|
21
|
+
}): Promise<IKycListResponse> => {
|
|
22
|
+
const raw = await GET<unknown, typeof params>(KYC_URLS.LIST, params);
|
|
23
|
+
return kycListResponseSchema.parse(raw);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// Component:
|
|
27
|
+
const { data } = useQuery({
|
|
28
|
+
queryKey: kycKeys.list({ page, pageSize, status }),
|
|
29
|
+
queryFn: () => getKycList({ page, pageSize, status }),
|
|
30
|
+
placeholderData: (prev) => prev, // smooth pagination — keep previous data visible
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Detail
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
export const getKycDetail = async (id: string): Promise<IKycRecord> => {
|
|
38
|
+
const raw = await GET<unknown>(KYC_URLS.DETAIL(id));
|
|
39
|
+
return kycRecordSchema.parse(raw);
|
|
40
|
+
};
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Create
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
export const submitKyc = async (payload: IKycSubmitRequest): Promise<IKycRecord> => {
|
|
47
|
+
const raw = await POST<unknown, IKycSubmitRequest>(KYC_URLS.SUBMIT, payload);
|
|
48
|
+
return kycRecordSchema.parse(raw);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// Component:
|
|
52
|
+
const submit = useMutation({
|
|
53
|
+
mutationFn: submitKyc,
|
|
54
|
+
onSuccess: (newRecord) => {
|
|
55
|
+
queryClient.invalidateQueries({ queryKey: kycKeys.lists() });
|
|
56
|
+
queryClient.setQueryData(kycKeys.detail(newRecord.id), newRecord);
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Update
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
export const updateKyc = async (args: {
|
|
65
|
+
id: string;
|
|
66
|
+
body: IKycUpdateRequest;
|
|
67
|
+
}): Promise<IKycRecord> => {
|
|
68
|
+
const raw = await PUT<unknown, IKycUpdateRequest>(KYC_URLS.DETAIL(args.id), args.body);
|
|
69
|
+
return kycRecordSchema.parse(raw);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// Component:
|
|
73
|
+
const update = useMutation({
|
|
74
|
+
mutationFn: updateKyc,
|
|
75
|
+
onSuccess: (updated, { id }) => {
|
|
76
|
+
queryClient.invalidateQueries({ queryKey: kycKeys.lists() });
|
|
77
|
+
queryClient.setQueryData(kycKeys.detail(id), updated);
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Pattern: mutations that take both an id and a body wrap them in a single object. Keeps `mutationFn` typed as `(args: {...}) => Promise<...>` rather than overloaded.
|
|
83
|
+
|
|
84
|
+
## Delete
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
export const deleteKyc = (id: string): Promise<void> => DELETE<void>(KYC_URLS.DETAIL(id));
|
|
88
|
+
|
|
89
|
+
// Component:
|
|
90
|
+
const del = useMutation({
|
|
91
|
+
mutationFn: deleteKyc,
|
|
92
|
+
onSuccess: (_, id) => {
|
|
93
|
+
queryClient.invalidateQueries({ queryKey: kycKeys.lists() });
|
|
94
|
+
queryClient.removeQueries({ queryKey: kycKeys.detail(id) });
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Polling (e.g. async job status)
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
export const getJobStatus = async (jobId: string): Promise<IJobStatus> => {
|
|
103
|
+
const raw = await GET<unknown>(JOB_URLS.STATUS(jobId));
|
|
104
|
+
return jobStatusSchema.parse(raw);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
// Component:
|
|
108
|
+
const { data } = useQuery({
|
|
109
|
+
queryKey: jobKeys.status(jobId),
|
|
110
|
+
queryFn: () => getJobStatus(jobId),
|
|
111
|
+
refetchInterval: (query) => {
|
|
112
|
+
const status = query.state.data?.status;
|
|
113
|
+
return status === 'completed' || status === 'failed' ? false : 3000;
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## File upload
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
export const uploadDocument = async (file: File): Promise<IDocumentRecord> => {
|
|
122
|
+
const form = new FormData();
|
|
123
|
+
form.append('file', file);
|
|
124
|
+
const raw = await POST<unknown, FormData>(DOCS_URLS.UPLOAD, form, {
|
|
125
|
+
headers: { 'Content-Type': 'multipart/form-data' },
|
|
126
|
+
});
|
|
127
|
+
return documentRecordSchema.parse(raw);
|
|
128
|
+
};
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## File download
|
|
132
|
+
|
|
133
|
+
```ts
|
|
134
|
+
export const downloadStatement = (id: string): Promise<Blob> =>
|
|
135
|
+
GET<Blob, void>(STATEMENT_URLS.DOWNLOAD(id), undefined, { responseType: 'blob' });
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Parallel fetches in one query
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
export const getDashboardData = async (userId: string): Promise<IDashboardData> => {
|
|
142
|
+
const [profile, accounts, recent] = await Promise.all([
|
|
143
|
+
GET<unknown>(USER_URLS.PROFILE(userId)),
|
|
144
|
+
GET<unknown>(ACCOUNT_URLS.LIST_FOR_USER(userId)),
|
|
145
|
+
GET<unknown>(TX_URLS.RECENT_FOR_USER(userId, { limit: 5 })),
|
|
146
|
+
]);
|
|
147
|
+
return dashboardSchema.parse({ profile, accounts, recent });
|
|
148
|
+
};
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Components calling this get one queryKey, one loading state, one error — even though it makes 3 HTTP calls.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Template: TanStack Query variant
|
|
2
|
+
|
|
3
|
+
Overlay applied on top of `templates/_shared/` when the user picks **TanStack Query**.
|
|
4
|
+
|
|
5
|
+
## Adds
|
|
6
|
+
|
|
7
|
+
- `@tanstack/react-query` + devtools + `zustand` deps
|
|
8
|
+
- `src/api/axiosInstance.ts` — single shared axios instance from `@<projectName>/core/http`
|
|
9
|
+
- `src/api/http.ts` — typed `GET<TRes, TParams>`, `POST<TRes, TReq>`, `PUT`, `PATCH`, `DELETE` helpers
|
|
10
|
+
- `src/api/queryClient.ts` — `QueryClient` with BFSI defaults (30s stale, no focus refetch, no mutation retry)
|
|
11
|
+
- `src/services/example.ts` — reference service showing the `IRequest`/`IResponse` pattern
|
|
12
|
+
- `src/app/App.tsx` — overlays \_shared App with `<QueryClientProvider>`
|
|
13
|
+
|
|
14
|
+
## Service-method pattern (vs RTK Query's dispatch style)
|
|
15
|
+
|
|
16
|
+
You call services directly — no hooks, no dispatch:
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
// src/services/kyc.ts
|
|
20
|
+
import { POST, GET } from '@/api/http';
|
|
21
|
+
|
|
22
|
+
export interface IKycRequest {
|
|
23
|
+
pan: string;
|
|
24
|
+
aadhaar: string;
|
|
25
|
+
}
|
|
26
|
+
export interface IKycResponse {
|
|
27
|
+
id: string;
|
|
28
|
+
status: 'pending' | 'approved';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const submitKyc = (payload: IKycRequest): Promise<IKycResponse> =>
|
|
32
|
+
POST<IKycResponse, IKycRequest>('/kyc', payload);
|
|
33
|
+
|
|
34
|
+
export const getKyc = (id: string): Promise<IKycResponse> => GET<IKycResponse>(`/kyc/${id}`);
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Inside components, wire with hooks:
|
|
38
|
+
|
|
39
|
+
```tsx
|
|
40
|
+
import { useMutation, useQuery } from '@tanstack/react-query';
|
|
41
|
+
import { submitKyc, getKyc } from '@/services/kyc';
|
|
42
|
+
|
|
43
|
+
const submit = useMutation({ mutationFn: submitKyc });
|
|
44
|
+
const detail = useQuery({ queryKey: ['kyc', id], queryFn: () => getKyc(id) });
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Auth: set-once at login
|
|
48
|
+
|
|
49
|
+
The axios instance has no per-request token interceptor. Set the token once at login:
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import { setAuthToken } from '@react-vault/core/http';
|
|
53
|
+
import axiosInstance from '@/api/axiosInstance';
|
|
54
|
+
|
|
55
|
+
// inside login mutation onSuccess:
|
|
56
|
+
setAuthToken(axiosInstance, response.token);
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
On 401, the instance's `onUnauthorized` callback clears the token and redirects to `/login`.
|
|
60
|
+
|
|
61
|
+
## Why Zustand for client state
|
|
62
|
+
|
|
63
|
+
TanStack Query owns **server state** (fetches, caches, invalidation). Use Zustand for **client state** (UI state surviving route changes, form drafts, cross-component selections). Don't use Zustand for server data — that's what TanStack Query is for.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single axios instance for the app. Auth token set ONCE at login via
|
|
3
|
+
* setAuthToken() — not per-request.
|
|
4
|
+
*/
|
|
5
|
+
import { createAxios } from '@react-vault/core/http';
|
|
6
|
+
import { env } from '../env.js';
|
|
7
|
+
|
|
8
|
+
const axiosInstance = createAxios({
|
|
9
|
+
baseURL: env.VITE_API_BASE_URL,
|
|
10
|
+
timeoutMs: env.VITE_API_TIMEOUT_MS,
|
|
11
|
+
authHeaderName: env.VITE_AUTH_HEADER_NAME,
|
|
12
|
+
snakeCaseBackend: false,
|
|
13
|
+
onUnauthorized: () => {
|
|
14
|
+
if (typeof window !== 'undefined') {
|
|
15
|
+
window.location.href = '/login';
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export default axiosInstance;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed HTTP helpers for TanStack Query feature services.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* import { GET, POST, PUT, PATCH, DELETE } from '@/api/http';
|
|
6
|
+
*
|
|
7
|
+
* interface ILoginRequest { email: string; password: string; }
|
|
8
|
+
* interface ILoginResponse { token: string; userId: string; }
|
|
9
|
+
*
|
|
10
|
+
* export const loginService = (payload: ILoginRequest) =>
|
|
11
|
+
* POST<ILoginResponse, ILoginRequest>('/auth/login', payload);
|
|
12
|
+
*
|
|
13
|
+
* Pair with useMutation / useQuery:
|
|
14
|
+
* const m = useMutation({ mutationFn: loginService });
|
|
15
|
+
* const q = useQuery({ queryKey: ['kyc', id], queryFn: () => getKyc(id) });
|
|
16
|
+
*/
|
|
17
|
+
import type { AxiosRequestConfig } from 'axios';
|
|
18
|
+
import axiosInstance from './axiosInstance.js';
|
|
19
|
+
|
|
20
|
+
export async function GET<TResponse, TParams = void>(
|
|
21
|
+
url: string,
|
|
22
|
+
params?: TParams,
|
|
23
|
+
config?: AxiosRequestConfig,
|
|
24
|
+
): Promise<TResponse> {
|
|
25
|
+
const { data } = await axiosInstance.get<TResponse>(url, { ...config, params });
|
|
26
|
+
return data;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function POST<TResponse, TRequest = void>(
|
|
30
|
+
url: string,
|
|
31
|
+
payload?: TRequest,
|
|
32
|
+
config?: AxiosRequestConfig,
|
|
33
|
+
): Promise<TResponse> {
|
|
34
|
+
const { data } = await axiosInstance.post<TResponse>(url, payload, config);
|
|
35
|
+
return data;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function PUT<TResponse, TRequest = void>(
|
|
39
|
+
url: string,
|
|
40
|
+
payload?: TRequest,
|
|
41
|
+
config?: AxiosRequestConfig,
|
|
42
|
+
): Promise<TResponse> {
|
|
43
|
+
const { data } = await axiosInstance.put<TResponse>(url, payload, config);
|
|
44
|
+
return data;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function PATCH<TResponse, TRequest = void>(
|
|
48
|
+
url: string,
|
|
49
|
+
payload?: TRequest,
|
|
50
|
+
config?: AxiosRequestConfig,
|
|
51
|
+
): Promise<TResponse> {
|
|
52
|
+
const { data } = await axiosInstance.patch<TResponse>(url, payload, config);
|
|
53
|
+
return data;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function DELETE<TResponse = void>(
|
|
57
|
+
url: string,
|
|
58
|
+
config?: AxiosRequestConfig,
|
|
59
|
+
): Promise<TResponse> {
|
|
60
|
+
const { data } = await axiosInstance.delete<TResponse>(url, config);
|
|
61
|
+
return data;
|
|
62
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TanStack Query client with BFSI-friendly defaults:
|
|
3
|
+
* - 30s stale time (don't thrash refetching)
|
|
4
|
+
* - No refetch on focus (user's choice, not surprise)
|
|
5
|
+
* - Don't retry 4xx (except 408/429)
|
|
6
|
+
* - Mutations never auto-retry (use idempotency-key + explicit retry)
|
|
7
|
+
*/
|
|
8
|
+
import { QueryClient } from '@tanstack/react-query';
|
|
9
|
+
|
|
10
|
+
export const queryClient = new QueryClient({
|
|
11
|
+
defaultOptions: {
|
|
12
|
+
queries: {
|
|
13
|
+
staleTime: 30_000,
|
|
14
|
+
gcTime: 5 * 60_000,
|
|
15
|
+
retry: (failureCount, error) => {
|
|
16
|
+
const status = (error as { status?: number })?.status;
|
|
17
|
+
if (status && status >= 400 && status < 500 && status !== 408 && status !== 429) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
return failureCount < 2;
|
|
21
|
+
},
|
|
22
|
+
refetchOnWindowFocus: false,
|
|
23
|
+
},
|
|
24
|
+
mutations: {
|
|
25
|
+
retry: false,
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TanStack variant App.tsx — overlays the _shared App.tsx with QueryClientProvider.
|
|
3
|
+
*/
|
|
4
|
+
import { QueryClientProvider } from '@tanstack/react-query';
|
|
5
|
+
import { BrowserRouter } from 'react-router-dom';
|
|
6
|
+
import { ErrorBoundary } from '../shared/ErrorBoundary.js';
|
|
7
|
+
import { AppRoutes } from '../routes/index.js';
|
|
8
|
+
import { queryClient } from '../api/queryClient.js';
|
|
9
|
+
|
|
10
|
+
export function App(): JSX.Element {
|
|
11
|
+
return (
|
|
12
|
+
<ErrorBoundary>
|
|
13
|
+
<QueryClientProvider client={queryClient}>
|
|
14
|
+
<BrowserRouter>
|
|
15
|
+
<AppRoutes />
|
|
16
|
+
</BrowserRouter>
|
|
17
|
+
</QueryClientProvider>
|
|
18
|
+
</ErrorBoundary>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example service. Pattern to copy for new features:
|
|
3
|
+
*
|
|
4
|
+
* 1. Define IRequest / IResponse interfaces
|
|
5
|
+
* 2. Export plain async functions that call GET/POST/PUT/PATCH/DELETE
|
|
6
|
+
* 3. Use them inside React components via useQuery / useMutation
|
|
7
|
+
*
|
|
8
|
+
* Keep services in src/services/<feature>.ts or src/features/<Feature>/service.ts —
|
|
9
|
+
* either works. Don't put TanStack hooks here; this layer is hook-free so
|
|
10
|
+
* services are easy to unit-test.
|
|
11
|
+
*/
|
|
12
|
+
import { GET, POST } from '../api/http.js';
|
|
13
|
+
|
|
14
|
+
export interface IExampleListResponse {
|
|
15
|
+
items: Array<{ id: string; name: string }>;
|
|
16
|
+
total: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface IExampleCreateRequest {
|
|
20
|
+
name: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface IExampleCreateResponse {
|
|
24
|
+
id: string;
|
|
25
|
+
name: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const getExamples = (): Promise<IExampleListResponse> =>
|
|
29
|
+
GET<IExampleListResponse>('/examples');
|
|
30
|
+
|
|
31
|
+
export const createExample = (payload: IExampleCreateRequest): Promise<IExampleCreateResponse> =>
|
|
32
|
+
POST<IExampleCreateResponse, IExampleCreateRequest>('/examples', payload);
|