@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,91 @@
1
+ # Configure optional deployment variables in Azure Pipelines or add a variable
2
+ # group below. Build-only CI needs no Azure connection, storage or database.
3
+ parameters:
4
+ - name: deploy
5
+ displayName: 'Upload and register the built mini app'
6
+ type: boolean
7
+ default: false
8
+ - name: agentPool
9
+ type: object
10
+ default:
11
+ vmImage: 'ubuntu-latest'
12
+ - name: nodeVersion
13
+ type: string
14
+ default: '22.x'
15
+
16
+ pool: ${{ parameters.agentPool }}
17
+
18
+ trigger:
19
+ branches:
20
+ include:
21
+ - beta
22
+ paths:
23
+ include:
24
+ - src/**
25
+ - assets/**
26
+ - declare/**
27
+ - env/**
28
+ - android/**
29
+ - ios/**
30
+ - scripts/**
31
+ - plugins/**
32
+ - pipelines/**
33
+ - __tests__/**
34
+ - package.json
35
+ - package-lock.json
36
+ - yarn.lock
37
+ - App.tsx
38
+ - aliases.cjs
39
+ - index.js
40
+ - index.repack.js
41
+ - app.json
42
+ - app.config.*
43
+ - babel.config.*
44
+ - metro.config.*
45
+ - rspack.config.*
46
+ - react-native.config.*
47
+ - tsconfig*.json
48
+ - jest.config.*
49
+ - jest.setup.*
50
+ - eslint.config.*
51
+ - .eslintrc*
52
+ - .npmrc
53
+
54
+ variables:
55
+ # Add your environment-specific Azure variable group here when deploy = true:
56
+ # - group: '<your-miniapp-variable-group>'
57
+ - name: envBuild
58
+ value: 'beta'
59
+ - name: envFolder
60
+ value: 'beta'
61
+ - name: appName
62
+ value: '__APP_NAME__'
63
+ - name: appId
64
+ value: '__APP_ID__'
65
+ - name: appCode
66
+ value: '__APP_CODE__'
67
+ - name: miniAppVersion
68
+ value: '$(Build.BuildNumber)'
69
+
70
+ stages:
71
+ - stage: Build
72
+ displayName: 'Verify and bundle beta mini app'
73
+ jobs:
74
+ - job: Build
75
+ displayName: 'Build Android and iOS remotes'
76
+ steps:
77
+ - checkout: self
78
+ clean: true
79
+ - template: templates/build-and-zip.yml
80
+ parameters:
81
+ nodeVersion: '${{ parameters.nodeVersion }}'
82
+ - ${{ if eq(parameters.deploy, true) }}:
83
+ - template: templates/upload-and-register.yml
84
+ parameters:
85
+ platform: android
86
+ checksum: '$(CHECKSUMANDROID)'
87
+ - template: templates/upload-and-register.yml
88
+ parameters:
89
+ platform: ios
90
+ checksum: '$(CHECKSUMIOS)'
91
+
@@ -0,0 +1,91 @@
1
+ # Configure optional deployment variables in Azure Pipelines or add a variable
2
+ # group below. Build-only CI needs no Azure connection, storage or database.
3
+ parameters:
4
+ - name: deploy
5
+ displayName: 'Upload and register the built mini app'
6
+ type: boolean
7
+ default: false
8
+ - name: agentPool
9
+ type: object
10
+ default:
11
+ vmImage: 'ubuntu-latest'
12
+ - name: nodeVersion
13
+ type: string
14
+ default: '22.x'
15
+
16
+ pool: ${{ parameters.agentPool }}
17
+
18
+ trigger:
19
+ tags:
20
+ include:
21
+ - '*'
22
+ paths:
23
+ include:
24
+ - src/**
25
+ - assets/**
26
+ - declare/**
27
+ - env/**
28
+ - android/**
29
+ - ios/**
30
+ - scripts/**
31
+ - plugins/**
32
+ - pipelines/**
33
+ - __tests__/**
34
+ - package.json
35
+ - package-lock.json
36
+ - yarn.lock
37
+ - App.tsx
38
+ - aliases.cjs
39
+ - index.js
40
+ - index.repack.js
41
+ - app.json
42
+ - app.config.*
43
+ - babel.config.*
44
+ - metro.config.*
45
+ - rspack.config.*
46
+ - react-native.config.*
47
+ - tsconfig*.json
48
+ - jest.config.*
49
+ - jest.setup.*
50
+ - eslint.config.*
51
+ - .eslintrc*
52
+ - .npmrc
53
+
54
+ variables:
55
+ # Add your environment-specific Azure variable group here when deploy = true:
56
+ # - group: '<your-miniapp-variable-group>'
57
+ - name: envBuild
58
+ value: 'prod'
59
+ - name: envFolder
60
+ value: 'production'
61
+ - name: appName
62
+ value: '__APP_NAME__'
63
+ - name: appId
64
+ value: '__APP_ID__'
65
+ - name: appCode
66
+ value: '__APP_CODE__'
67
+ - name: miniAppVersion
68
+ value: '$(Build.BuildNumber)'
69
+
70
+ stages:
71
+ - stage: Build
72
+ displayName: 'Verify and bundle production mini app'
73
+ jobs:
74
+ - job: Build
75
+ displayName: 'Build Android and iOS remotes'
76
+ steps:
77
+ - checkout: self
78
+ clean: true
79
+ - template: templates/build-and-zip.yml
80
+ parameters:
81
+ nodeVersion: '${{ parameters.nodeVersion }}'
82
+ - ${{ if eq(parameters.deploy, true) }}:
83
+ - template: templates/upload-and-register.yml
84
+ parameters:
85
+ platform: android
86
+ checksum: '$(CHECKSUMANDROID)'
87
+ - template: templates/upload-and-register.yml
88
+ parameters:
89
+ platform: ios
90
+ checksum: '$(CHECKSUMIOS)'
91
+
@@ -0,0 +1,91 @@
1
+ # Configure optional deployment variables in Azure Pipelines or add a variable
2
+ # group below. Build-only CI needs no Azure connection, storage or database.
3
+ parameters:
4
+ - name: deploy
5
+ displayName: 'Upload and register the built mini app'
6
+ type: boolean
7
+ default: false
8
+ - name: agentPool
9
+ type: object
10
+ default:
11
+ vmImage: 'ubuntu-latest'
12
+ - name: nodeVersion
13
+ type: string
14
+ default: '22.x'
15
+
16
+ pool: ${{ parameters.agentPool }}
17
+
18
+ trigger:
19
+ branches:
20
+ include:
21
+ - staging
22
+ paths:
23
+ include:
24
+ - src/**
25
+ - assets/**
26
+ - declare/**
27
+ - env/**
28
+ - android/**
29
+ - ios/**
30
+ - scripts/**
31
+ - plugins/**
32
+ - pipelines/**
33
+ - __tests__/**
34
+ - package.json
35
+ - package-lock.json
36
+ - yarn.lock
37
+ - App.tsx
38
+ - aliases.cjs
39
+ - index.js
40
+ - index.repack.js
41
+ - app.json
42
+ - app.config.*
43
+ - babel.config.*
44
+ - metro.config.*
45
+ - rspack.config.*
46
+ - react-native.config.*
47
+ - tsconfig*.json
48
+ - jest.config.*
49
+ - jest.setup.*
50
+ - eslint.config.*
51
+ - .eslintrc*
52
+ - .npmrc
53
+
54
+ variables:
55
+ # Add your environment-specific Azure variable group here when deploy = true:
56
+ # - group: '<your-miniapp-variable-group>'
57
+ - name: envBuild
58
+ value: 'staging'
59
+ - name: envFolder
60
+ value: 'staging'
61
+ - name: appName
62
+ value: '__APP_NAME__'
63
+ - name: appId
64
+ value: '__APP_ID__'
65
+ - name: appCode
66
+ value: '__APP_CODE__'
67
+ - name: miniAppVersion
68
+ value: '$(Build.BuildNumber)'
69
+
70
+ stages:
71
+ - stage: Build
72
+ displayName: 'Verify and bundle staging mini app'
73
+ jobs:
74
+ - job: Build
75
+ displayName: 'Build Android and iOS remotes'
76
+ steps:
77
+ - checkout: self
78
+ clean: true
79
+ - template: templates/build-and-zip.yml
80
+ parameters:
81
+ nodeVersion: '${{ parameters.nodeVersion }}'
82
+ - ${{ if eq(parameters.deploy, true) }}:
83
+ - template: templates/upload-and-register.yml
84
+ parameters:
85
+ platform: android
86
+ checksum: '$(CHECKSUMANDROID)'
87
+ - template: templates/upload-and-register.yml
88
+ parameters:
89
+ platform: ios
90
+ checksum: '$(CHECKSUMIOS)'
91
+
@@ -0,0 +1,89 @@
1
+ parameters:
2
+ - name: nodeVersion
3
+ type: string
4
+ default: '22.x'
5
+
6
+ steps:
7
+ - task: NodeTool@0
8
+ displayName: 'Use Node.js ${{ parameters.nodeVersion }}'
9
+ inputs:
10
+ versionSpec: '${{ parameters.nodeVersion }}'
11
+
12
+ - task: Bash@3
13
+ displayName: 'Install, verify and bundle mini app'
14
+ inputs:
15
+ targetType: 'inline'
16
+ workingDirectory: '$(Build.SourcesDirectory)'
17
+ script: |
18
+ set -euo pipefail
19
+
20
+ if [ -f package-lock.json ]; then
21
+ npm ci
22
+ else
23
+ # The scaffold has no dependency lock yet. Commit the first generated
24
+ # package-lock.json so subsequent builds use reproducible npm ci.
25
+ echo 'No package-lock.json found; using npm install for the first build.'
26
+ npm install
27
+ fi
28
+
29
+ node scripts/sync-env.js "$MINIAPP_ENV_BUILD"
30
+ npm run typecheck
31
+ npm run lint
32
+ npm run test
33
+ npm run bundle:mf:android
34
+ npm run bundle:mf:ios
35
+ env:
36
+ CI: 'true'
37
+ MINIAPP_ENV_BUILD: '$(envBuild)'
38
+
39
+ - task: Bash@3
40
+ displayName: 'Archive remote bundles and compute SHA-1'
41
+ inputs:
42
+ targetType: 'inline'
43
+ workingDirectory: '$(Build.SourcesDirectory)'
44
+ script: |
45
+ set -euo pipefail
46
+
47
+ if [[ ! "$MINIAPP_VERSION" =~ ^[A-Za-z0-9._-]+$ ]]; then
48
+ echo 'miniAppVersion must contain only letters, digits, dots, underscores or hyphens.' >&2
49
+ exit 1
50
+ fi
51
+
52
+ mkdir -p "$ARTIFACT_DIRECTORY"
53
+ for platform in android ios; do
54
+ remoteDirectory="$BUILD_CONTEXT/build/outputs/$platform/remotes"
55
+ archiveDirectory="$BUILD_CONTEXT/build/generated/$platform"
56
+ archive="$archiveDirectory/$MINIAPP_VERSION.zip"
57
+ test -f "$remoteDirectory/$MINIAPP_CONTAINER_NAME.container.js.bundle"
58
+ mkdir -p "$archiveDirectory" "$ARTIFACT_DIRECTORY/$platform"
59
+
60
+ (
61
+ cd "$remoteDirectory"
62
+ # A container may depend on every other chunk in this directory.
63
+ # generated/ contains standalone/compiler output and is not shipped.
64
+ zip -r "$archive" . -x '*.map'
65
+ )
66
+
67
+ checksum=$(sha1sum "$archive" | awk '{print $1}')
68
+ if [ "$platform" = android ]; then
69
+ echo "##vso[task.setvariable variable=CHECKSUMANDROID]$checksum"
70
+ else
71
+ echo "##vso[task.setvariable variable=CHECKSUMIOS]$checksum"
72
+ fi
73
+
74
+ cp "$archive" "$ARTIFACT_DIRECTORY/$platform/"
75
+ printf '%s %s.zip\n' "$checksum" "$MINIAPP_VERSION" > "$ARTIFACT_DIRECTORY/$platform/$MINIAPP_VERSION.zip.sha1"
76
+ done
77
+ env:
78
+ BUILD_CONTEXT: '$(Build.SourcesDirectory)'
79
+ ARTIFACT_DIRECTORY: '$(Build.ArtifactStagingDirectory)/miniapp'
80
+ MINIAPP_VERSION: '$(miniAppVersion)'
81
+ MINIAPP_CONTAINER_NAME: '$(appName)'
82
+
83
+ - task: PublishPipelineArtifact@1
84
+ displayName: 'Publish Android and iOS mini app archives'
85
+ condition: succeeded()
86
+ inputs:
87
+ targetPath: '$(Build.ArtifactStagingDirectory)/miniapp'
88
+ artifact: 'miniapp-$(envBuild)'
89
+ publishLocation: 'pipeline'
@@ -0,0 +1,89 @@
1
+ parameters:
2
+ - name: platform
3
+ type: string
4
+ values:
5
+ - android
6
+ - ios
7
+ - name: checksum
8
+ type: string
9
+
10
+ steps:
11
+ - task: AzureCLI@2
12
+ displayName: 'Upload ${{ parameters.platform }} mini app archive'
13
+ inputs:
14
+ azureSubscription: '$(MINIAPP_AZURE_SUBSCRIPTION)'
15
+ scriptType: 'bash'
16
+ scriptLocation: 'inlineScript'
17
+ inlineScript: |
18
+ set -euo pipefail
19
+
20
+ for variable in STORAGE_ACCOUNT STORAGE_CONTAINER APP_ID ENV_FOLDER MINIAPP_VERSION; do
21
+ case "${!variable:-}" in
22
+ ''|'$('*) echo "Required pipeline variable is missing: $variable" >&2; exit 1 ;;
23
+ esac
24
+ done
25
+
26
+ az storage blob upload \
27
+ --file "$BUILD_CONTEXT/build/generated/$PLATFORM/$MINIAPP_VERSION.zip" \
28
+ --container-name "$STORAGE_CONTAINER" \
29
+ --name "$APP_ID/$ENV_FOLDER/$PLATFORM/$MINIAPP_VERSION.zip" \
30
+ --account-name "$STORAGE_ACCOUNT" \
31
+ --auth-mode login \
32
+ --overwrite false \
33
+ --only-show-errors
34
+ env:
35
+ BUILD_CONTEXT: '$(Build.SourcesDirectory)'
36
+ STORAGE_ACCOUNT: '$(MINIAPP_STORAGE_ACCOUNT)'
37
+ STORAGE_CONTAINER: '$(MINIAPP_STORAGE_CONTAINER)'
38
+ APP_ID: '$(appId)'
39
+ ENV_FOLDER: '$(envFolder)'
40
+ PLATFORM: '${{ parameters.platform }}'
41
+ MINIAPP_VERSION: '$(miniAppVersion)'
42
+
43
+ - task: Bash@3
44
+ displayName: 'Register ${{ parameters.platform }} mini app version'
45
+ inputs:
46
+ targetType: 'inline'
47
+ script: |
48
+ set -euo pipefail
49
+
50
+ for variable in PGHOST PGUSER PGDATABASE PGPASSWORD APP_ID APP_NAME APP_CODE STORAGE_CONTAINER MINIAPP_VERSION CHECKSUM; do
51
+ case "${!variable:-}" in
52
+ ''|'$('*) echo "Required pipeline variable is missing: $variable" >&2; exit 1 ;;
53
+ esac
54
+ done
55
+
56
+ archivePath="$APP_ID/$ENV_FOLDER/$PLATFORM/$MINIAPP_VERSION.zip"
57
+ psql --no-psqlrc --set=ON_ERROR_STOP=1 \
58
+ --set=app_id="$APP_ID" \
59
+ --set=app_name="$APP_NAME" \
60
+ --set=app_code="$APP_CODE" \
61
+ --set=miniapp_version="$MINIAPP_VERSION" \
62
+ --set=platform="$PLATFORM" \
63
+ --set=checksum="$CHECKSUM" \
64
+ --set=storage_container="$STORAGE_CONTAINER" \
65
+ --set=archive_path="$archivePath" <<'SQL'
66
+ BEGIN;
67
+ INSERT INTO mini_app (id, bundle_name, name, created_at, updated_at, forced, disabled, app_code)
68
+ VALUES (:'app_id', :'app_id', :'app_name', now(), now(), false, false, :'app_code')
69
+ ON CONFLICT (id) DO NOTHING;
70
+
71
+ INSERT INTO mini_app_version (id, id_version, platform, checkSum, container, path, created_at, updated_at)
72
+ VALUES (:'app_id', :'miniapp_version', :'platform', :'checksum', :'storage_container', :'archive_path', now(), now());
73
+ COMMIT;
74
+ SQL
75
+ env:
76
+ PGHOST: '$(DB_HOST)'
77
+ PGPORT: '5432'
78
+ PGUSER: '$(DB_LOGIN)'
79
+ PGDATABASE: '$(DB_DATABASE)'
80
+ PGPASSWORD: '$(DB_PASSWORD)'
81
+ PGSSLMODE: 'require'
82
+ APP_ID: '$(appId)'
83
+ APP_NAME: '$(appName)'
84
+ APP_CODE: '$(appCode)'
85
+ ENV_FOLDER: '$(envFolder)'
86
+ PLATFORM: '${{ parameters.platform }}'
87
+ MINIAPP_VERSION: '$(miniAppVersion)'
88
+ CHECKSUM: '${{ parameters.checksum }}'
89
+ STORAGE_CONTAINER: '$(MINIAPP_STORAGE_CONTAINER)'
@@ -0,0 +1,3 @@
1
+ module.exports = process.env.REPACK === '1'
2
+ ? { commands: require('@callstack/repack/commands/rspack') }
3
+ : {};
@@ -0,0 +1,53 @@
1
+ const path = require('node:path');
2
+ const Repack = require('@callstack/repack');
3
+ const { ExpoModulesPlugin } = require('@callstack/repack-plugin-expo-modules');
4
+ const { ReanimatedPlugin } = require('@callstack/repack-plugin-reanimated');
5
+ const pkg = require('./package.json');
6
+
7
+ const aliases = Object.fromEntries(Object.entries(require('./aliases.cjs')).map(([name, target]) => [name, path.resolve(__dirname, target)]));
8
+ const sharedNames = [
9
+ 'react', 'react-native', 'expo', 'expo-image',
10
+ 'react-native-unistyles', 'react-native-nitro-modules',
11
+ '@thanhphi/miniapp-sdk', '@thanhphi/ui360',
12
+ '@react-navigation/native', '@react-navigation/native-stack',
13
+ 'react-native-gesture-handler', 'react-native-safe-area-context',
14
+ 'react-native-screens', 'react-native-svg', 'react-native-calendars',
15
+ 'react-native-reanimated', 'react-native-worklets',
16
+ ];
17
+ const shared = Object.fromEntries(sharedNames.map(name => [name, {
18
+ singleton: true,
19
+ strictVersion: name === '@thanhphi/ui360',
20
+ eager: false,
21
+ packageName: name,
22
+ requiredVersion: pkg.dependencies[name],
23
+ version: require(`${name}/package.json`).version,
24
+ }]));
25
+
26
+ module.exports = Repack.defineRspackConfig(({ mode, context, platform }) => ({
27
+ mode, context,
28
+ devtool: mode === 'production' ? false : 'source-map',
29
+ entry: './index.repack.js',
30
+ resolve: { ...Repack.getResolveOptions({ enablePackageExports: true }), alias: aliases },
31
+ output: { path: '[context]/build/generated/[platform]', uniqueName: '__APP_NAME__' },
32
+ module: { rules: [
33
+ { test: /\.[cm]?[jt]sx?$/, type: 'javascript/auto', use: {
34
+ loader: '@callstack/repack/babel-loader', options: {
35
+ configFile: path.resolve(__dirname, 'babel.config.js'),
36
+ caller: { name: '@callstack/repack', platform },
37
+ },
38
+ } },
39
+ { oneOf: [
40
+ { test: /\.svg$/, use: [{ loader: '@svgr/webpack', options: { native: true } }] },
41
+ ...Repack.getAssetTransformRules({ inline: true, maxInlineSize: 90 * 1024 }),
42
+ ] },
43
+ ] },
44
+ plugins: [
45
+ new Repack.RepackPlugin({ extraChunks: [{ include: /.*/, type: 'remote', outputPath: `build/outputs/${platform}/remotes` }] }),
46
+ new ExpoModulesPlugin(),
47
+ new ReanimatedPlugin({ unstable_disableTransform: true }),
48
+ new Repack.plugins.ModuleFederationPluginV2({
49
+ name: '__APP_NAME__', filename: '__APP_NAME__.container.js.bundle',
50
+ exposes: { './MiniApp': './src/App.tsx' }, dts: false, shared,
51
+ }),
52
+ ],
53
+ }));
@@ -0,0 +1,9 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+ module.exports = function clean(platform) {
4
+ if (!['ios', 'android'].includes(platform)) throw new Error('Platform must be ios or android.');
5
+ for (const directory of [`build/generated/${platform}`, `build/outputs/${platform}`]) {
6
+ fs.rmSync(path.resolve(__dirname, '..', directory), { recursive: true, force: true });
7
+ }
8
+ };
9
+ if (require.main === module) module.exports(process.argv[2]);
@@ -0,0 +1,29 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+
4
+ function syncEnv(flavor, root = path.resolve(__dirname, '..')) {
5
+ if (!['staging', 'beta', 'prod'].includes(flavor)) throw new Error('Use staging, beta or prod.');
6
+ const input = JSON.parse(fs.readFileSync(path.join(root, 'env', `${flavor}.json`), 'utf8'));
7
+ if (input.secure) throw new Error('Do not put secrets in client env files. Use public app config only.');
8
+ const config = input.public || input;
9
+ if (config.ENV !== flavor) throw new Error(`ENV must match ${flavor}.`);
10
+ const api = new URL(config.BASE_URL);
11
+ if (!['https:', 'http:'].includes(api.protocol) || api.username || api.password || api.search || api.hash) throw new Error('BASE_URL must be an HTTP(S) URL without credentials/query/hash.');
12
+ if (!Number.isInteger(config.API_TIMEOUT_MS) || config.API_TIMEOUT_MS <= 0) throw new Error('API_TIMEOUT_MS must be a positive integer.');
13
+ if (!config.AUTH || typeof config.AUTH.clientId !== 'string' || typeof config.AUTH.issuer !== 'string' || typeof config.AUTH.redirectUrl !== 'string' || !Array.isArray(config.AUTH.scopes) || config.AUTH.scopes.some(scope => typeof scope !== 'string')) throw new Error('AUTH requires issuer, clientId, redirectUrl and scopes.');
14
+ const issuer = new URL(config.AUTH.issuer);
15
+ const redirect = new URL(config.AUTH.redirectUrl);
16
+ if (issuer.protocol !== 'https:' || issuer.username || issuer.password) throw new Error('AUTH.issuer must use HTTPS without credentials.');
17
+ if (!/^[a-z][a-z0-9+.-]*:\/\//.test(config.AUTH.redirectUrl) || ['http:', 'https:'].includes(redirect.protocol) || redirect.username || redirect.password) throw new Error('Use a custom OAuth redirect scheme:// URL without credentials for this native base.');
18
+ const contents = `// Generated by scripts/sync-env.js (${flavor}). Values are public in the bundle.\nconst config = ${JSON.stringify(config, null, 2)} as const;\nexport default config;\nexport const { ENV, BASE_URL, API_TIMEOUT_MS, AUTH } = config;\n`;
19
+ fs.writeFileSync(path.join(root, 'env', 'env-config.ts'), contents);
20
+ // app.config.js uses the same selected env for native OAuth redirect setup.
21
+ fs.writeFileSync(path.join(root, 'env', 'selected.json'), JSON.stringify({ flavor }) + '\n');
22
+ return config;
23
+ }
24
+
25
+ if (require.main === module) {
26
+ try { syncEnv(process.argv[2]); console.log(`Selected ${process.argv[2]} environment. Restart Metro/Re.Pack after changing env.`); }
27
+ catch (error) { console.error(error.message); process.exitCode = 1; }
28
+ }
29
+ module.exports = { syncEnv };
@@ -0,0 +1,50 @@
1
+ import React from 'react';
2
+ import { ActivityIndicator, View } from 'react-native';
3
+ import { ApplicationProvider } from './common/mini-app/provider';
4
+ import type { MiniAppProps } from './common/mini-app/types';
5
+ import { MainStack } from './navigation/MainStack';
6
+ import {
7
+ createApplicationServices,
8
+ type ApplicationServices,
9
+ } from './services/application';
10
+
11
+ export type { MiniAppProps } from './common/mini-app/types';
12
+
13
+ /** Federation entry. Host authentication never falls back to standalone OIDC. */
14
+ export function MyApp(props: MiniAppProps) {
15
+ const { hostCapabilities } = props;
16
+ const generation = React.useRef(0);
17
+ const [instance, setInstance] = React.useState<{
18
+ services: ApplicationServices;
19
+ key: number;
20
+ } | null>(null);
21
+
22
+ React.useLayoutEffect(() => {
23
+ if (!hostCapabilities) return;
24
+ const services = createApplicationServices(hostCapabilities);
25
+ setInstance({ services, key: ++generation.current });
26
+ return () => {
27
+ services.dispose();
28
+ };
29
+ }, [hostCapabilities]);
30
+
31
+ // Fail at the integration boundary rather than treating an old host as standalone.
32
+ if (!hostCapabilities) {
33
+ throw new Error(
34
+ 'Mini app chưa được truyền hostCapabilities. Vui lòng kiểm tra cấu hình tích hợp.',
35
+ );
36
+ }
37
+
38
+ if (!instance)
39
+ return (
40
+ <View style={{ flex: 1, justifyContent: 'center' }}>
41
+ <ActivityIndicator />
42
+ </View>
43
+ );
44
+ return (
45
+ <ApplicationProvider key={instance.key} services={instance.services}>
46
+ <MainStack />
47
+ </ApplicationProvider>
48
+ );
49
+ }
50
+ export default MyApp;
@@ -0,0 +1,43 @@
1
+ import './themes/standalone';
2
+ import React from 'react';
3
+ import { ActivityIndicator, View } from 'react-native';
4
+ import { ApplicationProvider } from './common/mini-app/provider';
5
+ import { createStandaloneSession } from './modules/auth/services/standalone-session';
6
+ import { oidcOperations } from './modules/auth/services/oidc';
7
+ import { MainStack } from './navigation/MainStack';
8
+ import {
9
+ createApplicationServices,
10
+ type ApplicationServices,
11
+ } from './services/application';
12
+
13
+ export default function StandaloneApp() {
14
+ const [services, setServices] = React.useState<ApplicationServices | null>(
15
+ null,
16
+ );
17
+ const generation = React.useRef(0);
18
+ // Allocate in the effect so StrictMode's setup/cleanup/setup gets fresh resources.
19
+ React.useLayoutEffect(() => {
20
+ const session = createStandaloneSession(oidcOperations);
21
+ const instance = createApplicationServices(
22
+ session.capabilities,
23
+ session.actions,
24
+ );
25
+ generation.current += 1;
26
+ setServices(instance);
27
+ return () => {
28
+ instance.dispose();
29
+ session.dispose();
30
+ };
31
+ }, []);
32
+ if (!services)
33
+ return (
34
+ <View style={{ flex: 1, justifyContent: 'center' }}>
35
+ <ActivityIndicator />
36
+ </View>
37
+ );
38
+ return (
39
+ <ApplicationProvider key={generation.current} services={services}>
40
+ <MainStack />
41
+ </ApplicationProvider>
42
+ );
43
+ }
@@ -0,0 +1 @@
1
+
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,7 @@
1
+ import './themes/standalone';
2
+ import 'react-native-gesture-handler';
3
+ import { registerRootComponent } from 'expo';
4
+ import StandaloneApp from './StandaloneApp';
5
+
6
+ // Expo prebuild uses "main"; federation exposes src/App.tsx independently.
7
+ registerRootComponent(StandaloneApp);