Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion src/api/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,13 @@ apiClient.interceptors.request.use(
!config.url?.includes(API_ENDPOINTS.AUTH_LOGIN) &&
!config.url?.includes(API_ENDPOINTS.AUTH_REGISTER)
) {
config.headers.Authorization = `Bearer ${token}`;
if (token.split('.').length === 3) {
config.headers.Authorization = `Bearer ${token}`;
} else {
console.error('Invalid token format');
await AsyncStorage.removeItem('userToken');
await AsyncStorage.removeItem('refreshToken');
}
}
return config;
},
Expand All @@ -42,10 +48,22 @@ apiClient.interceptors.response.use(

const originalRequest = error.config as AxiosRequestConfig & {
_retry?: boolean;
_refreshAttempts?: number;
};

if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
originalRequest._refreshAttempts =
(originalRequest._refreshAttempts || 0) + 1;

// Prevent infinite refresh loops
if (originalRequest._refreshAttempts > 3) {
await AsyncStorage.removeItem('userToken');
await AsyncStorage.removeItem('refreshToken');
await AsyncStorage.removeItem('isDummyToken');
console.error('Too many token refresh attempts, logging out user');
return Promise.reject(error);
}

try {
const refreshToken = await AsyncStorage.getItem('refreshToken');
Expand Down
1 change: 1 addition & 0 deletions src/api/endpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const API_ENDPOINTS = {

USER_ME: '/users/me',
USER_UPDATE: '/users/me',
USER_CHARACTER: '/users/me/character',

TRANSACTION: '/transactions',
};
30 changes: 30 additions & 0 deletions src/api/userService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,24 @@ import useSWR from 'swr';
import {useAuth} from '../contexts/AuthContext';

interface UserProfileResponse {
id: string;
email: string;
name: string;
character: {
id: string;
image_url: string;
};
wallet: {
diamonds: number;
saving: number;
};
}

interface SetCharacterRequest {
character_id: string;
image_url: string;
}

export const userService = {
getUserProfile: async (): Promise<UserProfileResponse> => {
const response = await apiClient.get(API_ENDPOINTS.USER_ME);
Expand All @@ -20,6 +32,13 @@ export const userService = {
const response = await apiClient.put(API_ENDPOINTS.USER_UPDATE, userData);
return response.data;
},

setCharacter: async (
data: SetCharacterRequest,
): Promise<UserProfileResponse> => {
const response = await apiClient.put(API_ENDPOINTS.USER_CHARACTER, data);
return response.data;
},
};

export const useUserProfile = () => {
Expand Down Expand Up @@ -49,10 +68,21 @@ export const useUserProfileManager = () => {
}
};

const setCharacter = async (data: SetCharacterRequest) => {
try {
const updatedUser = await userService.setCharacter(data);
mutate(updatedUser, false);
return updatedUser;
} catch (error) {
throw error;
}
};

return {
user,
isLoading,
isError,
updateProfile,
setCharacter,
};
};
47 changes: 47 additions & 0 deletions src/components/setup/GoalSetting.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import React from 'react';
import {View, Text, TextInput, StyleSheet} from 'react-native';

interface GoalSettingProps {
goal: string;
onGoalChange: (goal: string) => void;
}

const GoalSetting: React.FC<GoalSettingProps> = ({goal, onGoalChange}) => {
return (
<View style={styles.stepContent}>
<Text style={styles.title}>設定理財目標</Text>
<TextInput
style={styles.input}
placeholder="輸入你的目標"
placeholderTextColor="#666"
value={goal}
onChangeText={onGoalChange}
/>
</View>
);
};

const styles = StyleSheet.create({
stepContent: {
width: '100%',
alignItems: 'center',
},
title: {
fontSize: 24,
marginBottom: 20,
color: '#fff',
textAlign: 'center',
},
input: {
width: '100%',
borderWidth: 1,
borderColor: '#ccc',
borderRadius: 5,
padding: 10,
marginBottom: 20,
backgroundColor: '#fff',
color: '#000',
},
});

export default GoalSetting;
135 changes: 135 additions & 0 deletions src/components/setup/PetSelection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import React from 'react';
import {View, Text, TouchableOpacity, Image, StyleSheet} from 'react-native';

interface Dino {
id: number;
name: string;
imageKey: string;
image: any;
}

interface PetSelectionProps {
selectedDino: Dino | null;
onDinoSelect: (dino: Dino) => void;
}

const dinosaurs: Dino[] = [
{
id: 1,
name: '寵物一',
imageKey: 'blue_1',
image: require('../../assets/characters/blue_1.png'),
},
{
id: 2,
name: '寵物二',
imageKey: 'blue_2',
image: require('../../assets/characters/blue_2.png'),
},
{
id: 3,
name: '寵物三',
imageKey: 'green_1',
image: require('../../assets/characters/green_1.png'),
},
{
id: 4,
name: '寵物四',
imageKey: 'green_2',
image: require('../../assets/characters/green_2.png'),
},
{
id: 5,
name: '寵物五',
imageKey: 'green_3',
image: require('../../assets/characters/green_3.png'),
},
{
id: 6,
name: '寵物六',
imageKey: 'main_character',
image: require('../../assets/characters/main_character.png'),
},
{
id: 7,
name: '寵物七',
imageKey: 'pink_1',
image: require('../../assets/characters/pink_1.png'),
},
{
id: 8,
name: '寵物八',
imageKey: 'yellow_1',
image: require('../../assets/characters/yellow_1.png'),
},
{
id: 9,
name: '寵物九',
imageKey: 'yellow_2',
image: require('../../assets/characters/yellow_2.png'),
},
];

const PetSelection: React.FC<PetSelectionProps> = ({
selectedDino,
onDinoSelect,
}) => {
return (
<View style={styles.stepContent}>
<Text style={styles.title}>選擇一個萌寵</Text>
<View style={styles.dinosContainer}>
{dinosaurs.map(dino => (
<TouchableOpacity
key={dino.id}
style={[
styles.dinoItem,
selectedDino?.id === dino.id && styles.selectedDinoItem,
]}
onPress={() => onDinoSelect(dino)}>
<Image source={dino.image} style={styles.dinoImage} />
<Text style={styles.dinoName}>{dino.name}</Text>
</TouchableOpacity>
))}
</View>
</View>
);
};

const styles = StyleSheet.create({
stepContent: {
width: '100%',
alignItems: 'center',
},
title: {
fontSize: 24,
marginBottom: 20,
color: '#fff',
textAlign: 'center',
},
dinosContainer: {
flexDirection: 'row',
justifyContent: 'space-around',
flexWrap: 'wrap',
width: '100%',
},
dinoItem: {
alignItems: 'center',
padding: 10,
borderRadius: 5,
},
selectedDinoItem: {
borderWidth: 2,
borderColor: '#007BFF',
},
dinoImage: {
width: 100,
height: 100,
marginBottom: 10,
},
dinoName: {
textAlign: 'center',
color: '#fff',
},
});

export default PetSelection;
49 changes: 14 additions & 35 deletions src/navigation/AppNavigator.tsx
Original file line number Diff line number Diff line change
@@ -1,53 +1,32 @@
import React, {useEffect, useState} from 'react';
import {createBottomTabNavigator} from '@react-navigation/bottom-tabs';
import React, {useState, useEffect} from 'react';
import {createStackNavigator} from '@react-navigation/stack';
import AsyncStorage from '@react-native-async-storage/async-storage';
import {useAuth} from '../contexts/AuthContext';

import HomeScreen from '../screens/HomeScreen';
import GachaScreen from '../screens/GachaScreen';
import InvestScreen from '../screens/InvestScreen';
import AnalysisScreen from '../screens/AnalysisScreen';
import BagScreen from '../screens/BagScreen';
import SettingsScreen from '../screens/SettingsScreen';
import TransactionScreen from '../screens/TransactionScreen';
import {useUserProfile} from '../api/userService';
import SetUp from '../screens/SetUp';
import TabNavigator from './TabNavigator';
import SettingsScreen from '../screens/SettingsScreen';

const Tab = createBottomTabNavigator();
const Stack = createStackNavigator();

const TabNavigator = () => {
return (
<Tab.Navigator
screenOptions={{
headerShown: false,
tabBarStyle: {display: 'none'},
}}>
<Tab.Screen name="Home" component={HomeScreen} />
<Tab.Screen name="Gacha" component={GachaScreen} />
<Tab.Screen name="Invest" component={InvestScreen} />
<Tab.Screen name="Analysis" component={AnalysisScreen} />
<Tab.Screen name="Bag" component={BagScreen} />
<Tab.Screen name="TransactionScreen" component={TransactionScreen} />
</Tab.Navigator>
);
};

const AppNavigator = () => {
const {user} = useAuth();
const {user: userProfile, isLoading: isUserLoading} = useUserProfile();
const [initialRoute, setInitialRoute] = useState<string | null>(null);

useEffect(() => {
const checkSetup = async () => {
if (!user?.uid) {return;}
const key = `setupDone-${user.uid}`;
const setupDone = await AsyncStorage.getItem(key);
setInitialRoute(setupDone === 'true' ? 'MainTabs' : 'SetUp');
if (!user?.uid || isUserLoading) {return;}

if (!userProfile?.character?.id) {
setInitialRoute('SetUp');
} else {
setInitialRoute('MainTabs');
}
};
checkSetup();
}, [user]);
}, [user, userProfile, isUserLoading]);

if (!initialRoute) {return null;}
if (!initialRoute || isUserLoading) {return null;}

return (
<Stack.Navigator
Expand Down
29 changes: 29 additions & 0 deletions src/navigation/TabNavigator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import React from 'react';
import {createBottomTabNavigator} from '@react-navigation/bottom-tabs';
import HomeScreen from '../screens/HomeScreen';
import GachaScreen from '../screens/GachaScreen';
import InvestScreen from '../screens/InvestScreen';
import AnalysisScreen from '../screens/AnalysisScreen';
import BagScreen from '../screens/BagScreen';
import TransactionScreen from '../screens/TransactionScreen';

const Tab = createBottomTabNavigator();

const TabNavigator = () => {
return (
<Tab.Navigator
screenOptions={{
headerShown: false,
tabBarStyle: {display: 'none'},
}}>
<Tab.Screen name="Home" component={HomeScreen} />
<Tab.Screen name="Gacha" component={GachaScreen} />
<Tab.Screen name="Invest" component={InvestScreen} />
<Tab.Screen name="Analysis" component={AnalysisScreen} />
<Tab.Screen name="Bag" component={BagScreen} />
<Tab.Screen name="TransactionScreen" component={TransactionScreen} />
</Tab.Navigator>
);
};

export default TabNavigator;
Loading