feat: implement core screens and authentication infrastructure including cart, login, and profile flows
This commit is contained in:
parent
44f6050e60
commit
7a0fd59892
5
ReactotronConfig.js
Normal file
5
ReactotronConfig.js
Normal file
@ -0,0 +1,5 @@
|
||||
import Reactotron from 'reactotron-react-native';
|
||||
|
||||
Reactotron.configure() // controls connection & communication settings
|
||||
.useReactNative() // add all built-in react native plugins
|
||||
.connect(); // let's connect!
|
||||
@ -6,6 +6,20 @@ buildscript {
|
||||
targetSdkVersion = 36
|
||||
ndkVersion = "27.1.12297006"
|
||||
kotlinVersion = "2.1.20"
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// Pin Google Play Services versions so all libraries use the same
|
||||
// artifact. react-native-geolocation-service defaults to 18.0.0
|
||||
// but react-native-maps requires 21.x where FusedLocationProvider-
|
||||
// Client is an interface, not a class. Using two different versions
|
||||
// causes IncompatibleClassChangeError and a hard app crash.
|
||||
// ---------------------------------------------------------------
|
||||
googlePlayServicesVersion = "21.3.0" // used by geolocation-service
|
||||
playServicesVersion = "21.3.0" // fallback alias
|
||||
playServicesLocationVersion = "21.3.0" // used by geolocation-service
|
||||
googlePlayServicesLocationVersion = "21.3.0" // used by react-native-maps
|
||||
googlePlayServicesMapsVersion = "19.1.0"
|
||||
googlePlayServicesBaseVersion = "18.5.0"
|
||||
}
|
||||
repositories {
|
||||
google()
|
||||
@ -19,3 +33,13 @@ buildscript {
|
||||
}
|
||||
|
||||
apply plugin: "com.facebook.react.rootproject"
|
||||
|
||||
// Force a single version of play-services-location across ALL modules
|
||||
// to prevent IncompatibleClassChangeError at runtime.
|
||||
subprojects {
|
||||
configurations.all {
|
||||
resolutionStrategy {
|
||||
force "com.google.android.gms:play-services-location:21.3.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,20 +1,8 @@
|
||||
import { apiClient } from '../services/apiClient';
|
||||
import { User } from '../interfaces';
|
||||
import { LoginResponse, User, VerifyOtpResponse } from '../interfaces';
|
||||
|
||||
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface LoginResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface VerifyOtpResponse {
|
||||
success: boolean;
|
||||
user: User;
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
}
|
||||
|
||||
export interface UpdateProfileResponse {
|
||||
success: boolean;
|
||||
user: User;
|
||||
@ -26,11 +14,11 @@ export interface LogoutResponse {
|
||||
|
||||
// ─── Endpoints ───────────────────────────────────────────────────────────────
|
||||
|
||||
export const loginApi = (mobileNumber: string) =>
|
||||
apiClient.post<LoginResponse>('/auth/login', { mobileNumber });
|
||||
export const loginApi = (phone: string) =>
|
||||
apiClient.post<LoginResponse>('/auth/otp/request', { phone });
|
||||
|
||||
export const verifyOtpApi = (mobileNumber: string, otp: string) =>
|
||||
apiClient.post<VerifyOtpResponse>('/auth/verify-otp', { mobileNumber, otp });
|
||||
export const verifyOtpApi = (phone: string, code: string, role: string) =>
|
||||
apiClient.post<VerifyOtpResponse>('/auth/otp/verify', { phone, code, role });
|
||||
|
||||
export const updateProfileApi = (data: {
|
||||
name: string;
|
||||
|
||||
@ -9,7 +9,8 @@ import {
|
||||
import { getStyles } from './loginScreen.styles';
|
||||
import { CustomInput, PrimaryButton } from '@components';
|
||||
import { useAppTheme } from '@theme';
|
||||
import { useLoginScreen } from '../../../hooks';
|
||||
import { useLoginScreen } from './hooks/useLoginScreen';
|
||||
import { RootState, useAppSelector } from '@store';
|
||||
|
||||
export const LoginScreen: React.FC = () => {
|
||||
const { colors } = useAppTheme();
|
||||
@ -18,6 +19,8 @@ export const LoginScreen: React.FC = () => {
|
||||
const { mobileNumber, error, onChangeMobileNumber, handleLogin } =
|
||||
useLoginScreen();
|
||||
|
||||
const { isLoading } = useAppSelector((state: RootState) => state.auth);
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView
|
||||
style={styles.container}
|
||||
@ -69,9 +72,10 @@ export const LoginScreen: React.FC = () => {
|
||||
)}
|
||||
|
||||
<PrimaryButton
|
||||
title="Get OTP"
|
||||
title={isLoading ? 'Loading...' : 'Get OTP'}
|
||||
onPress={handleLogin}
|
||||
style={{ marginTop: 12 }}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</View>
|
||||
|
||||
|
||||
@ -2,7 +2,7 @@ import { useState, useCallback } from 'react';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { StackNavigationProp } from '@react-navigation/stack';
|
||||
import { AuthStackParamList } from 'app/navigation/authStack';
|
||||
import { loginWithPhone, useAppDispatch } from '../store';
|
||||
import { loginWithPhone, useAppDispatch } from '@store';
|
||||
|
||||
type LoginNavProp = StackNavigationProp<AuthStackParamList, 'LoginScreen'>;
|
||||
|
||||
@ -9,10 +9,10 @@ import { StackNavigationProp } from '@react-navigation/stack';
|
||||
import { getStyles } from './accountScreen.styles';
|
||||
import { Header, PrimaryButton } from '@components';
|
||||
import { useAppTheme } from '@theme';
|
||||
import { useAppDispatch, useAppSelector } from '../../../hooks/useAppDispatch';
|
||||
import { logout } from '../../../store/commonreducers/auth';
|
||||
import { AppStackParamList } from '../../../navigation/appStack';
|
||||
import { MainTabParamList } from '../../../navigation/mainTabNavigator';
|
||||
import { useAppDispatch, useAppSelector } from '@store';
|
||||
|
||||
type AccountNavProp = CompositeNavigationProp<
|
||||
BottomTabNavigationProp<MainTabParamList, 'AccountScreen'>,
|
||||
|
||||
@ -11,7 +11,6 @@ import { StackNavigationProp } from '@react-navigation/stack';
|
||||
import { getStyles } from './cartScreen.styles';
|
||||
import { Header, QuantitySelector, PrimaryButton } from '@components';
|
||||
import { useAppTheme } from '@theme';
|
||||
import { useAppDispatch, useAppSelector } from '../../../hooks/useAppDispatch';
|
||||
import {
|
||||
removeItem,
|
||||
applyCoupon,
|
||||
@ -19,6 +18,7 @@ import {
|
||||
selectCartTotal,
|
||||
} from '../../../store/commonreducers/cart';
|
||||
import { AppStackParamList } from '../../../navigation/appStack';
|
||||
import { useAppDispatch, useAppSelector } from '@store';
|
||||
|
||||
type CartScreenNavProp = StackNavigationProp<AppStackParamList, 'CartScreen'>;
|
||||
|
||||
|
||||
@ -12,9 +12,10 @@ import { StackNavigationProp } from '@react-navigation/stack';
|
||||
import { getStyles } from './completeProfileScreen.styles';
|
||||
import { CustomInput, PrimaryButton } from '@components';
|
||||
import { useAppTheme } from '@theme';
|
||||
import { useAppDispatch } from '../../../hooks/useAppDispatch';
|
||||
|
||||
import { completeProfile } from '../../../store/commonreducers/auth';
|
||||
import { AuthStackParamList } from '../../../navigation/authStack';
|
||||
import { useAppDispatch } from '@store';
|
||||
|
||||
type NavProp = StackNavigationProp<AuthStackParamList, 'CompleteProfileScreen'>;
|
||||
|
||||
@ -61,7 +62,7 @@ export const CompleteProfileScreen: React.FC = () => {
|
||||
|
||||
<Text style={styles.labelText}>Location Label</Text>
|
||||
<View style={styles.labelRow}>
|
||||
{LOCATION_LABELS.map((label) => (
|
||||
{LOCATION_LABELS.map(label => (
|
||||
<TouchableOpacity
|
||||
key={label}
|
||||
style={[
|
||||
|
||||
@ -3,8 +3,9 @@ import { View, Text } from 'react-native';
|
||||
import { getStyles } from './onboardingCompleteScreen.styles';
|
||||
import { PrimaryButton } from '@components';
|
||||
import { useAppTheme } from '@theme';
|
||||
import { useAppDispatch } from '../../../hooks/useAppDispatch';
|
||||
|
||||
import { completeOnboarding } from '../../../store/commonreducers/auth';
|
||||
import { useAppDispatch } from '@store';
|
||||
|
||||
export const OnboardingCompleteScreen: React.FC = () => {
|
||||
const { colors } = useAppTheme();
|
||||
@ -25,10 +26,7 @@ export const OnboardingCompleteScreen: React.FC = () => {
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.footer}>
|
||||
<PrimaryButton
|
||||
title="Explore Now"
|
||||
onPress={handleExplore}
|
||||
/>
|
||||
<PrimaryButton title="Explore Now" onPress={handleExplore} />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
108
app/features/screens/otpScreen/hooks/useOtpScreen.ts
Normal file
108
app/features/screens/otpScreen/hooks/useOtpScreen.ts
Normal file
@ -0,0 +1,108 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Alert } from 'react-native';
|
||||
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
|
||||
import { StackNavigationProp } from '@react-navigation/stack';
|
||||
import { RootState, useAppDispatch, useAppSelector, verifyOtp } from '@store';
|
||||
import { AuthStackParamList } from '@navigation/authStack';
|
||||
|
||||
type OtpNavProp = StackNavigationProp<AuthStackParamList, 'OtpScreen'>;
|
||||
type OtpRouteProp = RouteProp<AuthStackParamList, 'OtpScreen'>;
|
||||
|
||||
const OTP_LENGTH = 6;
|
||||
const RESEND_TIMER = 30;
|
||||
|
||||
export const useOtpScreen = () => {
|
||||
const navigation = useNavigation<OtpNavProp>();
|
||||
const route = useRoute<OtpRouteProp>();
|
||||
const dispatch = useAppDispatch();
|
||||
const { mobileNumber } = route.params;
|
||||
|
||||
const { loginData, user } = useAppSelector((state: RootState) => state.auth);
|
||||
|
||||
const [otp, setOtp] = useState<string[]>(Array(OTP_LENGTH).fill(''));
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
const [timer, setTimer] = useState(RESEND_TIMER);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (timer > 0) {
|
||||
const interval = setInterval(() => setTimer(t => t - 1), 1000);
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
}, [timer]);
|
||||
|
||||
const resetOtp = useCallback(() => {
|
||||
setOtp(Array(OTP_LENGTH).fill(''));
|
||||
setActiveIndex(0);
|
||||
}, []);
|
||||
|
||||
const handleKeyPress = useCallback(
|
||||
(key: string) => {
|
||||
if (key === 'backspace') {
|
||||
if (otp[activeIndex] || activeIndex > 0) {
|
||||
const newOtp = [...otp];
|
||||
if (otp[activeIndex]) {
|
||||
newOtp[activeIndex] = '';
|
||||
} else if (activeIndex > 0) {
|
||||
newOtp[activeIndex - 1] = '';
|
||||
setActiveIndex(activeIndex - 1);
|
||||
}
|
||||
setOtp(newOtp);
|
||||
}
|
||||
} else if (key >= '0' && key <= '9' && activeIndex < OTP_LENGTH) {
|
||||
const newOtp = [...otp];
|
||||
newOtp[activeIndex] = key;
|
||||
setOtp(newOtp);
|
||||
if (activeIndex < OTP_LENGTH - 1) {
|
||||
setActiveIndex(activeIndex + 1);
|
||||
}
|
||||
}
|
||||
},
|
||||
[otp, activeIndex],
|
||||
);
|
||||
|
||||
const handleResend = useCallback(() => {
|
||||
setTimer(RESEND_TIMER);
|
||||
resetOtp();
|
||||
// TODO: dispatch resend/loginWithPhone thunk here if needed
|
||||
}, [resetOtp]);
|
||||
|
||||
const handleVerify = useCallback(async () => {
|
||||
const otpString = otp.join('');
|
||||
if (otpString.length !== OTP_LENGTH) return;
|
||||
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await dispatch(
|
||||
verifyOtp({ phone: mobileNumber, code: otpString, role: 'CUSTOMER' }),
|
||||
).unwrap();
|
||||
|
||||
navigation.navigate('SetLocationScreen');
|
||||
} catch (error) {
|
||||
const message =
|
||||
typeof error === 'string' ? error : 'Invalid OTP. Please try again.';
|
||||
Alert.alert('Verification Failed', message);
|
||||
resetOtp();
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [dispatch, mobileNumber, navigation, otp, resetOtp]);
|
||||
|
||||
return {
|
||||
// data
|
||||
mobileNumber,
|
||||
loginData,
|
||||
user,
|
||||
otp,
|
||||
activeIndex,
|
||||
timer,
|
||||
isLoading,
|
||||
otpLength: OTP_LENGTH,
|
||||
isOtpComplete: otp.join('').length === OTP_LENGTH,
|
||||
|
||||
// actions
|
||||
handleKeyPress,
|
||||
handleVerify,
|
||||
handleResend,
|
||||
};
|
||||
};
|
||||
@ -1,4 +1,4 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
@ -6,71 +6,27 @@ import {
|
||||
KeyboardAvoidingView,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
import { useNavigation, useRoute, RouteProp } from '@react-navigation/native';
|
||||
import { StackNavigationProp } from '@react-navigation/stack';
|
||||
import { getStyles } from './otpScreen.styles';
|
||||
import { PrimaryButton } from '@components';
|
||||
import { useAppTheme } from '@theme';
|
||||
import { useAppDispatch } from '../../../hooks/useAppDispatch';
|
||||
import { verifyOtp } from '../../../store/commonreducers/auth';
|
||||
import { AuthStackParamList } from '../../../navigation/authStack';
|
||||
|
||||
type OtpNavProp = StackNavigationProp<AuthStackParamList, 'OtpScreen'>;
|
||||
type OtpRouteProp = RouteProp<AuthStackParamList, 'OtpScreen'>;
|
||||
|
||||
const OTP_LENGTH = 6;
|
||||
const RESEND_TIMER = 30;
|
||||
import { useOtpScreen } from './hooks/useOtpScreen';
|
||||
|
||||
export const OtpScreen: React.FC = () => {
|
||||
const { colors } = useAppTheme();
|
||||
const styles = getStyles(colors);
|
||||
const navigation = useNavigation<OtpNavProp>();
|
||||
const route = useRoute<OtpRouteProp>();
|
||||
const dispatch = useAppDispatch();
|
||||
const { mobileNumber } = route.params;
|
||||
|
||||
const [otp, setOtp] = useState<string[]>(Array(OTP_LENGTH).fill(''));
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
const [timer, setTimer] = useState(RESEND_TIMER);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (timer > 0) {
|
||||
const interval = setInterval(() => setTimer((t) => t - 1), 1000);
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
}, [timer]);
|
||||
|
||||
const handleKeyPress = (key: string) => {
|
||||
if (key === 'backspace') {
|
||||
if (otp[activeIndex] || activeIndex > 0) {
|
||||
const newOtp = [...otp];
|
||||
if (otp[activeIndex]) {
|
||||
newOtp[activeIndex] = '';
|
||||
} else if (activeIndex > 0) {
|
||||
newOtp[activeIndex - 1] = '';
|
||||
setActiveIndex(activeIndex - 1);
|
||||
}
|
||||
setOtp(newOtp);
|
||||
}
|
||||
} else if (key >= '0' && key <= '9' && activeIndex < OTP_LENGTH) {
|
||||
const newOtp = [...otp];
|
||||
newOtp[activeIndex] = key;
|
||||
setOtp(newOtp);
|
||||
if (activeIndex < OTP_LENGTH - 1) {
|
||||
setActiveIndex(activeIndex + 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleVerify = async () => {
|
||||
const otpString = otp.join('');
|
||||
if (otpString.length !== OTP_LENGTH) return;
|
||||
setIsLoading(true);
|
||||
await dispatch(verifyOtp({ mobileNumber, otp: otpString }));
|
||||
setIsLoading(false);
|
||||
navigation.navigate('SetLocationScreen');
|
||||
};
|
||||
const {
|
||||
mobileNumber,
|
||||
otp,
|
||||
activeIndex,
|
||||
timer,
|
||||
isLoading,
|
||||
otpLength,
|
||||
isOtpComplete,
|
||||
handleKeyPress,
|
||||
handleVerify,
|
||||
handleResend,
|
||||
} = useOtpScreen();
|
||||
|
||||
return (
|
||||
<KeyboardAvoidingView
|
||||
@ -79,27 +35,56 @@ export const OtpScreen: React.FC = () => {
|
||||
>
|
||||
<View style={styles.content}>
|
||||
<Text style={styles.title}>Verify OTP</Text>
|
||||
<Text style={styles.subtitle}>
|
||||
Code sent to {mobileNumber}
|
||||
</Text>
|
||||
<Text style={styles.subtitle}>Code sent to {mobileNumber}</Text>
|
||||
|
||||
<View style={styles.otpContainer}>
|
||||
{otp.map((digit, index) => (
|
||||
<View
|
||||
key={index}
|
||||
style={[
|
||||
styles.otpCell,
|
||||
index === activeIndex && styles.otpCellActive,
|
||||
digit ? styles.otpCellFilled : null,
|
||||
]}
|
||||
>
|
||||
<Text style={styles.otpCellText}>{digit}</Text>
|
||||
</View>
|
||||
))}
|
||||
{otp.map(
|
||||
(
|
||||
digit:
|
||||
| string
|
||||
| number
|
||||
| bigint
|
||||
| boolean
|
||||
| React.ReactElement<
|
||||
unknown,
|
||||
string | React.JSXElementConstructor<any>
|
||||
>
|
||||
| Iterable<React.ReactNode>
|
||||
| React.ReactPortal
|
||||
| Promise<
|
||||
| string
|
||||
| number
|
||||
| bigint
|
||||
| boolean
|
||||
| React.ReactPortal
|
||||
| React.ReactElement<
|
||||
unknown,
|
||||
string | React.JSXElementConstructor<any>
|
||||
>
|
||||
| Iterable<React.ReactNode>
|
||||
| null
|
||||
| undefined
|
||||
>
|
||||
| null
|
||||
| undefined,
|
||||
index: React.Key | null | undefined,
|
||||
) => (
|
||||
<View
|
||||
key={index}
|
||||
style={[
|
||||
styles.otpCell,
|
||||
index === activeIndex && styles.otpCellActive,
|
||||
digit ? styles.otpCellFilled : null,
|
||||
]}
|
||||
>
|
||||
<Text style={styles.otpCellText}>{digit}</Text>
|
||||
</View>
|
||||
),
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={styles.keypad}>
|
||||
{[1, 2, 3, 4, 5, 6, 7, 8, 9].map((num) => (
|
||||
{[1, 2, 3, 4, 5, 6, 7, 8, 9].map(num => (
|
||||
<TouchableOpacity
|
||||
key={num}
|
||||
style={styles.key}
|
||||
@ -129,14 +114,7 @@ export const OtpScreen: React.FC = () => {
|
||||
{timer > 0 ? (
|
||||
<Text style={styles.timerText}>Resend code in {timer}s</Text>
|
||||
) : (
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
setTimer(RESEND_TIMER);
|
||||
setOtp(Array(OTP_LENGTH).fill(''));
|
||||
setActiveIndex(0);
|
||||
}}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<TouchableOpacity onPress={handleResend} activeOpacity={0.7}>
|
||||
<Text style={styles.resendText}>Resend OTP</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
@ -145,7 +123,7 @@ export const OtpScreen: React.FC = () => {
|
||||
title="Verify"
|
||||
onPress={handleVerify}
|
||||
isLoading={isLoading}
|
||||
disabled={otp.join('').length !== OTP_LENGTH}
|
||||
disabled={!isOtpComplete}
|
||||
style={{ marginTop: 24 }}
|
||||
/>
|
||||
</View>
|
||||
|
||||
@ -12,11 +12,14 @@ import { StackNavigationProp } from '@react-navigation/stack';
|
||||
import { getStyles } from './providerDetailsScreen.styles';
|
||||
import { CatalogItemRow } from '@components';
|
||||
import { useAppTheme } from '@theme';
|
||||
import { useAppDispatch, useAppSelector } from '../../../hooks/useAppDispatch';
|
||||
import { addItem } from '../../../store/commonreducers/cart';
|
||||
import { getProviderCatalogApi, getProvidersApi } from '../../../api/deliveryApi';
|
||||
import {
|
||||
getProviderCatalogApi,
|
||||
getProvidersApi,
|
||||
} from '../../../api/deliveryApi';
|
||||
import { CatalogItem, Provider } from '../../../interfaces';
|
||||
import { AppStackParamList } from '../../../navigation/appStack';
|
||||
import { useAppDispatch, useAppSelector } from '@store';
|
||||
|
||||
type ProviderDetailsNavProp = StackNavigationProp<
|
||||
AppStackParamList,
|
||||
|
||||
@ -3,8 +3,8 @@ import {
|
||||
View,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
Platform,
|
||||
ActivityIndicator,
|
||||
InteractionManager,
|
||||
} from 'react-native';
|
||||
import MapView, { Marker, Region } from 'react-native-maps';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
@ -17,6 +17,7 @@ import {
|
||||
DEFAULT_LOCATION,
|
||||
getCurrentLocationWithAddress,
|
||||
reverseGeocode,
|
||||
showPermissionDeniedAlert,
|
||||
LatLng,
|
||||
} from '../../../services/locationServices';
|
||||
|
||||
@ -29,6 +30,8 @@ export const SetLocationScreen: React.FC = () => {
|
||||
const styles = getStyles(colors);
|
||||
const navigation = useNavigation<NavProp>();
|
||||
const mapRef = useRef<MapView>(null);
|
||||
// Guard: never call setState after the component has unmounted
|
||||
const isMounted = useRef(true);
|
||||
|
||||
const [coords, setCoords] = useState<LatLng>(DEFAULT_LOCATION);
|
||||
const [address, setAddress] = useState('Fetching your location…');
|
||||
@ -38,27 +41,43 @@ export const SetLocationScreen: React.FC = () => {
|
||||
// Fetch current location on mount
|
||||
// ------------------------------------------------------------------
|
||||
const fetchCurrentLocation = useCallback(async () => {
|
||||
if (!isMounted.current) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
console.log('--------------');
|
||||
const result = await getCurrentLocationWithAddress();
|
||||
console.log('result', result);
|
||||
if (!isMounted.current) return;
|
||||
setCoords(result.coords);
|
||||
setAddress(result.address);
|
||||
|
||||
// Animate the map to the user's location
|
||||
mapRef.current?.animateToRegion({ ...result.coords, ...DELTA }, 600);
|
||||
} catch {
|
||||
// Permission denied or GPS unavailable — keep the default location
|
||||
console.log('--------error------');
|
||||
} catch (err: unknown) {
|
||||
const message = err instanceof Error ? err.message : '';
|
||||
if (message === 'Location permission denied') {
|
||||
// Show the alert here — safe, outside the Promise chain
|
||||
showPermissionDeniedAlert();
|
||||
}
|
||||
console.log('--------error------', err);
|
||||
if (!isMounted.current) return;
|
||||
setAddress('Could not determine location');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
if (isMounted.current) setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchCurrentLocation();
|
||||
// Wait for the screen-transition animation to fully finish before
|
||||
// calling PermissionsAndroid.request(). Calling it while the Activity
|
||||
// is still transitioning causes a native crash on Android.
|
||||
const task = InteractionManager.runAfterInteractions(() => {
|
||||
fetchCurrentLocation();
|
||||
});
|
||||
return () => {
|
||||
task.cancel();
|
||||
isMounted.current = false;
|
||||
};
|
||||
}, [fetchCurrentLocation]);
|
||||
|
||||
// ------------------------------------------------------------------
|
||||
@ -83,8 +102,6 @@ export const SetLocationScreen: React.FC = () => {
|
||||
ref={mapRef}
|
||||
style={styles.map}
|
||||
initialRegion={{ ...DEFAULT_LOCATION, ...DELTA }}
|
||||
showsUserLocation
|
||||
showsMyLocationButton={true}
|
||||
onRegionChangeComplete={handleRegionChangeComplete}
|
||||
>
|
||||
<Marker
|
||||
|
||||
@ -1 +0,0 @@
|
||||
export * from './useLoginScreen';
|
||||
60
app/interfaces/auth.ts
Normal file
60
app/interfaces/auth.ts
Normal file
@ -0,0 +1,60 @@
|
||||
export interface LoginResponse {
|
||||
message: string;
|
||||
phone: string;
|
||||
code: string;
|
||||
}
|
||||
|
||||
export interface VerifyOtpResponse {
|
||||
message: string;
|
||||
isNewUser: boolean;
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
user: User;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
roleId: string;
|
||||
roleEnum: RoleEnum;
|
||||
name: string | null;
|
||||
email: string | null;
|
||||
phone: string;
|
||||
profileImage: string | null;
|
||||
status: UserStatus;
|
||||
mfaEnabled: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
deletedAt: string | null;
|
||||
isDeleted: boolean;
|
||||
lastLogoutAt: string | null;
|
||||
loginAt: string;
|
||||
refreshTokenId: string;
|
||||
refreshTokenHash: string;
|
||||
refreshTokenExpiresAt: string;
|
||||
categoryPreferences: CategoryPreference[];
|
||||
gender: Gender | null;
|
||||
dateOfBirth: string | null;
|
||||
role: Role;
|
||||
merchants: Merchant[];
|
||||
}
|
||||
|
||||
export interface Role {
|
||||
id: string;
|
||||
code: string;
|
||||
scope: string;
|
||||
isSystem: boolean;
|
||||
}
|
||||
|
||||
export interface CategoryPreference {
|
||||
// Extend when backend starts returning data
|
||||
}
|
||||
|
||||
export interface Merchant {
|
||||
// Extend when backend starts returning data
|
||||
}
|
||||
|
||||
export type RoleEnum = 'CUSTOMER';
|
||||
|
||||
export type UserStatus = 'ONBOARDING' | 'ACTIVE' | 'INACTIVE' | 'BLOCKED';
|
||||
|
||||
export type Gender = 'MALE' | 'FEMALE' | 'OTHER';
|
||||
@ -43,7 +43,12 @@ export interface CartItem {
|
||||
quantity: number;
|
||||
}
|
||||
|
||||
export type OrderStatus = 'placed' | 'confirmed' | 'dispatched' | 'delivered' | 'cancelled';
|
||||
export type OrderStatus =
|
||||
| 'placed'
|
||||
| 'confirmed'
|
||||
| 'dispatched'
|
||||
| 'delivered'
|
||||
| 'cancelled';
|
||||
|
||||
export interface Order {
|
||||
id: string;
|
||||
@ -83,3 +88,5 @@ export interface PaymentMethod {
|
||||
label: string;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
export * from './auth';
|
||||
|
||||
@ -15,7 +15,7 @@ const STORAGE_KEYS = {
|
||||
} as const;
|
||||
|
||||
// ─── Config ──────────────────────────────────────────────────────────────────
|
||||
const BASE_URL = 'https://api.example.com/v1'; // TODO: replace with your actual base URL
|
||||
const BASE_URL = 'https://a84e-202-8-116-13.ngrok-free.app'; // TODO: replace with your actual base URL
|
||||
|
||||
// ─── Token Helpers ───────────────────────────────────────────────────────────
|
||||
export const tokenManager = {
|
||||
@ -53,8 +53,8 @@ axiosInstance.interceptors.request.use(
|
||||
async (config: InternalAxiosRequestConfig) => {
|
||||
// Skip attaching token for auth endpoints (login, register, refresh)
|
||||
const publicPaths = [
|
||||
'/auth/login',
|
||||
'/auth/register',
|
||||
'/auth/otp/request',
|
||||
'/auth/otp/verify',
|
||||
'/auth/refresh-token',
|
||||
];
|
||||
const isPublic = publicPaths.some(path => config.url?.includes(path));
|
||||
@ -68,7 +68,7 @@ axiosInstance.interceptors.request.use(
|
||||
|
||||
// Debug logging (remove in production)
|
||||
if (__DEV__) {
|
||||
console.log(`🚀 [API] ${config.method?.toUpperCase()} ${config.url}`);
|
||||
// console.log(`🚀 [API] ${config.method?.toUpperCase()} ${config.url}`);s
|
||||
}
|
||||
|
||||
return config;
|
||||
|
||||
@ -46,13 +46,21 @@ export const requestLocationPermission = async (): Promise<boolean> => {
|
||||
}
|
||||
}
|
||||
|
||||
// Android
|
||||
// Android — check first to avoid showing the dialog when already granted
|
||||
try {
|
||||
const alreadyGranted = await PermissionsAndroid.check(
|
||||
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
|
||||
);
|
||||
if (alreadyGranted) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const granted = await PermissionsAndroid.request(
|
||||
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
|
||||
{
|
||||
title: 'Location Permission',
|
||||
message: 'This app needs access to your location to set it on the map.',
|
||||
message:
|
||||
'This app needs access to your location to set it on the map.',
|
||||
buttonNeutral: 'Ask Me Later',
|
||||
buttonNegative: 'Cancel',
|
||||
buttonPositive: 'OK',
|
||||
@ -88,38 +96,47 @@ export const showPermissionDeniedAlert = (): void => {
|
||||
* Get the device's current GPS position.
|
||||
* Wraps the callback-based Geolocation API in a Promise.
|
||||
*/
|
||||
/**
|
||||
* Wraps Geolocation.getCurrentPosition in a Promise.
|
||||
* Tries high-accuracy first; falls back to low-accuracy on failure.
|
||||
*/
|
||||
const getPositionOnce = (highAccuracy: boolean): Promise<LatLng> =>
|
||||
new Promise<LatLng>((resolve, reject) => {
|
||||
Geolocation.getCurrentPosition(
|
||||
(position: GeoPosition) => {
|
||||
resolve({
|
||||
latitude: position.coords.latitude,
|
||||
longitude: position.coords.longitude,
|
||||
});
|
||||
},
|
||||
(error: GeoError) => {
|
||||
console.log('Geolocation error (highAccuracy=' + highAccuracy + '):', error);
|
||||
reject(error);
|
||||
},
|
||||
{
|
||||
enableHighAccuracy: highAccuracy,
|
||||
timeout: highAccuracy ? 5000 : 10000,
|
||||
maximumAge: 10000,
|
||||
forceRequestLocation: false, // avoid native dialog that can crash
|
||||
showLocationDialog: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
export const getCurrentPosition = async (): Promise<LatLng> => {
|
||||
console.log('getCurrentPosition called');
|
||||
try {
|
||||
console.log('get current position');
|
||||
console.log('Geolocation:', Geolocation);
|
||||
console.log('getCurrentPosition:', Geolocation?.getCurrentPosition);
|
||||
|
||||
const location = await new Promise<LatLng>((resolve, reject) => {
|
||||
Geolocation.getCurrentPosition(
|
||||
(position: GeoPosition) => {
|
||||
resolve({
|
||||
latitude: position.coords.latitude,
|
||||
longitude: position.coords.longitude,
|
||||
});
|
||||
},
|
||||
(error: GeoError) => {
|
||||
console.log('Geolocation Error:', error);
|
||||
reject(error);
|
||||
},
|
||||
{
|
||||
enableHighAccuracy: true,
|
||||
timeout: 15000,
|
||||
maximumAge: 10000,
|
||||
forceRequestLocation: true,
|
||||
showLocationDialog: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
return location;
|
||||
} catch (error) {
|
||||
console.error('Failed to get current position:', error);
|
||||
throw error;
|
||||
// First attempt: high accuracy (GPS)
|
||||
return await getPositionOnce(true);
|
||||
} catch (highAccuracyError) {
|
||||
console.warn('High-accuracy location failed, retrying with low accuracy…', highAccuracyError);
|
||||
try {
|
||||
// Fallback: network / cell-tower accuracy
|
||||
return await getPositionOnce(false);
|
||||
} catch (lowAccuracyError) {
|
||||
console.error('Failed to get current position:', lowAccuracyError);
|
||||
throw lowAccuracyError;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -146,9 +163,38 @@ export const reverseGeocode = async (coords: LatLng): Promise<string> => {
|
||||
return data.results[0].formatted_address;
|
||||
}
|
||||
|
||||
console.warn('Google Maps Geocoding failed:', data.status, data.error_message || '');
|
||||
|
||||
// Fallback to OpenStreetMap Nominatim
|
||||
const osmUrl = `https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=${coords.latitude}&lon=${coords.longitude}`;
|
||||
const osmResponse = await fetch(osmUrl, {
|
||||
headers: {
|
||||
'User-Agent': 'SGDeliveryCustomerApp/1.0',
|
||||
},
|
||||
});
|
||||
const osmData = await osmResponse.json();
|
||||
if (osmData && osmData.display_name) {
|
||||
return osmData.display_name;
|
||||
}
|
||||
|
||||
return 'Address not found';
|
||||
} catch (error) {
|
||||
console.log('reverseGeocode error', error);
|
||||
try {
|
||||
// Fallback to OpenStreetMap Nominatim on catch
|
||||
const osmUrl = `https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=${coords.latitude}&lon=${coords.longitude}`;
|
||||
const osmResponse = await fetch(osmUrl, {
|
||||
headers: {
|
||||
'User-Agent': 'SGDeliveryCustomerApp/1.0',
|
||||
},
|
||||
});
|
||||
const osmData = await osmResponse.json();
|
||||
if (osmData && osmData.display_name) {
|
||||
return osmData.display_name;
|
||||
}
|
||||
} catch (osmError) {
|
||||
console.log('OSM fallback error', osmError);
|
||||
}
|
||||
return 'Unable to fetch address';
|
||||
}
|
||||
};
|
||||
@ -166,9 +212,10 @@ export const reverseGeocode = async (coords: LatLng): Promise<string> => {
|
||||
export const getCurrentLocationWithAddress =
|
||||
async (): Promise<LocationResult> => {
|
||||
const hasPermission = await requestLocationPermission();
|
||||
|
||||
console.log('hasPermission', hasPermission);
|
||||
if (!hasPermission) {
|
||||
showPermissionDeniedAlert();
|
||||
// Alert shown by the caller so it doesn't fire inside a Promise chain
|
||||
// on mount (which can cause a crash on some Android versions).
|
||||
throw new Error('Location permission denied');
|
||||
}
|
||||
|
||||
|
||||
@ -1,129 +1,66 @@
|
||||
import { createAction, createReducer } from '@reduxjs/toolkit';
|
||||
import { User, Location } from '../../../interfaces';
|
||||
import { loginWithPhone, verifyOtp, updateProfile, logoutUser } from './thunk';
|
||||
import { createReducer } from '@reduxjs/toolkit';
|
||||
import { LoginResponse, VerifyOtpResponse } from '../../../interfaces';
|
||||
import { loginWithPhone, verifyOtp } from './thunk';
|
||||
import { User } from '@interfaces/auth';
|
||||
|
||||
// ─── State ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface AuthState {
|
||||
loginData: LoginResponse | null;
|
||||
user: User | null;
|
||||
isAuthenticated: boolean;
|
||||
isLoading: boolean;
|
||||
onboardingComplete: boolean;
|
||||
accessToken: string | null;
|
||||
refreshToken: string | null;
|
||||
isNewUser: boolean | null;
|
||||
error: string | null;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
const initialState: AuthState = {
|
||||
loginData: null,
|
||||
user: null,
|
||||
isAuthenticated: false,
|
||||
accessToken: null,
|
||||
refreshToken: null,
|
||||
isNewUser: null,
|
||||
isLoading: false,
|
||||
onboardingComplete: false,
|
||||
error: null,
|
||||
};
|
||||
|
||||
// ─── Sync Actions ────────────────────────────────────────────────────────────
|
||||
|
||||
export const setUser = createAction<User>('auth/setUser');
|
||||
export const setLocation = createAction<Location>('auth/setLocation');
|
||||
export const completeProfile = createAction<{
|
||||
name: string;
|
||||
email: string;
|
||||
locationLabels: string[];
|
||||
}>('auth/completeProfile');
|
||||
export const completeOnboarding = createAction('auth/completeOnboarding');
|
||||
export const logout = createAction('auth/logout');
|
||||
export const clearError = createAction('auth/clearError');
|
||||
|
||||
// ─── Reducer ─────────────────────────────────────────────────────────────────
|
||||
|
||||
const authReducer = createReducer(initialState, builder => {
|
||||
builder
|
||||
// ── Sync Actions ───────────────────────────────────────────────────────
|
||||
.addCase(setUser, (state, action) => {
|
||||
state.user = action.payload;
|
||||
})
|
||||
.addCase(setLocation, (state, _action) => {
|
||||
if (state.user) {
|
||||
state.user = { ...state.user };
|
||||
}
|
||||
})
|
||||
.addCase(completeProfile, (state, action) => {
|
||||
if (state.user) {
|
||||
state.user = {
|
||||
...state.user,
|
||||
name: action.payload.name,
|
||||
email: action.payload.email,
|
||||
locationLabels: action.payload.locationLabels,
|
||||
};
|
||||
}
|
||||
})
|
||||
.addCase(completeOnboarding, state => {
|
||||
state.onboardingComplete = true;
|
||||
})
|
||||
.addCase(logout, state => {
|
||||
state.user = null;
|
||||
state.isAuthenticated = false;
|
||||
state.onboardingComplete = false;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(clearError, state => {
|
||||
state.error = null;
|
||||
})
|
||||
|
||||
// ── loginWithPhone ─────────────────────────────────────────────────────
|
||||
// Login
|
||||
.addCase(loginWithPhone.pending, state => {
|
||||
state.isLoading = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(loginWithPhone.fulfilled, state => {
|
||||
.addCase(loginWithPhone.fulfilled, (state, action) => {
|
||||
state.loginData = action.payload;
|
||||
state.isLoading = false;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(loginWithPhone.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.error = (action.payload as string) || 'Login failed';
|
||||
state.error = action.payload as string;
|
||||
})
|
||||
|
||||
// ── verifyOtp ──────────────────────────────────────────────────────────
|
||||
// Verify OTP
|
||||
.addCase(verifyOtp.pending, state => {
|
||||
state.isLoading = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(verifyOtp.fulfilled, (state, action) => {
|
||||
const { user, accessToken, refreshToken, isNewUser } = action.payload;
|
||||
state.user = user;
|
||||
state.accessToken = accessToken;
|
||||
state.refreshToken = refreshToken;
|
||||
state.isNewUser = isNewUser;
|
||||
state.isLoading = false;
|
||||
state.isAuthenticated = true;
|
||||
state.user = action.payload.user;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(verifyOtp.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.error = (action.payload as string) || 'OTP verification failed';
|
||||
})
|
||||
|
||||
// ── updateProfile ──────────────────────────────────────────────────────
|
||||
.addCase(updateProfile.pending, state => {
|
||||
state.isLoading = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(updateProfile.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.user = action.payload.user;
|
||||
})
|
||||
.addCase(updateProfile.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.error = (action.payload as string) || 'Profile update failed';
|
||||
})
|
||||
|
||||
// ── logoutUser ─────────────────────────────────────────────────────────
|
||||
.addCase(logoutUser.fulfilled, state => {
|
||||
state.user = null;
|
||||
state.isAuthenticated = false;
|
||||
state.onboardingComplete = false;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(logoutUser.rejected, state => {
|
||||
// Still clear state even if API fails (tokens already cleared in thunk)
|
||||
state.user = null;
|
||||
state.isAuthenticated = false;
|
||||
state.onboardingComplete = false;
|
||||
state.error = null;
|
||||
state.error = action.payload as string;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -1,18 +1,23 @@
|
||||
import { createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { loginApi, verifyOtpApi, updateProfileApi, logoutApi } from '../../../api/authApi';
|
||||
import {
|
||||
loginApi,
|
||||
verifyOtpApi,
|
||||
updateProfileApi,
|
||||
logoutApi,
|
||||
} from '../../../api/authApi';
|
||||
import { tokenManager } from '../../../services/apiClient';
|
||||
|
||||
// ─── Login ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export const loginWithPhone = createAsyncThunk(
|
||||
'auth/loginWithPhone',
|
||||
async (mobileNumber: string, { rejectWithValue }) => {
|
||||
async (phone: string, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await loginApi(mobileNumber);
|
||||
const response = await loginApi(phone);
|
||||
console.log(response);
|
||||
return response;
|
||||
} catch (error: unknown) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : 'Login failed';
|
||||
const message = error instanceof Error ? error.message : 'Login failed';
|
||||
return rejectWithValue(message);
|
||||
}
|
||||
},
|
||||
@ -23,11 +28,11 @@ export const loginWithPhone = createAsyncThunk(
|
||||
export const verifyOtp = createAsyncThunk(
|
||||
'auth/verifyOtp',
|
||||
async (
|
||||
{ mobileNumber, otp }: { mobileNumber: string; otp: string },
|
||||
{ phone, code, role }: { phone: string; code: string; role: string },
|
||||
{ rejectWithValue },
|
||||
) => {
|
||||
try {
|
||||
const response = await verifyOtpApi(mobileNumber, otp);
|
||||
const response = await verifyOtpApi(phone, code, role);
|
||||
|
||||
// Persist tokens on successful verification
|
||||
if (response.accessToken && response.refreshToken) {
|
||||
@ -77,8 +82,7 @@ export const logoutUser = createAsyncThunk(
|
||||
} catch (error: unknown) {
|
||||
// Clear tokens even if API call fails
|
||||
await tokenManager.clearTokens();
|
||||
const message =
|
||||
error instanceof Error ? error.message : 'Logout failed';
|
||||
const message = error instanceof Error ? error.message : 'Logout failed';
|
||||
return rejectWithValue(message);
|
||||
}
|
||||
},
|
||||
|
||||
@ -9,6 +9,10 @@ module.exports = {
|
||||
'@components': './app/components',
|
||||
'@features/screens': './app/features/screens',
|
||||
'@theme': './app/theme',
|
||||
'@hooks': './app/hooks',
|
||||
'@navigation': './app/navigation',
|
||||
'@services': './app/services',
|
||||
'@store': './app/store',
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
4
index.js
4
index.js
@ -2,4 +2,8 @@ import { AppRegistry } from 'react-native';
|
||||
import App from './app/App';
|
||||
import { name as appName } from './app.json';
|
||||
|
||||
if (__DEV__) {
|
||||
require('./ReactotronConfig');
|
||||
}
|
||||
|
||||
AppRegistry.registerComponent(appName, () => App);
|
||||
|
||||
@ -29,6 +29,7 @@
|
||||
"react-native-svg": "^15.15.5",
|
||||
"react-native-worklets": "^0.10.0",
|
||||
"react-redux": "^9.3.0",
|
||||
"reactotron-react-native": "^5.2.0",
|
||||
"redux-persist": "^6.0.0",
|
||||
"redux-thunk": "^3.1.0"
|
||||
},
|
||||
|
||||
@ -9,7 +9,17 @@
|
||||
"@features/screens": ["./app/features/screens"],
|
||||
"@features/screens/*": ["./app/features/screens/*"],
|
||||
"@theme": ["./app/theme"],
|
||||
"@theme/*": ["./app/theme/*"]
|
||||
"@theme/*": ["./app/theme/*"],
|
||||
"@api": ["./app/api"],
|
||||
"@api/*": ["./app/api/*"],
|
||||
"@store": ["./app/store"],
|
||||
"@store/*": ["./app/store/*"],
|
||||
"@interfaces": ["./app/interfaces"],
|
||||
"@interfaces/*": ["./app/interfaces/*"],
|
||||
"@navigation": ["./app/navigation"],
|
||||
"@navigation/*": ["./app/navigation/*"],
|
||||
"@hooks": ["./app/hooks"],
|
||||
"@hooks/*": ["./app/hooks/*"]
|
||||
}
|
||||
},
|
||||
"include": ["**/*.ts", "**/*.tsx"],
|
||||
|
||||
26
yarn.lock
26
yarn.lock
@ -5290,6 +5290,11 @@ minipass@^4.2.4:
|
||||
resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.3.tgz#79389b4eb1bb2d003a9bba87d492f2bd37bdc65b"
|
||||
integrity sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==
|
||||
|
||||
mitt@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/mitt/-/mitt-3.0.1.tgz#ea36cf0cc30403601ae074c8f77b7092cdab36d1"
|
||||
integrity sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==
|
||||
|
||||
mkdirp@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
|
||||
@ -5935,6 +5940,27 @@ react@19.2.3:
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-19.2.3.tgz#d83e5e8e7a258cf6b4fe28640515f99b87cd19b8"
|
||||
integrity sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==
|
||||
|
||||
reactotron-core-client@2.10.0:
|
||||
version "2.10.0"
|
||||
resolved "https://registry.yarnpkg.com/reactotron-core-client/-/reactotron-core-client-2.10.0.tgz#494e6e56bb018774a0a63c797d48b53ac6986256"
|
||||
integrity sha512-mOzkc1Rb4sN98OLY/frkP0e1A60bTFjuXUgG8D4m+pwCcjVCWZupdSFV8c5+gqcZBnreoVR20/ko6D8X/7KPvg==
|
||||
dependencies:
|
||||
reactotron-core-contract "0.4.0"
|
||||
|
||||
reactotron-core-contract@0.4.0:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.yarnpkg.com/reactotron-core-contract/-/reactotron-core-contract-0.4.0.tgz#7227fe19be6b564c083318798bdf88af943ca995"
|
||||
integrity sha512-o7m4mSzaUM/IGDc1dBfe0q6rgSi7mZCP7FCs3kHF7AJQwXBOKx/awjVaeNVqz+w6ADiw7X4dtDdQvEROAXdD3w==
|
||||
|
||||
reactotron-react-native@^5.2.0:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/reactotron-react-native/-/reactotron-react-native-5.2.0.tgz#e722f92860951b383d17400115825aa3a3b10502"
|
||||
integrity sha512-7Jd47Eti4wvyn1p5MsQ4mEg2mGG8B5HIZHASjK5RKVfOajEP9Sf4Vume9URG6inB/UQWGpBP6ahCGqcSo4qn3w==
|
||||
dependencies:
|
||||
mitt "^3.0.1"
|
||||
reactotron-core-client "2.10.0"
|
||||
reactotron-core-contract "0.4.0"
|
||||
|
||||
readable-stream@^3.4.0:
|
||||
version "3.6.2"
|
||||
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user