@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.
Files changed (117) hide show
  1. package/LICENSE +12 -0
  2. package/README.md +16 -0
  3. package/bin/create-app.js +8 -0
  4. package/claude-toolkit/README.md +131 -0
  5. package/claude-toolkit/agents/bfsi-accessibility-auditor.md +132 -0
  6. package/claude-toolkit/agents/bfsi-architect.md +156 -0
  7. package/claude-toolkit/agents/bfsi-code-reviewer.md +137 -0
  8. package/claude-toolkit/agents/bfsi-compliance-auditor.md +161 -0
  9. package/claude-toolkit/agents/bfsi-pii-scanner.md +142 -0
  10. package/claude-toolkit/agents/bfsi-pr-reviewer.md +114 -0
  11. package/claude-toolkit/agents/bfsi-security-reviewer.md +136 -0
  12. package/claude-toolkit/commands/bfsi-audit.md +46 -0
  13. package/claude-toolkit/commands/bfsi-doctor.md +97 -0
  14. package/claude-toolkit/commands/bfsi-review.md +46 -0
  15. package/claude-toolkit/commands/bfsi-scaffold.md +47 -0
  16. package/claude-toolkit/hooks/hooks.json +181 -0
  17. package/claude-toolkit/hooks/scripts/a11y-check.sh +63 -0
  18. package/claude-toolkit/hooks/scripts/audit-prompt.sh +36 -0
  19. package/claude-toolkit/hooks/scripts/block-destructive.sh +41 -0
  20. package/claude-toolkit/hooks/scripts/block-force-push.sh +30 -0
  21. package/claude-toolkit/hooks/scripts/format.sh +42 -0
  22. package/claude-toolkit/hooks/scripts/inject-context.sh +44 -0
  23. package/claude-toolkit/hooks/scripts/lint.sh +45 -0
  24. package/claude-toolkit/hooks/scripts/protect-files.sh +53 -0
  25. package/claude-toolkit/hooks/scripts/save-compliance-context.sh +35 -0
  26. package/claude-toolkit/hooks/scripts/scan-pii.sh +87 -0
  27. package/claude-toolkit/hooks/scripts/scan-secrets.sh +67 -0
  28. package/claude-toolkit/hooks/scripts/verify-clean.sh +50 -0
  29. package/claude-toolkit/package.json +22 -0
  30. package/claude-toolkit/plugin.json +31 -0
  31. package/claude-toolkit/skills/bfsi-api-endpoint/SKILL.md +105 -0
  32. package/claude-toolkit/skills/bfsi-commit/SKILL.md +102 -0
  33. package/claude-toolkit/skills/bfsi-compliance-check/SKILL.md +107 -0
  34. package/claude-toolkit/skills/bfsi-encrypt-helper/SKILL.md +127 -0
  35. package/claude-toolkit/skills/bfsi-error-message/SKILL.md +162 -0
  36. package/claude-toolkit/skills/bfsi-feature/SKILL.md +120 -0
  37. package/claude-toolkit/skills/bfsi-feature/references/architecture.md +69 -0
  38. package/claude-toolkit/skills/bfsi-feature/references/audit-events.md +70 -0
  39. package/claude-toolkit/skills/bfsi-feature/scripts/scaffold.mjs +136 -0
  40. package/claude-toolkit/skills/bfsi-form/SKILL.md +73 -0
  41. package/claude-toolkit/skills/bfsi-form/references/validation-regex.md +50 -0
  42. package/claude-toolkit/skills/bfsi-onboarding/SKILL.md +110 -0
  43. package/claude-toolkit/skills/bfsi-pii-field/SKILL.md +90 -0
  44. package/claude-toolkit/skills/bfsi-test-pattern/SKILL.md +179 -0
  45. package/dist/index.d.ts +2 -0
  46. package/dist/index.d.ts.map +1 -0
  47. package/dist/index.js +339 -0
  48. package/dist/index.js.map +1 -0
  49. package/package.json +69 -0
  50. package/templates/_shared/.claude/settings.json +31 -0
  51. package/templates/_shared/.env.local.sample +25 -0
  52. package/templates/_shared/.github/workflows/ci.yml +49 -0
  53. package/templates/_shared/CLAUDE.md +89 -0
  54. package/templates/_shared/README.md +50 -0
  55. package/templates/_shared/index.html +16 -0
  56. package/templates/_shared/package.json +73 -0
  57. package/templates/_shared/postcss.config.cjs +6 -0
  58. package/templates/_shared/src/app/App.tsx +13 -0
  59. package/templates/_shared/src/app/globals.css +64 -0
  60. package/templates/_shared/src/env.ts +33 -0
  61. package/templates/_shared/src/i18n/i18n.ts +18 -0
  62. package/templates/_shared/src/i18n/translations/en.json +54 -0
  63. package/templates/_shared/src/i18n/translations/hi.json +30 -0
  64. package/templates/_shared/src/main.tsx +16 -0
  65. package/templates/_shared/src/routes/ProtectedRoute.tsx +28 -0
  66. package/templates/_shared/src/routes/index.tsx +67 -0
  67. package/templates/_shared/src/shared/ErrorBoundary.tsx +60 -0
  68. package/templates/_shared/tailwind.config.ts +68 -0
  69. package/templates/_shared/tests/setup.ts +7 -0
  70. package/templates/_shared/tsconfig.json +33 -0
  71. package/templates/_shared/tsconfig.node.json +13 -0
  72. package/templates/_shared/vite.config.ts +47 -0
  73. package/templates/rtk-query/.claude/skills/axios-auth/SKILL.md +103 -0
  74. package/templates/rtk-query/.claude/skills/axios-auth/references/error-shape.md +84 -0
  75. package/templates/rtk-query/.claude/skills/axios-auth/references/full-code-walkthrough.md +146 -0
  76. package/templates/rtk-query/.claude/skills/axios-auth/references/notification-wiring.md +141 -0
  77. package/templates/rtk-query/.claude/skills/constants-organization/SKILL.md +112 -0
  78. package/templates/rtk-query/.claude/skills/constants-organization/references/example-files.md +134 -0
  79. package/templates/rtk-query/.claude/skills/constants-organization/references/tag-types-catalog.md +53 -0
  80. package/templates/rtk-query/.claude/skills/redux-store-integration/SKILL.md +159 -0
  81. package/templates/rtk-query/.claude/skills/redux-store-integration/references/localStorage-persistence.md +70 -0
  82. package/templates/rtk-query/.claude/skills/redux-store-integration/references/middleware-patterns.md +82 -0
  83. package/templates/rtk-query/.claude/skills/rtk-query-api/SKILL.md +148 -0
  84. package/templates/rtk-query/.claude/skills/rtk-query-api/references/cache-strategies.md +96 -0
  85. package/templates/rtk-query/.claude/skills/rtk-query-api/references/endpoint-cookbook.md +145 -0
  86. package/templates/rtk-query/.claude/skills/rtk-query-api/references/optimistic-update.md +53 -0
  87. package/templates/rtk-query/README.md +84 -0
  88. package/templates/rtk-query/package.partial.json +7 -0
  89. package/templates/rtk-query/src/app/App.tsx +23 -0
  90. package/templates/rtk-query/src/axiosconfig/axiosInstance.ts +26 -0
  91. package/templates/rtk-query/src/axiosconfig/baseQuery.ts +72 -0
  92. package/templates/rtk-query/src/axiosconfig/interceptor.ts +42 -0
  93. package/templates/rtk-query/src/redux/invalidateCacheMiddleware.ts +20 -0
  94. package/templates/rtk-query/src/redux/reduxHooks.ts +10 -0
  95. package/templates/rtk-query/src/redux/rootReducer.ts +18 -0
  96. package/templates/rtk-query/src/redux/store.ts +36 -0
  97. package/templates/tanstack-query/.claude/skills/axios-auth/SKILL.md +109 -0
  98. package/templates/tanstack-query/.claude/skills/axios-auth/references/error-shape.md +89 -0
  99. package/templates/tanstack-query/.claude/skills/axios-auth/references/full-code-walkthrough.md +121 -0
  100. package/templates/tanstack-query/.claude/skills/axios-auth/references/notification-pattern.md +109 -0
  101. package/templates/tanstack-query/.claude/skills/constants-organization/SKILL.md +144 -0
  102. package/templates/tanstack-query/.claude/skills/constants-organization/references/example-files.md +111 -0
  103. package/templates/tanstack-query/.claude/skills/constants-organization/references/query-key-factories.md +129 -0
  104. package/templates/tanstack-query/.claude/skills/query-client-setup/SKILL.md +165 -0
  105. package/templates/tanstack-query/.claude/skills/query-client-setup/references/devtools.md +67 -0
  106. package/templates/tanstack-query/.claude/skills/query-client-setup/references/global-handlers.md +94 -0
  107. package/templates/tanstack-query/.claude/skills/tanstack-services/SKILL.md +142 -0
  108. package/templates/tanstack-query/.claude/skills/tanstack-services/references/audited-mutation.md +144 -0
  109. package/templates/tanstack-query/.claude/skills/tanstack-services/references/optimistic-update.md +102 -0
  110. package/templates/tanstack-query/.claude/skills/tanstack-services/references/service-cookbook.md +151 -0
  111. package/templates/tanstack-query/README.md +63 -0
  112. package/templates/tanstack-query/package.partial.json +8 -0
  113. package/templates/tanstack-query/src/api/axiosInstance.ts +20 -0
  114. package/templates/tanstack-query/src/api/http.ts +62 -0
  115. package/templates/tanstack-query/src/api/queryClient.ts +28 -0
  116. package/templates/tanstack-query/src/app/App.tsx +20 -0
  117. package/templates/tanstack-query/src/services/example.ts +32 -0
@@ -0,0 +1,96 @@
1
+ # Cache strategies — when to use which tagging pattern
2
+
3
+ ## Single-entity tag
4
+
5
+ When a feature has one type of resource:
6
+
7
+ ```ts
8
+ tagTypes: ['Kyc'];
9
+ // queries: providesTags: ['Kyc']
10
+ // mutations: invalidatesTags: ['Kyc']
11
+ ```
12
+
13
+ Simple. Every mutation refetches everything tagged `Kyc`. Fine for small lists.
14
+
15
+ ## Per-record tags (id-scoped)
16
+
17
+ When the list is large and you want detail updates to NOT refetch the whole list:
18
+
19
+ ```ts
20
+ providesTags: (result) =>
21
+ result
22
+ ? [
23
+ ...result.items.map(({ id }) => ({ type: 'Kyc' as const, id })),
24
+ { type: 'Kyc' as const, id: 'LIST' },
25
+ ]
26
+ : [{ type: 'Kyc' as const, id: 'LIST' }],
27
+
28
+ // Detail provides one tag:
29
+ providesTags: (_, __, id) => [{ type: 'Kyc', id }],
30
+
31
+ // Update mutation invalidates that one tag:
32
+ invalidatesTags: (_, __, { id }) => [{ type: 'Kyc', id }],
33
+
34
+ // Create mutation invalidates only the list:
35
+ invalidatesTags: [{ type: 'Kyc', id: 'LIST' }],
36
+ ```
37
+
38
+ The `LIST` is a synthetic id used as the "any list" tag.
39
+
40
+ ## Tag everything mutating affects (mutation joined with list)
41
+
42
+ When an update changes a derived list (e.g. updating a transaction affects account balance):
43
+
44
+ ```ts
45
+ updateTransaction: builder.mutation({
46
+ invalidatesTags: (_, __, { id }) => [
47
+ { type: 'Transaction', id },
48
+ { type: 'AccountBalance' }, // tag from a different api
49
+ ],
50
+ }),
51
+ ```
52
+
53
+ For cross-API invalidations, the target API must also be in the `tagTypes` registry or you need `invalidateCacheMiddleware` instead.
54
+
55
+ ## Stale-while-revalidate
56
+
57
+ Default RTK Query behaviour — cached data shows immediately, refetch happens in background. To control:
58
+
59
+ ```ts
60
+ getKycList: builder.query({
61
+ query: ...,
62
+ keepUnusedDataFor: 60, // seconds after last subscriber unsubscribes
63
+ refetchOnMountOrArgChange: 30, // seconds — refetch if cache > 30s old
64
+ }),
65
+ ```
66
+
67
+ For audit-critical lists where staleness is unacceptable, set `keepUnusedDataFor: 0` so cache is wiped on unmount.
68
+
69
+ ## Skip cache (force fresh)
70
+
71
+ Pass `{ refetchOnMountOrArgChange: true }` to a single hook call:
72
+
73
+ ```tsx
74
+ const { data } = useGetKycListQuery(undefined, { refetchOnMountOrArgChange: true });
75
+ ```
76
+
77
+ Use ONLY for sensitive views (audit log, balance display in transaction flow).
78
+
79
+ ## Pagination + cache
80
+
81
+ When paginating, key the query by page so each page caches independently:
82
+
83
+ ```ts
84
+ getKycList: builder.query<KycListResponse, { page: number; pageSize: number }>({
85
+ query: (params) => ({ url: KYC_URLS.LIST, method: GET, data: params }),
86
+ providesTags: (result, _, arg) =>
87
+ result
88
+ ? [
89
+ ...result.items.map(({ id }) => ({ type: 'Kyc' as const, id })),
90
+ { type: 'Kyc' as const, id: `PAGE-${arg.page}` },
91
+ ]
92
+ : [{ type: 'Kyc' as const, id: `PAGE-${arg.page}` }],
93
+ }),
94
+ ```
95
+
96
+ This way refetching one page doesn't blow away the others.
@@ -0,0 +1,145 @@
1
+ # Endpoint cookbook
2
+
3
+ Templates for the common shapes. Copy + adapt.
4
+
5
+ ## List
6
+
7
+ ```ts
8
+ getKycList: builder.query<KycListResponse, void>({
9
+ query: () => ({ url: KYC_URLS.LIST, method: GET }),
10
+ transformResponse: (raw: unknown) => kycListResponseSchema.parse(raw),
11
+ providesTags: ['Kyc'],
12
+ }),
13
+ ```
14
+
15
+ ## Paginated list
16
+
17
+ ```ts
18
+ getKycList: builder.query<KycListResponse, { page: number; pageSize: number; status?: KycStatus }>({
19
+ query: (params) => ({ url: KYC_URLS.LIST, method: GET, data: params }),
20
+ transformResponse: (raw: unknown) => kycListResponseSchema.parse(raw),
21
+ providesTags: (result) =>
22
+ result
23
+ ? [
24
+ ...result.items.map(({ id }) => ({ type: 'Kyc' as const, id })),
25
+ { type: 'Kyc' as const, id: 'LIST' },
26
+ ]
27
+ : [{ type: 'Kyc' as const, id: 'LIST' }],
28
+ }),
29
+ ```
30
+
31
+ ## Detail
32
+
33
+ ```ts
34
+ getKycDetail: builder.query<KycRecord, string>({
35
+ query: (id) => ({ url: KYC_URLS.DETAIL(id), method: GET }),
36
+ transformResponse: (raw: unknown) => kycRecordSchema.parse(raw),
37
+ providesTags: (_, __, id) => [{ type: 'Kyc', id }],
38
+ }),
39
+ ```
40
+
41
+ ## Create (mutation)
42
+
43
+ ```ts
44
+ submitKyc: builder.mutation<KycRecord, KycSubmitRequest>({
45
+ query: (body) => ({
46
+ url: KYC_URLS.SUBMIT,
47
+ method: POST,
48
+ data: body,
49
+ showSuccessNotification: true,
50
+ showFailureNotification: true,
51
+ }),
52
+ transformResponse: (raw: unknown) => kycRecordSchema.parse(raw),
53
+ invalidatesTags: [{ type: 'Kyc', id: 'LIST' }],
54
+ }),
55
+ ```
56
+
57
+ ## Update (mutation, per-record + list invalidation)
58
+
59
+ ```ts
60
+ updateKyc: builder.mutation<KycRecord, { id: string; body: KycUpdateRequest }>({
61
+ query: ({ id, body }) => ({
62
+ url: KYC_URLS.DETAIL(id),
63
+ method: PUT,
64
+ data: body,
65
+ showFailureNotification: true,
66
+ }),
67
+ transformResponse: (raw: unknown) => kycRecordSchema.parse(raw),
68
+ invalidatesTags: (_, __, { id }) => [
69
+ { type: 'Kyc', id },
70
+ { type: 'Kyc', id: 'LIST' },
71
+ ],
72
+ }),
73
+ ```
74
+
75
+ ## Delete
76
+
77
+ ```ts
78
+ deleteKyc: builder.mutation<void, string>({
79
+ query: (id) => ({
80
+ url: KYC_URLS.DETAIL(id),
81
+ method: DELETE,
82
+ showSuccessNotification: true,
83
+ showFailureNotification: true,
84
+ }),
85
+ invalidatesTags: (_, __, id) => [
86
+ { type: 'Kyc', id },
87
+ { type: 'Kyc', id: 'LIST' },
88
+ ],
89
+ }),
90
+ ```
91
+
92
+ ## Polling (e.g. job status)
93
+
94
+ In the component:
95
+
96
+ ```tsx
97
+ const { data } = useGetJobStatusQuery(jobId, {
98
+ pollingInterval: data?.status === 'pending' ? 3000 : 0,
99
+ });
100
+ ```
101
+
102
+ Skip polling once a terminal state is reached by setting interval to 0.
103
+
104
+ ## File upload
105
+
106
+ ```ts
107
+ uploadDocument: builder.mutation<DocumentRecord, FormData>({
108
+ query: (formData) => ({
109
+ url: DOCS_URLS.UPLOAD,
110
+ method: POST,
111
+ data: formData,
112
+ headers: { 'Content-Type': 'multipart/form-data' },
113
+ showFailureNotification: true,
114
+ }),
115
+ transformResponse: (raw: unknown) => documentRecordSchema.parse(raw),
116
+ invalidatesTags: ['Document'],
117
+ }),
118
+ ```
119
+
120
+ ## File download (responseType blob)
121
+
122
+ ```ts
123
+ downloadStatement: builder.query<Blob, string>({
124
+ query: (statementId) => ({
125
+ url: STATEMENT_URLS.DOWNLOAD(statementId),
126
+ method: GET,
127
+ responseType: 'blob',
128
+ }),
129
+ }),
130
+ ```
131
+
132
+ Trigger from component:
133
+
134
+ ```tsx
135
+ const [trigger] = useLazyDownloadStatementQuery();
136
+ const handleDownload = async (id: string) => {
137
+ const { data: blob } = await trigger(id).unwrap();
138
+ const url = URL.createObjectURL(blob);
139
+ const a = document.createElement('a');
140
+ a.href = url;
141
+ a.download = `statement-${id}.pdf`;
142
+ a.click();
143
+ URL.revokeObjectURL(url);
144
+ };
145
+ ```
@@ -0,0 +1,53 @@
1
+ # Optimistic updates
2
+
3
+ Use sparingly. Optimistic updates risk showing stale state to the user; only use when the success rate is very high (>99%) AND the perceived latency is meaningful.
4
+
5
+ ## When to use
6
+
7
+ ✅ Good fit:
8
+
9
+ - Toggling a boolean flag (favourited, archived, read/unread)
10
+ - Reordering a list (drag-and-drop)
11
+ - Inline editing of a single field with very low failure rate
12
+
13
+ ❌ Bad fit:
14
+
15
+ - Financial transactions (NEVER show success before backend confirms)
16
+ - KYC submissions (regulatory + the value MIGHT be rejected by ML)
17
+ - Anything where the failure path is "value also gets rejected for other reasons we can't predict"
18
+
19
+ ## Pattern — `onQueryStarted` with cache patch
20
+
21
+ ```ts
22
+ toggleFavorite: builder.mutation<void, { id: string; isFavorite: boolean }>({
23
+ query: ({ id, isFavorite }) => ({
24
+ url: KYC_URLS.FAVORITE(id),
25
+ method: PATCH,
26
+ data: { isFavorite },
27
+ }),
28
+ async onQueryStarted({ id, isFavorite }, { dispatch, queryFulfilled }) {
29
+ // Optimistic: patch the cached list entry
30
+ const patch = dispatch(
31
+ kycApi.util.updateQueryData('getKycList', undefined, (draft) => {
32
+ const item = draft.items.find((x) => x.id === id);
33
+ if (item) (item as { isFavorite: boolean }).isFavorite = isFavorite;
34
+ }),
35
+ );
36
+
37
+ try {
38
+ await queryFulfilled;
39
+ } catch {
40
+ // Rollback on failure
41
+ patch.undo();
42
+ }
43
+ },
44
+ invalidatesTags: (_, __, { id }) => [{ type: 'Kyc', id }],
45
+ }),
46
+ ```
47
+
48
+ ## Rules
49
+
50
+ 1. ALWAYS roll back on failure (`patch.undo()`)
51
+ 2. Still invalidate tags so the server's truth eventually wins
52
+ 3. Don't show success toast optimistically — wait for `queryFulfilled`
53
+ 4. Don't optimistically create new entries — wait for the server's ID assignment
@@ -0,0 +1,84 @@
1
+ # Template: RTK Query variant
2
+
3
+ Overlay applied on top of `templates/_shared/` when the user picks **RTK Query**.
4
+
5
+ ## Structure (mirrors rsense-react-org)
6
+
7
+ ```
8
+ src/
9
+ ├── axiosconfig/
10
+ │ ├── axiosInstance.ts # single shared axios instance
11
+ │ ├── interceptor.ts # response interceptor (notifications, 401)
12
+ │ └── baseQuery.ts # axiosBaseQuery for RTK Query
13
+ ├── redux/
14
+ │ ├── store.ts # configureStore + middleware concat
15
+ │ ├── rootReducer.ts # combineReducers (slices + API reducers)
16
+ │ ├── reduxHooks.ts # typed useAppDispatch / useAppSelector
17
+ │ └── invalidateCacheMiddleware.ts # cross-API tag invalidation
18
+ └── app/
19
+ └── App.tsx # overlays _shared App with <Provider>
20
+ ```
21
+
22
+ ## Auth: set-once at login
23
+
24
+ Tokens are set on the axios instance ONCE at login (rsense pattern, not per-request):
25
+
26
+ ```ts
27
+ import { setAuthToken } from '@react-vault/core/http';
28
+ import axiosInstance from '@/axiosconfig/axiosInstance';
29
+
30
+ // inside loginApi's onQueryStarted or login slice:
31
+ setAuthToken(axiosInstance, response.token);
32
+ ```
33
+
34
+ On 401, the instance's `onUnauthorized` callback clears the token and redirects to `/login`.
35
+
36
+ ## Feature pattern
37
+
38
+ ```ts
39
+ // src/features/Foo/api.ts
40
+ import { createApi } from '@reduxjs/toolkit/query/react';
41
+ import axiosBaseQuery from '@/axiosconfig/baseQuery';
42
+ import { fooResponseSchema } from './schema';
43
+ import type { FooResponse, FooQuery, FooBody } from './types';
44
+
45
+ const fooApi = createApi({
46
+ reducerPath: 'fooApi',
47
+ baseQuery: axiosBaseQuery(),
48
+ tagTypes: ['Foo'],
49
+ endpoints: (builder) => ({
50
+ getFoos: builder.query<FooResponse, FooQuery>({
51
+ query: (arg) => ({ url: '/foo', method: 'GET', data: arg }),
52
+ transformResponse: (raw: unknown) => fooResponseSchema.parse(raw),
53
+ providesTags: ['Foo'],
54
+ }),
55
+ createFoo: builder.mutation<FooResponse, FooBody>({
56
+ query: (body) => ({
57
+ url: '/foo',
58
+ method: 'POST',
59
+ data: body,
60
+ showSuccessNotification: true,
61
+ showFailureNotification: true,
62
+ }),
63
+ transformResponse: (raw: unknown) => fooResponseSchema.parse(raw),
64
+ invalidatesTags: ['Foo'],
65
+ }),
66
+ }),
67
+ });
68
+
69
+ export const { useGetFoosQuery, useCreateFooMutation } = fooApi;
70
+ export default fooApi;
71
+ ```
72
+
73
+ Register the API in `src/redux/rootReducer.ts` (reducer) and `src/redux/store.ts` (middleware).
74
+
75
+ ## Bundled skills (when this variant is picked)
76
+
77
+ The CLI copies a curated set of RTK-pattern skills into the scaffolded project's `.claude/skills/`:
78
+
79
+ - `axios-auth`
80
+ - `constants-organization`
81
+ - `redux-store-integration`
82
+ - `rtk-query-api`
83
+
84
+ Use `/bfsi-feature MyFeature` (provided by the inlined toolkit) — it'll generate RTK-style scaffolding aligned with these skills.
@@ -0,0 +1,7 @@
1
+ {
2
+ "_comment": "Partial package.json — CLI merges this with templates/_shared/package.json. Adds RTK Query deps.",
3
+ "dependencies": {
4
+ "@reduxjs/toolkit": "2.2.3",
5
+ "react-redux": "9.1.2"
6
+ }
7
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * RTK variant App.tsx — overlays the _shared App.tsx with Redux Provider.
3
+ *
4
+ * If you need to change the global layout / routes / error boundary, do it
5
+ * here. This file replaces _shared/src/app/App.tsx during scaffold.
6
+ */
7
+ import { Provider } from 'react-redux';
8
+ import { BrowserRouter } from 'react-router-dom';
9
+ import { ErrorBoundary } from '../shared/ErrorBoundary.js';
10
+ import { AppRoutes } from '../routes/index.js';
11
+ import store from '../redux/store.js';
12
+
13
+ export function App(): JSX.Element {
14
+ return (
15
+ <ErrorBoundary>
16
+ <Provider store={store}>
17
+ <BrowserRouter>
18
+ <AppRoutes />
19
+ </BrowserRouter>
20
+ </Provider>
21
+ </ErrorBoundary>
22
+ );
23
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Single axios instance shared across the app. Auth token is set ONCE at
3
+ * login via setAuthToken() from @react-vault/core/http (rsense-style,
4
+ * set-at-login — not injected per-request).
5
+ *
6
+ * Side-effect import of `./interceptor` wires the response interceptor for
7
+ * notifications + 401 handling.
8
+ */
9
+ import { createAxios } from '@react-vault/core/http';
10
+ import { env } from '../env.js';
11
+ import './interceptor.js';
12
+
13
+ const axiosInstance = createAxios({
14
+ baseURL: env.VITE_API_BASE_URL,
15
+ timeoutMs: env.VITE_API_TIMEOUT_MS,
16
+ authHeaderName: env.VITE_AUTH_HEADER_NAME,
17
+ snakeCaseBackend: false, // flip to true if backend uses snake_case
18
+ onUnauthorized: () => {
19
+ // Token already cleared by core; route to login here.
20
+ if (typeof window !== 'undefined') {
21
+ window.location.href = '/login';
22
+ }
23
+ },
24
+ });
25
+
26
+ export default axiosInstance;
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Custom RTK Query baseQuery built on the shared axios instance.
3
+ * Mirrors rsense-react-org's baseQuery: same shape, same notification flags.
4
+ *
5
+ * Each endpoint passes:
6
+ * { url, method, data, params, headers, showSuccessNotification, showFailureNotification }
7
+ */
8
+ import type { AxiosRequestConfig, AxiosResponse, ResponseType } from 'axios';
9
+ import type { BaseQueryFn } from '@reduxjs/toolkit/query';
10
+ import axiosInstance from './axiosInstance.js';
11
+ import type { ApiErrorShape } from './interceptor.js';
12
+
13
+ const GET = 'GET' as const;
14
+
15
+ const axiosBaseQuery =
16
+ (): BaseQueryFn<
17
+ {
18
+ url: string;
19
+ method?: AxiosRequestConfig['method'];
20
+ data?: AxiosRequestConfig['data'];
21
+ params?: AxiosRequestConfig['params'];
22
+ headers?: AxiosRequestConfig['headers'];
23
+ showSuccessNotification?: boolean;
24
+ showFailureNotification?: boolean;
25
+ responseType?: ResponseType;
26
+ },
27
+ unknown,
28
+ unknown
29
+ > =>
30
+ async ({
31
+ url,
32
+ method,
33
+ data,
34
+ params,
35
+ headers,
36
+ showSuccessNotification: _showSuccess = false,
37
+ showFailureNotification: _showFailure = false,
38
+ responseType,
39
+ }) => {
40
+ try {
41
+ const requestConfig: AxiosRequestConfig =
42
+ method === GET
43
+ ? { url, method, params: params ?? data, headers }
44
+ : { url, method, data, params, headers };
45
+
46
+ if (responseType) {
47
+ requestConfig.responseType = responseType;
48
+ }
49
+
50
+ const result: AxiosResponse = await axiosInstance(requestConfig);
51
+
52
+ // Wire notification dispatch here once you have the slice:
53
+ // if (showSuccessNotification) {
54
+ // store.dispatch(setNotification({ type: 'success', message: result.data?.message }));
55
+ // }
56
+
57
+ return { data: result.data };
58
+ } catch (axiosError) {
59
+ const error = axiosError as ApiErrorShape;
60
+ // if (showFailureNotification) {
61
+ // store.dispatch(setNotification({ type: 'error', message: ... }));
62
+ // }
63
+ return {
64
+ error: {
65
+ status: error.response?.status,
66
+ data: error.response?.data?.errors,
67
+ },
68
+ };
69
+ }
70
+ };
71
+
72
+ export default axiosBaseQuery;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Response interceptor for the shared axios instance. Imported for its side
3
+ * effect from axiosInstance.ts. Pattern mirrors rsense-react-org.
4
+ *
5
+ * - 401: clear local auth state (handled by createAxios's onUnauthorized
6
+ * callback), dispatch error notification.
7
+ * - Other 4xx/5xx: surface the server message via the notification slice.
8
+ */
9
+ import type { AxiosError, AxiosResponse } from 'axios';
10
+ import axiosInstance from './axiosInstance.js';
11
+ // Wire your notification slice + store here. Placeholders below.
12
+ // import store from '../redux/store.js';
13
+ // import { setNotification } from '../shared/Notification/slice.js';
14
+
15
+ export interface ApiErrorShape {
16
+ config?: { url?: string };
17
+ response?: {
18
+ status?: number;
19
+ data?: {
20
+ errors?: Array<{ detail?: string; details?: string }> | Record<string, string[]>;
21
+ message?: string;
22
+ };
23
+ };
24
+ }
25
+
26
+ axiosInstance.interceptors.response.use(
27
+ (response: AxiosResponse) => response,
28
+ (error: AxiosError) => {
29
+ const err = error as unknown as ApiErrorShape;
30
+ const status = err.response?.status;
31
+
32
+ // Hook this up to your notification slice once you wire it.
33
+ // store.dispatch(setNotification({ type: 'error', message: extractMessage(err) }));
34
+
35
+ if (status === 401) {
36
+ // 401 is also handled by createAxios's onUnauthorized callback (clears token).
37
+ // Add your own redirect / dispatch here.
38
+ }
39
+
40
+ return Promise.reject(err);
41
+ },
42
+ );
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Cross-API cache invalidation middleware. Pattern from rsense-react-org:
3
+ * when one API's mutation fulfills, invalidate tags on related APIs.
4
+ *
5
+ * Add invalidation rules below. The default impl is a no-op; specialise as
6
+ * cross-API relationships emerge.
7
+ */
8
+ import type { Middleware } from '@reduxjs/toolkit';
9
+
10
+ const invalidateCacheMiddleware: Middleware = () => (next) => (action) => {
11
+ // Example pattern (uncomment + adapt):
12
+ //
13
+ // if (kycApi.endpoints.submitKyc.matchFulfilled(action)) {
14
+ // storeApi.dispatch(userApi.util.invalidateTags(['User']));
15
+ // }
16
+
17
+ return next(action);
18
+ };
19
+
20
+ export default invalidateCacheMiddleware;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Typed Redux hooks — use these instead of plain useDispatch / useSelector.
3
+ * Same pattern as rsense-react-org.
4
+ */
5
+ import { useDispatch, useSelector } from 'react-redux';
6
+ import type { TypedUseSelectorHook } from 'react-redux';
7
+ import type { RootState, AppDispatch } from './store.js';
8
+
9
+ export const useAppDispatch: () => AppDispatch = useDispatch;
10
+ export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Root reducer. Compose every slice + every RTK Query API's reducer here.
3
+ *
4
+ * As you add features, import the slice/api and add to the object below.
5
+ */
6
+ import { combineReducers } from '@reduxjs/toolkit';
7
+
8
+ const rootReducer = combineReducers({
9
+ // Slices:
10
+ // login: loginReducer,
11
+ // notification: notificationReducer,
12
+ //
13
+ // RTK Query API reducers (by their reducerPath):
14
+ // [loginApi.reducerPath]: loginApi.reducer,
15
+ // [kycApi.reducerPath]: kycApi.reducer,
16
+ });
17
+
18
+ export default rootReducer;
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Redux store. Mirrors rsense-react-org's pattern:
3
+ * - configureStore with rootReducer
4
+ * - middleware concat with each feature API's middleware (add as you create them)
5
+ * - setupListeners for RTK Query refetch-on-focus / reconnect
6
+ * - devTools enabled
7
+ *
8
+ * As you add features, import their api and:
9
+ * 1. Register the reducer in ./rootReducer.ts
10
+ * 2. Add the middleware below
11
+ */
12
+ import { configureStore } from '@reduxjs/toolkit';
13
+ import { setupListeners } from '@reduxjs/toolkit/query';
14
+ import rootReducer from './rootReducer.js';
15
+
16
+ const store = configureStore({
17
+ reducer: rootReducer,
18
+ middleware: (getDefaultMiddleware) =>
19
+ getDefaultMiddleware({
20
+ serializableCheck: {
21
+ ignoredActions: [],
22
+ },
23
+ }).concat([
24
+ // Append feature API middleware here, e.g.:
25
+ // loginApi.middleware,
26
+ // kycApi.middleware,
27
+ ]),
28
+ devTools: true,
29
+ });
30
+
31
+ setupListeners(store.dispatch);
32
+
33
+ export type AppDispatch = typeof store.dispatch;
34
+ export type RootState = ReturnType<typeof store.getState>;
35
+
36
+ export default store;