@thanhphi/create-360-miniapp 0.0.0-stage → 0.2.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 (81) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +79 -2
  3. package/bin/create-360-miniapp.cjs +54 -0
  4. package/lib/create.cjs +56 -0
  5. package/package.json +9 -4
  6. package/template/.eslintrc.cjs +1 -0
  7. package/template/.prettierrc.cjs +1 -0
  8. package/template/.watchmanconfig +1 -0
  9. package/template/App.tsx +2 -0
  10. package/template/README.md +234 -0
  11. package/template/__tests__/auth.test.ts +287 -0
  12. package/template/aliases.cjs +16 -0
  13. package/template/app.config.js +23 -0
  14. package/template/app.json +17 -0
  15. package/template/assets/.gitkeep +0 -0
  16. package/template/babel.config.js +7 -0
  17. package/template/declare/.gitkeep +0 -0
  18. package/template/declare/assets.d.ts +11 -0
  19. package/template/env/beta.json +16 -0
  20. package/template/env/prod.json +16 -0
  21. package/template/env/staging.json +16 -0
  22. package/template/gitignore +19 -0
  23. package/template/index.js +2 -0
  24. package/template/index.repack.js +14 -0
  25. package/template/jest.config.cjs +29 -0
  26. package/template/metro.config.js +6 -0
  27. package/template/package.json +88 -0
  28. package/template/pipelines/beta.yml +91 -0
  29. package/template/pipelines/production.yml +91 -0
  30. package/template/pipelines/staging.yml +91 -0
  31. package/template/pipelines/templates/build-and-zip.yml +89 -0
  32. package/template/pipelines/templates/upload-and-register.yml +89 -0
  33. package/template/react-native.config.js +3 -0
  34. package/template/rspack.config.cjs +53 -0
  35. package/template/scripts/clean-mini-app-output.js +9 -0
  36. package/template/scripts/sync-env.js +29 -0
  37. package/template/src/App.tsx +50 -0
  38. package/template/src/StandaloneApp.tsx +43 -0
  39. package/template/src/assets/icons/.gitkeep +1 -0
  40. package/template/src/assets/images/.gitkeep +1 -0
  41. package/template/src/assets/svg/.gitkeep +1 -0
  42. package/template/src/bootstrap.tsx +7 -0
  43. package/template/src/common/mini-app/provider.tsx +44 -0
  44. package/template/src/common/mini-app/types.ts +13 -0
  45. package/template/src/config/.gitkeep +0 -0
  46. package/template/src/constants/.gitkeep +0 -0
  47. package/template/src/hooks/.gitkeep +0 -0
  48. package/template/src/library/components/.gitkeep +0 -0
  49. package/template/src/library/networking/client.ts +53 -0
  50. package/template/src/library/networking/errors.ts +25 -0
  51. package/template/src/library/utils/responsive.ts +7 -0
  52. package/template/src/library/utils/storage/.gitkeep +0 -0
  53. package/template/src/modules/__MODULE__/assets/.gitkeep +0 -0
  54. package/template/src/modules/__MODULE__/components/ProfileCard.tsx +37 -0
  55. package/template/src/modules/__MODULE__/constants/.gitkeep +0 -0
  56. package/template/src/modules/__MODULE__/navigation/ModuleNavigator.tsx +18 -0
  57. package/template/src/modules/__MODULE__/redux/home-slice.ts +13 -0
  58. package/template/src/modules/__MODULE__/screens/home/hooks/use-home.ts +30 -0
  59. package/template/src/modules/__MODULE__/screens/home/index.tsx +97 -0
  60. package/template/src/modules/__MODULE__/services/profile.service.ts +32 -0
  61. package/template/src/modules/__MODULE__/types/.gitkeep +0 -0
  62. package/template/src/modules/auth/components/.gitkeep +0 -0
  63. package/template/src/modules/auth/constants/.gitkeep +0 -0
  64. package/template/src/modules/auth/hooks/use-auth.ts +55 -0
  65. package/template/src/modules/auth/navigation/AuthenticationNavigator.tsx +16 -0
  66. package/template/src/modules/auth/redux/auth-slice.ts +36 -0
  67. package/template/src/modules/auth/redux/selectors.ts +6 -0
  68. package/template/src/modules/auth/screens/LoginScreen.tsx +75 -0
  69. package/template/src/modules/auth/services/oidc.ts +64 -0
  70. package/template/src/modules/auth/services/standalone-session.ts +121 -0
  71. package/template/src/navigation/MainStack.tsx +46 -0
  72. package/template/src/redux/RootReducers.ts +8 -0
  73. package/template/src/redux/hooks.ts +5 -0
  74. package/template/src/redux/store.ts +10 -0
  75. package/template/src/services/application.ts +87 -0
  76. package/template/src/themes/colors/dark.ts +90 -0
  77. package/template/src/themes/colors/light.ts +90 -0
  78. package/template/src/themes/index.ts +38 -0
  79. package/template/src/themes/standalone.ts +6 -0
  80. package/template/src/types/.gitkeep +0 -0
  81. package/template/tsconfig.json +65 -0
@@ -0,0 +1,287 @@
1
+ import {
2
+ AuthError,
3
+ HttpError,
4
+ type HostCapabilities,
5
+ type SessionSnapshot,
6
+ } from '@thanhphi/miniapp-sdk';
7
+ import {
8
+ createStandaloneSession,
9
+ type OidcTokens,
10
+ } from '../src/modules/auth/services/standalone-session';
11
+ import { createApplicationServices } from '../src/services/application';
12
+ import { profileRequested } from '../src/modules/__MODULE__/redux/home-slice';
13
+ import { refresh as nativeRefresh } from 'react-native-app-auth';
14
+ import {
15
+ isOidcConfigured,
16
+ oidcOperations,
17
+ } from '../src/modules/auth/services/oidc';
18
+
19
+ jest.mock('react-native-app-auth', () => ({
20
+ authorize: jest.fn(),
21
+ refresh: jest.fn(),
22
+ }));
23
+ jest.mock('@env-config', () => ({
24
+ __esModule: true,
25
+ default: {
26
+ ENV: 'staging',
27
+ BASE_URL: 'https://api.example.test',
28
+ API_TIMEOUT_MS: 1000,
29
+ AUTH: { issuer: '', clientId: '', redirectUrl: '', scopes: ['openid'] },
30
+ },
31
+ }));
32
+
33
+ function deferred<T>() {
34
+ let resolve!: (value: T) => void;
35
+ let reject!: (error: unknown) => void;
36
+ const promise = new Promise<T>((yes, no) => {
37
+ resolve = yes;
38
+ reject = no;
39
+ });
40
+ return { promise, resolve, reject };
41
+ }
42
+
43
+ function testHost(initial: SessionSnapshot) {
44
+ let value = initial;
45
+ const listeners = new Set<(snapshot: SessionSnapshot) => void>();
46
+ const capabilities: HostCapabilities = {
47
+ auth: {
48
+ getSnapshot: () => ({ ...value }),
49
+ refreshIfNeeded: jest.fn(async () => value.accessToken),
50
+ subscribe(listener) {
51
+ listeners.add(listener);
52
+ return () => {
53
+ listeners.delete(listener);
54
+ };
55
+ },
56
+ },
57
+ closeMiniApp: jest.fn(),
58
+ };
59
+ return {
60
+ capabilities,
61
+ listeners,
62
+ emit(next: SessionSnapshot) {
63
+ value = next;
64
+ for (const listener of listeners) listener({ ...next });
65
+ },
66
+ };
67
+ }
68
+
69
+ describe('standalone session', () => {
70
+ test('rejects example OIDC values and maps revoked grants distinctly from network failures', async () => {
71
+ expect(
72
+ isOidcConfigured({
73
+ issuer: 'https://YOUR_OIDC_ISSUER',
74
+ clientId: 'YOUR_CLIENT_ID',
75
+ redirectUrl: 'sample://oauth',
76
+ scopes: ['openid'],
77
+ }),
78
+ ).toBe(false);
79
+ expect(
80
+ isOidcConfigured({
81
+ issuer: 'https://identity.example.test',
82
+ clientId: 'mobile',
83
+ redirectUrl: 'sample://oauth',
84
+ scopes: ['openid'],
85
+ }),
86
+ ).toBe(true);
87
+ const native = jest.mocked(nativeRefresh);
88
+ native.mockRejectedValueOnce({ code: 'invalid_grant' });
89
+ await expect(oidcOperations.refresh('revoked')).rejects.toMatchObject({
90
+ kind: 'expired',
91
+ });
92
+ native.mockRejectedValueOnce({ code: 'token_refresh_failed' });
93
+ await expect(oidcOperations.refresh('offline')).rejects.toMatchObject({
94
+ kind: 'unavailable',
95
+ });
96
+ });
97
+ test('coalesces a refresh and blocks its token write after local logout', async () => {
98
+ const response = deferred<OidcTokens>();
99
+ const refresh = jest.fn(() => response.promise);
100
+ const session = createStandaloneSession({
101
+ isConfigured: true,
102
+ authorize: async () => ({
103
+ accessToken: 'first-token',
104
+ refreshToken: 'private-refresh',
105
+ }),
106
+ refresh,
107
+ });
108
+ await session.actions.signIn();
109
+ const before = session.capabilities.auth.getSnapshot();
110
+ const first = session.capabilities.auth.refreshIfNeeded({
111
+ sessionId: before.sessionId,
112
+ failedAccessToken: before.accessToken,
113
+ });
114
+ const second = session.capabilities.auth.refreshIfNeeded({
115
+ sessionId: before.sessionId,
116
+ failedAccessToken: before.accessToken,
117
+ });
118
+ expect(first).toBe(second);
119
+ await Promise.resolve();
120
+ expect(refresh).toHaveBeenCalledTimes(1);
121
+ const checked = expect(first).rejects.toMatchObject({
122
+ kind: 'session-changed',
123
+ });
124
+ session.actions.signOut();
125
+ response.resolve({
126
+ accessToken: 'old-account-token',
127
+ refreshToken: 'old-refresh',
128
+ });
129
+ await checked;
130
+ expect(session.capabilities.auth.getSnapshot()).toEqual({
131
+ sessionId: null,
132
+ accessToken: null,
133
+ });
134
+ session.dispose();
135
+ });
136
+
137
+ test('blocks a late login result after logout', async () => {
138
+ const response = deferred<OidcTokens>();
139
+ const session = createStandaloneSession({
140
+ isConfigured: true,
141
+ authorize: () => response.promise,
142
+ refresh: jest.fn(),
143
+ });
144
+ const login = session.actions.signIn();
145
+ await Promise.resolve();
146
+ const checked = expect(login).rejects.toMatchObject({
147
+ kind: 'session-changed',
148
+ });
149
+ session.actions.signOut();
150
+ response.resolve({ accessToken: 'late-token' });
151
+ await checked;
152
+ expect(session.capabilities.auth.getSnapshot().accessToken).toBeNull();
153
+ session.dispose();
154
+ });
155
+
156
+ test('requires configured OIDC and never synthesizes credentials', async () => {
157
+ const authorize = jest.fn();
158
+ const session = createStandaloneSession({
159
+ isConfigured: false,
160
+ authorize,
161
+ refresh: jest.fn(),
162
+ });
163
+ await expect(session.actions.signIn()).rejects.toBeInstanceOf(AuthError);
164
+ expect(authorize).not.toHaveBeenCalled();
165
+ expect(session.capabilities.auth.getSnapshot()).toEqual({
166
+ sessionId: null,
167
+ accessToken: null,
168
+ });
169
+ session.dispose();
170
+ });
171
+ });
172
+
173
+ describe('application resources', () => {
174
+ const clients: ReturnType<typeof createApplicationServices>[] = [];
175
+ const originalFetch = globalThis.fetch;
176
+ beforeEach(() => {
177
+ globalThis.fetch = jest.fn();
178
+ });
179
+ afterEach(() => {
180
+ clients.splice(0).forEach(client => client.dispose());
181
+ globalThis.fetch = originalFetch;
182
+ });
183
+ const create = (host: HostCapabilities) => {
184
+ const client = createApplicationServices(host);
185
+ clients.push(client);
186
+ return client;
187
+ };
188
+
189
+ test('isolates instances; rotation retains caches; account change resets Redux and queries', () => {
190
+ const host = testHost({ sessionId: 'alice', accessToken: 'first-token' });
191
+ const first = create(host.capabilities);
192
+ const second = create(host.capabilities);
193
+ first.store.dispatch(profileRequested());
194
+ first.queryClient.setQueryData(['profile', 'alice'], { name: 'Alice' });
195
+ expect(second.store.getState().home.profileRequested).toBe(false);
196
+ expect(
197
+ second.queryClient.getQueryData(['profile', 'alice']),
198
+ ).toBeUndefined();
199
+ expect(first.store.getState().auth).not.toHaveProperty('accessToken');
200
+ expect(JSON.stringify(first.store.getState())).not.toContain('first-token');
201
+ const dispatched = jest.spyOn(first.store, 'dispatch');
202
+ host.emit({ sessionId: 'alice', accessToken: 'rotated-token' });
203
+ expect(JSON.stringify(dispatched.mock.calls)).not.toContain(
204
+ 'rotated-token',
205
+ );
206
+ expect(JSON.stringify(first.store.getState())).not.toContain('rotated-token');
207
+ expect(first.queryClient.getQueryData(['profile', 'alice'])).toEqual({
208
+ name: 'Alice',
209
+ });
210
+ expect(first.store.getState().home.profileRequested).toBe(true);
211
+ host.emit({ sessionId: 'bob', accessToken: 'bob-token' });
212
+ expect(
213
+ first.queryClient.getQueryData(['profile', 'alice']),
214
+ ).toBeUndefined();
215
+ expect(first.store.getState().home.profileRequested).toBe(false);
216
+ expect(first.store.getState().auth.sessionId).toBe('bob');
217
+ expect(JSON.stringify(first.store.getState())).not.toContain('bob-token');
218
+ });
219
+
220
+ test('dispose cancels a pending HTTP request, removes subscriptions and permits a fresh mount', async () => {
221
+ globalThis.fetch = jest.fn(() => new Promise<Response>(() => {}));
222
+ const host = testHost({ sessionId: 'alice', accessToken: 'alice-token' });
223
+ const first = create(host.capabilities);
224
+ const request = first.api.get('/me');
225
+ const checked = expect(request).rejects.toMatchObject({
226
+ kind: 'disposed',
227
+ } satisfies Partial<HttpError>);
228
+ await Promise.resolve();
229
+ first.dispose();
230
+ await checked;
231
+ expect(host.listeners.size).toBe(0);
232
+ expect(first.store.getState().auth.authenticated).toBe(false);
233
+ const next = create(host.capabilities);
234
+ expect(next.store.getState().auth.authenticated).toBe(true);
235
+ expect(next.store).not.toBe(first.store);
236
+ expect(host.listeners.size).toBe(1);
237
+ });
238
+
239
+ test('HTTP 401 retries once then clears user state when refresh still expires', async () => {
240
+ const unauthorized = {
241
+ status: 401,
242
+ ok: false,
243
+ headers: new Headers(),
244
+ url: '',
245
+ text: async () => '{}',
246
+ } as Response;
247
+ globalThis.fetch = jest.fn(async () => unauthorized);
248
+ const host = testHost({ sessionId: 'alice', accessToken: 'alice-token' });
249
+ const client = create(host.capabilities);
250
+ client.store.dispatch(profileRequested());
251
+ client.queryClient.setQueryData(['profile', 'alice'], { name: 'Alice' });
252
+ await expect(client.api.get('/me')).rejects.toMatchObject({
253
+ kind: 'expired',
254
+ });
255
+ expect(globalThis.fetch).toHaveBeenCalledTimes(2);
256
+ expect(client.store.getState().home.profileRequested).toBe(false);
257
+ expect(
258
+ client.queryClient.getQueryData(['profile', 'alice']),
259
+ ).toBeUndefined();
260
+ expect(client.store.getState().auth.authenticated).toBe(false);
261
+ expect(client.store.getState().auth.message).toContain('hết hạn');
262
+ });
263
+
264
+ test('all HTTP verbs inject the current host token and return parsed data', async () => {
265
+ const response = {
266
+ status: 200,
267
+ ok: true,
268
+ headers: new Headers(),
269
+ url: '',
270
+ text: async () => '{"ok":true}',
271
+ } as Response;
272
+ const fetch = jest.fn(async () => response);
273
+ globalThis.fetch = fetch;
274
+ const host = testHost({ sessionId: 'alice', accessToken: 'alice-token' });
275
+ const client = create(host.capabilities);
276
+ for (const method of ['get', 'post', 'put', 'patch', 'delete'] as const) {
277
+ await expect(client.api[method]('/me')).resolves.toEqual({ ok: true });
278
+ const [, init] = fetch.mock.calls[
279
+ fetch.mock.calls.length - 1
280
+ ] as unknown as [string, RequestInit];
281
+ expect(init.method).toBe(method.toUpperCase());
282
+ expect(new Headers(init.headers).get('Authorization')).toBe(
283
+ 'Bearer alice-token',
284
+ );
285
+ }
286
+ });
287
+ });
@@ -0,0 +1,16 @@
1
+ module.exports = {
2
+ "@": "./src",
3
+ "@auth": "./src/modules/auth",
4
+ "@feature": "./src/modules/__MODULE__",
5
+ "@assets": "./src/assets",
6
+ "@common": "./src/common",
7
+ "@components": "./src/library/components",
8
+ "@env-config": "./env/env-config",
9
+ "@modules": "./src/modules",
10
+ "@navigation": "./src/navigation",
11
+ "@networking": "./src/library/networking",
12
+ "@redux": "./src/redux",
13
+ "@storage": "./src/library/utils/storage",
14
+ "@theme": "./src/themes",
15
+ "@utils": "./src/library/utils"
16
+ };
@@ -0,0 +1,23 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+ module.exports = ({ config }) => {
4
+ const selected = path.join(__dirname, 'env/selected.json');
5
+ const flavor =
6
+ process.env.APP_ENV ||
7
+ (fs.existsSync(selected)
8
+ ? JSON.parse(fs.readFileSync(selected, 'utf8')).flavor
9
+ : 'staging');
10
+ if (!['staging', 'beta', 'prod'].includes(flavor))
11
+ throw new Error('APP_ENV must be staging, beta or prod');
12
+ const input = JSON.parse(
13
+ fs.readFileSync(path.join(__dirname, 'env', `${flavor}.json`), 'utf8'),
14
+ );
15
+ const env = input.public || input;
16
+ return {
17
+ ...config,
18
+ plugins: [
19
+ ...(config.plugins || []),
20
+ ['react-native-app-auth', { redirectUrls: [env.AUTH.redirectUrl] }],
21
+ ],
22
+ };
23
+ };
@@ -0,0 +1,17 @@
1
+ {
2
+ "expo": {
3
+ "name": "__DISPLAY_NAME__",
4
+ "slug": "__PACKAGE_NAME__",
5
+ "version": "0.0.1",
6
+ "scheme": "__BUNDLE_ID__",
7
+ "ios": {
8
+ "bundleIdentifier": "__BUNDLE_ID__"
9
+ },
10
+ "android": {
11
+ "package": "__BUNDLE_ID__"
12
+ },
13
+ "plugins": [
14
+ "expo-dev-client"
15
+ ]
16
+ }
17
+ }
File without changes
@@ -0,0 +1,7 @@
1
+ module.exports = {
2
+ presets: ['babel-preset-expo'],
3
+ plugins: [['module-resolver', {
4
+ extensions: ['.ios.ts', '.android.ts', '.ts', '.ios.tsx', '.android.tsx', '.tsx', '.ios.js', '.android.js', '.js', '.json'],
5
+ alias: require('./aliases.cjs'),
6
+ }], ['react-native-unistyles/plugin', { root: 'src' }], 'react-native-worklets/plugin'],
7
+ };
File without changes
@@ -0,0 +1,11 @@
1
+ declare module '*.svg' {
2
+ import type { FunctionComponent } from 'react';
3
+ import type { SvgProps } from 'react-native-svg';
4
+ const Component: FunctionComponent<SvgProps>;
5
+ export default Component;
6
+ }
7
+ declare module '*.png' {
8
+ import type { ImageSourcePropType } from 'react-native';
9
+ const source: ImageSourcePropType;
10
+ export default source;
11
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "ENV": "beta",
3
+ "BASE_URL": "https://api.beta.example.com",
4
+ "API_TIMEOUT_MS": 15000,
5
+ "AUTH": {
6
+ "issuer": "https://login.microsoftonline.com/YOUR_TENANT_ID/v2.0",
7
+ "clientId": "YOUR_CLIENT_ID",
8
+ "redirectUrl": "__BUNDLE_ID__.auth://oauth/redirect",
9
+ "scopes": [
10
+ "openid",
11
+ "profile",
12
+ "offline_access",
13
+ "api://YOUR_API_CLIENT_ID/access_as_user"
14
+ ]
15
+ }
16
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "ENV": "prod",
3
+ "BASE_URL": "https://api.prod.example.com",
4
+ "API_TIMEOUT_MS": 15000,
5
+ "AUTH": {
6
+ "issuer": "https://login.microsoftonline.com/YOUR_TENANT_ID/v2.0",
7
+ "clientId": "YOUR_CLIENT_ID",
8
+ "redirectUrl": "__BUNDLE_ID__.auth://oauth/redirect",
9
+ "scopes": [
10
+ "openid",
11
+ "profile",
12
+ "offline_access",
13
+ "api://YOUR_API_CLIENT_ID/access_as_user"
14
+ ]
15
+ }
16
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "ENV": "staging",
3
+ "BASE_URL": "https://api.staging.example.com",
4
+ "API_TIMEOUT_MS": 15000,
5
+ "AUTH": {
6
+ "issuer": "https://login.microsoftonline.com/YOUR_TENANT_ID/v2.0",
7
+ "clientId": "YOUR_CLIENT_ID",
8
+ "redirectUrl": "__BUNDLE_ID__.auth://oauth/redirect",
9
+ "scopes": [
10
+ "openid",
11
+ "profile",
12
+ "offline_access",
13
+ "api://YOUR_API_CLIENT_ID/access_as_user"
14
+ ]
15
+ }
16
+ }
@@ -0,0 +1,19 @@
1
+ node_modules/
2
+ .expo/
3
+ build/
4
+ coverage/
5
+ *.tgz
6
+ .DS_Store
7
+ .env*
8
+ env/env-config.ts
9
+ env/selected.json
10
+ # Commit package-lock.json after npm install.
11
+ # Native projects are generated by npx expo prebuild; commit them if customizing native code.
12
+ ios/Pods/
13
+ ios/build/
14
+ android/.gradle/
15
+ android/app/build/
16
+ android/build/
17
+ android/local.properties
18
+ *.keystore
19
+ *.jks
@@ -0,0 +1,2 @@
1
+ import 'react-native-gesture-handler';
2
+ import './src/bootstrap';
@@ -0,0 +1,14 @@
1
+ import { Script, ScriptManager } from '@callstack/repack/client';
2
+
3
+ if (__DEV__) {
4
+ ScriptManager.shared.addResolver(
5
+ async scriptId => ({
6
+ url: Script.getDevServerURL(scriptId),
7
+ cache: false,
8
+ }),
9
+ { key: '__APP_NAME__-dev-chunks', priority: 1 },
10
+ );
11
+ }
12
+
13
+ // Keep app/native modules behind the federation bootstrap boundary.
14
+ import('./src/bootstrap');
@@ -0,0 +1,29 @@
1
+ const aliases = require('./aliases.cjs');
2
+ const moduleNameMapper = {};
3
+ for (const [alias, target] of Object.entries(aliases)) {
4
+ const escaped = alias.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
5
+ moduleNameMapper[`^${escaped}$`] = `<rootDir>/${target.replace(/^\.\//, '')}`;
6
+ moduleNameMapper[`^${escaped}/(.*)$`] = `<rootDir>/${target.replace(
7
+ /^\.\//,
8
+ '',
9
+ )}/$1`;
10
+ }
11
+
12
+ module.exports = {
13
+ preset: 'react-native',
14
+ testMatch: ['<rootDir>/__tests__/**/*.test.[jt]s?(x)'],
15
+ transform: {
16
+ '^.+\\.[jt]sx?$': [
17
+ 'babel-jest',
18
+ {
19
+ presets: ['module:@react-native/babel-preset'],
20
+ babelrc: false,
21
+ configFile: false,
22
+ },
23
+ ],
24
+ },
25
+ transformIgnorePatterns: [
26
+ 'node_modules/(?!((jest-)?react-native|@react-native|@thanhphi/miniapp-sdk|immer)/)',
27
+ ],
28
+ moduleNameMapper,
29
+ };
@@ -0,0 +1,6 @@
1
+ const { getDefaultConfig } = require('expo/metro-config');
2
+ const config = getDefaultConfig(__dirname);
3
+ config.transformer.babelTransformerPath = require.resolve('react-native-svg-transformer/expo');
4
+ config.resolver.assetExts = config.resolver.assetExts.filter(ext => ext !== 'svg');
5
+ config.resolver.sourceExts.push('svg');
6
+ module.exports = config;
@@ -0,0 +1,88 @@
1
+ {
2
+ "name": "__PACKAGE_NAME__",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "main": "index.js",
6
+ "scripts": {
7
+ "android": "expo run:android",
8
+ "ios": "expo run:ios",
9
+ "lint": "eslint src App.tsx",
10
+ "start": "expo start",
11
+ "start:expo": "expo start",
12
+ "start:mf": "REPACK=1 react-native start --port __PORT__ --config rspack.config.cjs",
13
+ "start:mf:device": "REPACK=1 react-native start --host 0.0.0.0 --port __PORT__ --config rspack.config.cjs",
14
+ "start:mf:reset": "REPACK=1 react-native start --port __PORT__ --config rspack.config.cjs --reset-cache",
15
+ "bundle:mf:ios": "node scripts/clean-mini-app-output.js ios && REPACK=1 react-native bundle --platform ios --dev false --minify true --entry-file index.repack.js --config rspack.config.cjs --bundle-output build/generated/ios/index.bundle --assets-dest build/generated/ios",
16
+ "bundle:mf:android": "node scripts/clean-mini-app-output.js android && REPACK=1 react-native bundle --platform android --dev false --minify true --entry-file index.repack.js --config rspack.config.cjs --bundle-output build/generated/android/index.bundle --assets-dest build/generated/android",
17
+ "env:staging": "node scripts/sync-env.js staging",
18
+ "env:beta": "node scripts/sync-env.js beta",
19
+ "env:prod": "node scripts/sync-env.js prod",
20
+ "start:staging": "node scripts/sync-env.js staging && expo start",
21
+ "start:beta": "node scripts/sync-env.js beta && expo start",
22
+ "start:prod": "node scripts/sync-env.js prod && expo start",
23
+ "ios:staging": "node scripts/sync-env.js staging && expo run:ios",
24
+ "ios:beta": "node scripts/sync-env.js beta && expo run:ios",
25
+ "ios:prod": "node scripts/sync-env.js prod && expo run:ios",
26
+ "android:staging": "node scripts/sync-env.js staging && expo run:android",
27
+ "android:beta": "node scripts/sync-env.js beta && expo run:android",
28
+ "android:prod": "node scripts/sync-env.js prod && expo run:android",
29
+ "typecheck": "tsc --noEmit",
30
+ "test": "jest --runInBand --watchman=false"
31
+ },
32
+ "dependencies": {
33
+ "@react-navigation/native": "7.2.2",
34
+ "@react-navigation/native-stack": "7.14.12",
35
+ "@reduxjs/toolkit": "2.11.2",
36
+ "@tanstack/react-query": "5.62.11",
37
+ "@thanhphi/miniapp-sdk": "0.1.0",
38
+ "@thanhphi/ui360": "0.2.0",
39
+ "expo": "~55.0.0",
40
+ "expo-dev-client": "~55.0.32",
41
+ "react": "19.2.0",
42
+ "react-native": "0.83.6",
43
+ "react-native-app-auth": "8.1.0",
44
+ "react-native-calendars": "1.1314.0",
45
+ "react-native-gesture-handler": "~2.30.0",
46
+ "react-native-reanimated": "4.3.1",
47
+ "react-native-safe-area-context": "~5.6.2",
48
+ "react-native-screens": "~4.23.0",
49
+ "react-native-svg": "15.15.4",
50
+ "react-native-worklets": "0.8.1",
51
+ "react-redux": "9.2.0",
52
+ "react-native-unistyles": "3.2.5",
53
+ "react-native-nitro-modules": "0.36.5",
54
+ "expo-image": "~55.0.10"
55
+ },
56
+ "devDependencies": {
57
+ "@babel/core": "^7.25.2",
58
+ "@babel/runtime": "^7.25.0",
59
+ "@callstack/repack": "^5.2.5",
60
+ "@callstack/repack-plugin-expo-modules": "^5.2.5",
61
+ "@callstack/repack-plugin-reanimated": "^5.2.5",
62
+ "@module-federation/enhanced": "^0.19.1",
63
+ "@react-native-community/cli": "20.0.0",
64
+ "@react-native-community/cli-platform-android": "20.0.0",
65
+ "@react-native-community/cli-platform-ios": "20.0.0",
66
+ "@react-native/babel-preset": "0.83.6",
67
+ "@react-native/eslint-config": "0.83.6",
68
+ "@react-native/metro-config": "0.83.6",
69
+ "@react-native/typescript-config": "0.83.6",
70
+ "@rspack/core": "1.7.12",
71
+ "@svgr/webpack": "8.1.0",
72
+ "@types/jest": "29.5.13",
73
+ "@types/react": "19.2.0",
74
+ "@types/react-test-renderer": "19.1.0",
75
+ "babel-jest": "29.6.3",
76
+ "babel-plugin-module-resolver": "5.0.3",
77
+ "babel-preset-expo": "~55.0.8",
78
+ "eslint": "8.19.0",
79
+ "jest": "29.6.3",
80
+ "prettier": "2.8.8",
81
+ "react-native-svg-transformer": "1.5.3",
82
+ "react-test-renderer": "19.2.0",
83
+ "typescript": "5.8.3"
84
+ },
85
+ "engines": {
86
+ "node": ">=20"
87
+ }
88
+ }