@thanhphi/create-360-miniapp 0.0.0-stage → 0.2.1

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 (82) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +328 -2
  3. package/bin/create-360-miniapp.cjs +54 -0
  4. package/lib/create.cjs +56 -0
  5. package/package.json +24 -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 +215 -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 +45 -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 +38 -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 +100 -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 +78 -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/themes/ui-geometry.ts +12 -0
  81. package/template/src/types/.gitkeep +0 -0
  82. package/template/tsconfig.json +65 -0
@@ -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);
@@ -0,0 +1,45 @@
1
+ import React from 'react';
2
+ import { Provider } from 'react-redux';
3
+ import { QueryClientProvider } from '@tanstack/react-query';
4
+ import { GestureHandlerRootView } from 'react-native-gesture-handler';
5
+ import { SafeAreaProvider } from 'react-native-safe-area-context';
6
+ import { createUITheme, UIThemeProvider } from '@thanhphi/ui360';
7
+ import { useUnistyles } from 'react-native-unistyles';
8
+ import { uiGeometry } from '../../themes/ui-geometry';
9
+ import type {} from '../../themes';
10
+ import type { ApplicationServices } from '../../services/application';
11
+
12
+ const RuntimeContext = React.createContext<ApplicationServices | null>(null);
13
+
14
+ export function useApplicationServices() {
15
+ const value = React.use(RuntimeContext);
16
+ if (!value) throw new Error('ApplicationProvider is required.');
17
+ return value;
18
+ }
19
+
20
+ export function ApplicationProvider({
21
+ services,
22
+ children,
23
+ }: React.PropsWithChildren<{ services: ApplicationServices }>) {
24
+ const {
25
+ theme: { type, color },
26
+ } = useUnistyles();
27
+ // Host supplies colors/mode; local UI360 supplies geometry without global setup.
28
+ const uiTheme = React.useMemo(
29
+ () => createUITheme({ mode: type, colors: color, ...uiGeometry }),
30
+ [type, color],
31
+ );
32
+ return (
33
+ <GestureHandlerRootView style={{ flex: 1 }}>
34
+ <SafeAreaProvider>
35
+ <RuntimeContext value={services}>
36
+ <Provider store={services.store}>
37
+ <QueryClientProvider client={services.queryClient}>
38
+ <UIThemeProvider theme={uiTheme}>{children}</UIThemeProvider>
39
+ </QueryClientProvider>
40
+ </Provider>
41
+ </RuntimeContext>
42
+ </SafeAreaProvider>
43
+ </GestureHandlerRootView>
44
+ );
45
+ }
@@ -0,0 +1,13 @@
1
+ import type { HostCapabilities } from '@thanhphi/miniapp-sdk';
2
+
3
+ export interface MiniAppProps {
4
+ /** Required hosted contract. Identity/tokens/refresh come from auth capabilities. */
5
+ hostCapabilities: HostCapabilities;
6
+ param_supper_app?: { notifications?: Record<string, unknown> };
7
+ }
8
+
9
+ export interface AuthActions {
10
+ isConfigured: boolean;
11
+ signIn(): Promise<void>;
12
+ signOut(): void;
13
+ }
File without changes
File without changes
File without changes
File without changes
@@ -0,0 +1,53 @@
1
+ import {
2
+ AuthError,
3
+ createHttpClient,
4
+ type AuthCapabilities,
5
+ type HttpRequestOptions,
6
+ } from '@thanhphi/miniapp-sdk';
7
+ import config from '@env-config';
8
+
9
+ export function createApiClient(
10
+ auth: AuthCapabilities,
11
+ reportAuthError: (error: AuthError, sessionId: string | null) => void,
12
+ ) {
13
+ // Empty configuration must remain reviewable; no request is made until configured.
14
+ const http = config.BASE_URL.trim()
15
+ ? createHttpClient({
16
+ baseURL: config.BASE_URL,
17
+ timeoutMs: config.API_TIMEOUT_MS,
18
+ auth,
19
+ })
20
+ : null;
21
+ async function request<T>(
22
+ path: string,
23
+ options: HttpRequestOptions = {},
24
+ ): Promise<T> {
25
+ const before = auth.getSnapshot();
26
+ if (!before.sessionId || !before.accessToken)
27
+ throw new AuthError('expired');
28
+ if (!http)
29
+ throw new Error('Hãy cấu hình BASE_URL trong env trước khi gọi API.');
30
+ try {
31
+ const response = await http.request<T>(path, { ...options, auth: true });
32
+ return response.data;
33
+ } catch (error) {
34
+ if (error instanceof AuthError) reportAuthError(error, before.sessionId);
35
+ throw error;
36
+ }
37
+ }
38
+ return {
39
+ request,
40
+ get: <T>(path: string, options?: HttpRequestOptions) =>
41
+ request<T>(path, { ...options, method: 'GET' }),
42
+ post: <T>(path: string, options?: HttpRequestOptions) =>
43
+ request<T>(path, { ...options, method: 'POST' }),
44
+ put: <T>(path: string, options?: HttpRequestOptions) =>
45
+ request<T>(path, { ...options, method: 'PUT' }),
46
+ patch: <T>(path: string, options?: HttpRequestOptions) =>
47
+ request<T>(path, { ...options, method: 'PATCH' }),
48
+ delete: <T>(path: string, options?: HttpRequestOptions) =>
49
+ request<T>(path, { ...options, method: 'DELETE' }),
50
+ dispose: () => http?.dispose(),
51
+ };
52
+ }
53
+ export type ApiClient = ReturnType<typeof createApiClient>;
@@ -0,0 +1,25 @@
1
+ import { AuthError, HttpError } from '@thanhphi/miniapp-sdk';
2
+
3
+ export function errorMessage(error: unknown): string {
4
+ if (error instanceof AuthError) {
5
+ if (error.kind === 'session-changed')
6
+ return 'Tài khoản đã thay đổi. Hãy tải lại dữ liệu.';
7
+ if (error.kind === 'expired')
8
+ return 'Phiên đăng nhập đã hết hạn. Hãy đăng nhập lại.';
9
+ return 'Không thể làm mới phiên. Hãy thử lại hoặc mở lại mini app.';
10
+ }
11
+ if (error instanceof HttpError) {
12
+ if (error.kind === 'timeout')
13
+ return 'Yêu cầu quá thời gian chờ. Hãy thử lại.';
14
+ if (error.kind === 'cancelled' || error.kind === 'disposed')
15
+ return 'Yêu cầu đã được huỷ.';
16
+ if (error.kind === 'http')
17
+ return `Máy chủ trả về lỗi ${error.status ?? ''}. Hãy thử lại.`;
18
+ if (error.kind === 'invalid-response')
19
+ return 'Dữ liệu trả về không đúng định dạng JSON.';
20
+ return 'Không thể kết nối máy chủ. Kiểm tra kết nối mạng rồi thử lại.';
21
+ }
22
+ return error instanceof Error
23
+ ? error.message
24
+ : 'Không thể hoàn tất yêu cầu. Hãy thử lại.';
25
+ }
@@ -0,0 +1,7 @@
1
+ import { Dimensions } from 'react-native';
2
+
3
+ const DESIGN_WIDTH = 375;
4
+ export const DEVICE_WIDTH = Dimensions.get('window').width;
5
+
6
+ /** Use for exceptional app geometry only; theme tokens are already scaled. */
7
+ export const ratioW = (value: number) => (value * DEVICE_WIDTH) / DESIGN_WIDTH;
File without changes
@@ -0,0 +1,38 @@
1
+ import React from 'react';
2
+ import { UIText } from '@thanhphi/ui360';
3
+ import { View } from 'react-native';
4
+ import { StyleSheet } from 'react-native-unistyles';
5
+ import type {} from '../../../themes';
6
+ import { uiGeometry } from '../../../themes/ui-geometry';
7
+ import type { Profile } from '../services/profile.service';
8
+
9
+ export function ProfileCard({ profile }: { profile: Profile }) {
10
+ return (
11
+ <View style={styles.card}>
12
+ <UIText selectable variant="title" weight="600">
13
+ {profile.name || 'Tài khoản'}
14
+ </UIText>
15
+ {profile.email && (
16
+ <UIText selectable color="content2">
17
+ {profile.email}
18
+ </UIText>
19
+ )}
20
+ {profile.id && (
21
+ <UIText selectable color="content2">
22
+ Mã: {profile.id}
23
+ </UIText>
24
+ )}
25
+ </View>
26
+ );
27
+ }
28
+
29
+ const { spacing, radius } = uiGeometry;
30
+ const styles = StyleSheet.create(({ color }) => ({
31
+ card: {
32
+ padding: spacing.xxl,
33
+ gap: spacing.sm,
34
+ borderRadius: radius.lg,
35
+ borderCurve: 'continuous',
36
+ backgroundColor: color.background,
37
+ },
38
+ }));
@@ -0,0 +1,18 @@
1
+ import React from 'react';
2
+ import { createNativeStackNavigator } from '@react-navigation/native-stack';
3
+ import HomeScreen from '../screens/home';
4
+
5
+ export type ModuleStackParams = { Home: undefined };
6
+ const Stack = createNativeStackNavigator<ModuleStackParams>();
7
+
8
+ export default function ModuleNavigator() {
9
+ return (
10
+ <Stack.Navigator>
11
+ <Stack.Screen
12
+ name="Home"
13
+ component={HomeScreen}
14
+ options={{ title: '__DISPLAY_NAME__' }}
15
+ />
16
+ </Stack.Navigator>
17
+ );
18
+ }
@@ -0,0 +1,13 @@
1
+ import { createSlice } from '@reduxjs/toolkit';
2
+
3
+ const homeSlice = createSlice({
4
+ name: '__MODULE__',
5
+ initialState: { profileRequested: false },
6
+ reducers: {
7
+ profileRequested(state) {
8
+ state.profileRequested = true;
9
+ },
10
+ },
11
+ });
12
+ export const { profileRequested } = homeSlice.actions;
13
+ export default homeSlice.reducer;
@@ -0,0 +1,30 @@
1
+ import { useQuery } from '@tanstack/react-query';
2
+ import { useApplicationServices } from '../../../../../common/mini-app/provider';
3
+ import { useAppDispatch, useAppSelector } from '../../../../../redux/hooks';
4
+ import {
5
+ selectIsAuthenticated,
6
+ selectSessionId,
7
+ } from '../../../../auth/redux/selectors';
8
+ import { profileRequested } from '../../../redux/home-slice';
9
+ import { getProfile } from '../../../services/profile.service';
10
+
11
+ export function useHome() {
12
+ const { api } = useApplicationServices();
13
+ const dispatch = useAppDispatch();
14
+ const sessionId = useAppSelector(selectSessionId);
15
+ const authenticated = useAppSelector(selectIsAuthenticated);
16
+ const requested = useAppSelector(state => state.home.profileRequested);
17
+ const query = useQuery({
18
+ queryKey: ['__MODULE__', 'profile', sessionId],
19
+ queryFn: ({ signal }) => getProfile(api, signal),
20
+ enabled: authenticated && requested,
21
+ });
22
+ return {
23
+ ...query,
24
+ requested,
25
+ loadProfile() {
26
+ if (requested) void query.refetch();
27
+ else dispatch(profileRequested());
28
+ },
29
+ };
30
+ }
@@ -0,0 +1,100 @@
1
+ import React from 'react';
2
+ import { Button, UIText } from '@thanhphi/ui360';
3
+ import { ActivityIndicator, ScrollView, View } from 'react-native';
4
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
5
+ import { StyleSheet, useUnistyles } from 'react-native-unistyles';
6
+ import { errorMessage } from '../../../../library/networking/errors';
7
+ import type {} from '../../../../themes';
8
+ import { uiGeometry } from '../../../../themes/ui-geometry';
9
+ import { useAuth } from '../../../auth/hooks/use-auth';
10
+ import { ProfileCard } from '../../components/ProfileCard';
11
+ import { useHome } from './hooks/use-home';
12
+
13
+ export default function HomeScreen() {
14
+ const {
15
+ theme: { color },
16
+ } = useUnistyles();
17
+ const { spacing } = uiGeometry;
18
+ const query = useHome();
19
+ const auth = useAuth();
20
+ const insets = useSafeAreaInsets();
21
+ return (
22
+ <ScrollView
23
+ contentInsetAdjustmentBehavior="automatic"
24
+ contentContainerStyle={[
25
+ styles.content,
26
+ { paddingBottom: insets.bottom + spacing.xxl },
27
+ ]}
28
+ >
29
+ {!query.requested && (
30
+ <View style={styles.intro}>
31
+ <UIText variant="title" weight="600" accessibilityRole="header">
32
+ Phiên đăng nhập đã sẵn sàng
33
+ </UIText>
34
+ <UIText color="content2">
35
+ Tải thông tin tài khoản khi bạn muốn kiểm tra kết nối.
36
+ </UIText>
37
+ </View>
38
+ )}
39
+ {query.isLoading && (
40
+ <ActivityIndicator
41
+ size="large"
42
+ color={color.primary500}
43
+ accessibilityLabel="Đang tải hồ sơ"
44
+ />
45
+ )}
46
+ {query.fetchStatus === 'paused' && (
47
+ <UIText selectable color="content2">
48
+ Kết nối đang tạm dừng. Thử lại khi có mạng.
49
+ </UIText>
50
+ )}
51
+ {query.error && (
52
+ <UIText selectable accessibilityRole="alert" color="danger">
53
+ {errorMessage(query.error)}
54
+ </UIText>
55
+ )}
56
+ {auth.message && (
57
+ <UIText selectable accessibilityRole="alert" color="danger">
58
+ {auth.message}
59
+ </UIText>
60
+ )}
61
+ {query.data === null && (
62
+ <UIText color="content2">
63
+ Chưa có thông tin tài khoản. Bạn có thể thử tải lại.
64
+ </UIText>
65
+ )}
66
+ {query.data && <ProfileCard profile={query.data} />}
67
+ <Button
68
+ label={
69
+ query.isFetching
70
+ ? 'Đang tải…'
71
+ : query.error
72
+ ? 'Thử lại'
73
+ : query.requested
74
+ ? 'Tải lại hồ sơ'
75
+ : 'Tải hồ sơ'
76
+ }
77
+ loading={query.isFetching}
78
+ onPress={query.loadProfile}
79
+ />
80
+ <Button
81
+ label={auth.isStandalone ? 'Đăng xuất khỏi ứng dụng' : 'Quay lại MySGR'}
82
+ onPress={() => {
83
+ if (auth.isStandalone) auth.signOut();
84
+ else void auth.closeMiniApp();
85
+ }}
86
+ />
87
+ </ScrollView>
88
+ );
89
+ }
90
+
91
+ const { spacing } = uiGeometry;
92
+ const styles = StyleSheet.create(({ color }) => ({
93
+ content: {
94
+ padding: spacing.xxl,
95
+ gap: spacing.xxl,
96
+ flexGrow: 1,
97
+ backgroundColor: color.base3,
98
+ },
99
+ intro: { gap: spacing.lg },
100
+ }));
@@ -0,0 +1,32 @@
1
+ import type { ApiClient } from '../../../library/networking/client';
2
+
3
+ export interface Profile {
4
+ id?: string;
5
+ name?: string;
6
+ email?: string;
7
+ }
8
+
9
+ /** Adapt this relative path and response shape to your backend before using it. */
10
+ export const PROFILE_PATH = '/me';
11
+
12
+ export async function getProfile(
13
+ api: ApiClient,
14
+ signal: AbortSignal,
15
+ ): Promise<Profile | null> {
16
+ const data = await api.get<unknown>(PROFILE_PATH, { signal });
17
+ if (data === null) return null;
18
+ if (typeof data !== 'object' || Array.isArray(data)) {
19
+ throw new Error(
20
+ 'API hồ sơ cần trả về object hoặc null. Hãy cập nhật profile.service.ts theo backend.',
21
+ );
22
+ }
23
+ const value = data as Record<string, unknown>;
24
+ const stringValue = (key: string) =>
25
+ typeof value[key] === 'string' ? (value[key] as string) : undefined;
26
+ const profile = {
27
+ id: stringValue('id'),
28
+ name: stringValue('name'),
29
+ email: stringValue('email'),
30
+ };
31
+ return profile.id || profile.name || profile.email ? profile : null;
32
+ }
File without changes
File without changes
File without changes
@@ -0,0 +1,55 @@
1
+ import React from 'react';
2
+ import { useApplicationServices } from '../../../common/mini-app/provider';
3
+ import { useAppSelector } from '../../../redux/hooks';
4
+ import {
5
+ selectAuthMessage,
6
+ selectIsAuthenticated,
7
+ selectSessionId,
8
+ } from '../redux/selectors';
9
+ import { errorMessage } from '../../../library/networking/errors';
10
+
11
+ export function useAuth() {
12
+ const { authActions, runtime } = useApplicationServices();
13
+ const authenticated = useAppSelector(selectIsAuthenticated);
14
+ const sessionId = useAppSelector(selectSessionId);
15
+ const sessionMessage = useAppSelector(selectAuthMessage);
16
+ const [pending, setPending] = React.useState(false);
17
+ const [message, setMessage] = React.useState<string | null>(null);
18
+ const mounted = React.useRef(true);
19
+ React.useEffect(() => {
20
+ mounted.current = true;
21
+ return () => {
22
+ mounted.current = false;
23
+ };
24
+ }, []);
25
+
26
+ const signIn = async () => {
27
+ if (!authActions || pending) return;
28
+ setPending(true);
29
+ setMessage(null);
30
+ try {
31
+ await authActions.signIn();
32
+ } catch (error) {
33
+ if (mounted.current) setMessage(errorMessage(error));
34
+ } finally {
35
+ if (mounted.current) setPending(false);
36
+ }
37
+ };
38
+ return {
39
+ authenticated,
40
+ sessionId,
41
+ message: message ?? sessionMessage,
42
+ pending,
43
+ isStandalone: Boolean(authActions),
44
+ canSignIn: Boolean(authActions?.isConfigured),
45
+ signIn,
46
+ signOut: () => authActions?.signOut(),
47
+ async closeMiniApp() {
48
+ try {
49
+ await runtime.capabilities.closeMiniApp();
50
+ } catch (error) {
51
+ if (mounted.current) setMessage(errorMessage(error));
52
+ }
53
+ },
54
+ };
55
+ }
@@ -0,0 +1,16 @@
1
+ import React from 'react';
2
+ import { createNativeStackNavigator } from '@react-navigation/native-stack';
3
+ import LoginScreen from '../screens/LoginScreen';
4
+
5
+ const Stack = createNativeStackNavigator<{ Login: undefined }>();
6
+ export default function AuthenticationNavigator() {
7
+ return (
8
+ <Stack.Navigator>
9
+ <Stack.Screen
10
+ name="Login"
11
+ component={LoginScreen}
12
+ options={{ title: '__DISPLAY_NAME__' }}
13
+ />
14
+ </Stack.Navigator>
15
+ );
16
+ }
@@ -0,0 +1,36 @@
1
+ import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
2
+
3
+ export interface AuthState {
4
+ sessionId: string | null;
5
+ authenticated: boolean;
6
+ message: string | null;
7
+ }
8
+ const initialState: AuthState = {
9
+ sessionId: null,
10
+ authenticated: false,
11
+ message: null,
12
+ };
13
+
14
+ const authSlice = createSlice({
15
+ name: 'auth',
16
+ initialState,
17
+ reducers: {
18
+ sessionReceived(
19
+ state,
20
+ {
21
+ payload,
22
+ }: PayloadAction<Pick<AuthState, 'sessionId' | 'authenticated'>>,
23
+ ) {
24
+ state.sessionId = payload.sessionId;
25
+ state.authenticated = payload.authenticated;
26
+ state.message = null;
27
+ },
28
+ authenticationFailed(state, { payload }: PayloadAction<string>) {
29
+ state.authenticated = false;
30
+ state.message = payload;
31
+ },
32
+ },
33
+ });
34
+
35
+ export const { sessionReceived, authenticationFailed } = authSlice.actions;
36
+ export default authSlice.reducer;
@@ -0,0 +1,6 @@
1
+ import type { RootState } from '../../../redux/store';
2
+
3
+ export const selectIsAuthenticated = (state: RootState) =>
4
+ state.auth.authenticated;
5
+ export const selectSessionId = (state: RootState) => state.auth.sessionId;
6
+ export const selectAuthMessage = (state: RootState) => state.auth.message;