Skip to content
Open
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
25 changes: 20 additions & 5 deletions App.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,29 @@
import { SafeAreaProvider } from 'react-native-safe-area-context';
import InvestScreen from 'components/pages/InvestScreen';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { MainLayout } from 'components/shared/MainLayout';
import InvestScreen from 'components/pages/InvestScreen';
import CreateAccountScreen from 'components/pages/CreateAccountScreen';
import PayScreen from 'components/pages/PayScreen';
import { RootStackParamList } from 'types/Navigation';
import './global.css';

const Stack = createNativeStackNavigator<RootStackParamList>();

export default function App() {
return (
<SafeAreaProvider>
<MainLayout>
<InvestScreen />
</MainLayout>
<NavigationContainer>
<MainLayout>
<Stack.Navigator
initialRouteName="Invest Screen"
screenOptions={{ headerShown: false }}>
<Stack.Screen name="Invest Screen" component={InvestScreen} />
<Stack.Screen name="Create Account" component={CreateAccountScreen} />
<Stack.Screen name="Pay Screen" component={PayScreen} />
</Stack.Navigator>
</MainLayout>
</NavigationContainer>
</SafeAreaProvider>
);
}
}
52 changes: 52 additions & 0 deletions api/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* Auth API module
* Covers: POST /auth/nonce and POST /auth/verify
*/

import { apiRequest } from '../lib/apiClient';

// ─── DTOs ────────────────────────────────────────────────────────────────────

export interface NonceRequestDto {
wallet: string;
}

export interface NonceResponseDto {
nonce: string;
expiresAt: string; // ISO-8601 datetime string
}

export interface VerifyRequestDto {
wallet: string;
nonce: string;
signature: string;
}

export interface VerifyResponseDto {
accessToken: string;
refreshToken: string;
}

// ─── API calls ───────────────────────────────────────────────────────────────

/**
* Step 1 β€” Request a nonce for the given wallet address.
* Public endpoint, no auth header needed.
*/
export async function fetchNonce(wallet: string): Promise<NonceResponseDto> {
return apiRequest<NonceResponseDto>('POST', '/auth/nonce', {
authenticated: false,
body: { wallet } satisfies NonceRequestDto,
});
}

/**
* Step 2 β€” Submit the signed nonce to authenticate (register or login).
* Returns accessToken and refreshToken on success.
*/
export async function verifySignature(dto: VerifyRequestDto): Promise<VerifyResponseDto> {
return apiRequest<VerifyResponseDto>('POST', '/auth/verify', {
authenticated: false,
body: dto,
});
}
42 changes: 42 additions & 0 deletions api/users.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* Users API module
* Covers: GET /users/me and PATCH /users/me
*/

import { apiRequest } from '../lib/apiClient';

// ─── DTOs ────────────────────────────────────────────────────────────────────

export interface UserProfile {
id: string;
wallet: string;
username: string;
displayName: string;
profileImage: string | null;
}

export interface UpdateProfileDto {
username?: string;
displayName?: string;
profileImage?: string | null;
}

// ─── API calls ───────────────────────────────────────────────────────────────

/**
* Fetch the authenticated user's profile.
* Requires a valid access token (attached automatically by apiRequest).
*/
export async function getMe(): Promise<UserProfile> {
return apiRequest<UserProfile>('GET', '/users/me', { authenticated: true });
}

/**
* Update the authenticated user's profile with the provided fields.
*/
export async function updateMe(dto: UpdateProfileDto): Promise<UserProfile> {
return apiRequest<UserProfile>('PATCH', '/users/me', {
authenticated: true,
body: dto,
});
}
250 changes: 42 additions & 208 deletions components/pages/CreateAccountScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,208 +1,42 @@
import React from 'react';
import { View, Text, TextInput, TouchableOpacity, Image, ScrollView } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { useCreateAccount } from '../../hooks/auth/use-create-account';
// Centralized color palette shared with Tailwind
const colors = require('../../theme/colors.json');

const CreateAccountScreen = ({ navigation }: any) => {
const {
formState,
errors,
isSubmitting,
showSuccess,
handleWalletAddressChange,
handleUsernameChange,
handleDisplayNameChange,
handleTermsAcceptedChange,
pickImage,
createAccount,
isFormValid,
} = useCreateAccount();

const { profileImage, walletAddress, username, displayName, termsAccepted } = formState;

return (
<View className="flex-1 bg-background">
{/* Header */}
<View className="flex-row items-center border-b border-gray-100 bg-white px-6 pb-4 pt-12">
<TouchableOpacity
onPress={() => navigation?.goBack()}
className="mr-3"
accessibilityLabel="Go back">
<Ionicons name="chevron-back" size={24} color={colors.text} />
</TouchableOpacity>
<Text className="text-xl font-bold text-text">Create Account</Text>
</View>

<ScrollView className="flex-1" showsVerticalScrollIndicator={false}>
<View className="px-6 pb-8 pt-8">
{/* Profile Picture Upload */}
<View className="mb-6 items-center">
<View className="relative">
<View className="h-32 w-32 items-center justify-center rounded-full bg-white shadow-sm">
{profileImage ? (
<Image source={{ uri: profileImage }} className="h-32 w-32 rounded-full" />
) : (
<Ionicons name="person-outline" size={48} color={colors.textMuted} />
)}
</View>
<TouchableOpacity
onPress={pickImage}
className="absolute bottom-0 right-0 h-10 w-10 items-center justify-center rounded-full bg-primary shadow-md"
accessibilityLabel="Upload profile picture">
<Ionicons name="cloud-upload-outline" size={20} color="white" />
</TouchableOpacity>
</View>
<Text className="mt-3 text-sm text-textMuted">Upload profile picture (optional)</Text>
{errors.profileImage ? (
<Text className="mt-1 text-xs text-red-500">{errors.profileImage}</Text>
) : null}
</View>

{/* Wallet Address Input */}
<View className="mb-4">
<Text className="mb-2 text-sm text-textMuted">Wallet Address</Text>
<View className="flex-row items-center rounded-xl bg-white px-4 py-4 shadow-sm">
<Ionicons name="wallet-outline" size={20} color={colors.textMuted} className="mr-3" />
<TextInput
placeholder="G..."
placeholderTextColor={colors.placeholder}
value={walletAddress}
onChangeText={handleWalletAddressChange}
className="ml-3 flex-1 text-base text-text"
autoCapitalize="characters"
accessibilityLabel="Wallet address input"
/>
</View>
{errors.walletAddress ? (
<Text className="mt-1 text-xs text-red-500">{errors.walletAddress}</Text>
) : null}
</View>

{/* Username Input */}
<View className="mb-4">
<Text className="mb-2 text-sm text-textMuted">Username</Text>
<View className="flex-row items-center rounded-xl bg-white px-4 py-4 shadow-sm">
<Ionicons name="person-outline" size={20} color={colors.textMuted} className="mr-3" />
<Text className="ml-3 text-base text-text">@</Text>
<TextInput
placeholder="josue_crypto"
placeholderTextColor={colors.placeholder}
value={username}
onChangeText={handleUsernameChange}
className="ml-1 flex-1 text-base text-text"
autoCapitalize="none"
accessibilityLabel="Username input"
/>
</View>
{errors.username ? (
<Text className="mt-1 text-xs text-red-500">{errors.username}</Text>
) : null}
</View>

{/* Display Name Input */}
<View className="mb-6">
<Text className="mb-2 text-sm text-textMuted">Display Name</Text>
<View className="flex-row items-center rounded-xl bg-white px-4 py-4 shadow-sm">
<Ionicons name="person-outline" size={20} color={colors.textMuted} className="mr-3" />
<TextInput
placeholder="JosuΓ© MartΓ­nez"
placeholderTextColor={colors.placeholder}
value={displayName}
onChangeText={handleDisplayNameChange}
className="ml-3 flex-1 text-base text-text"
accessibilityLabel="Display name input"
/>
</View>
{errors.displayName ? (
<Text className="mt-1 text-xs text-red-500">{errors.displayName}</Text>
) : null}
</View>

{/* Info Box */}
<View className="mb-6 flex-row rounded-xl bg-primarySoft p-4">
<View className="mr-3 h-10 w-10 items-center justify-center rounded-full bg-primaryTint">
<Ionicons name="information" size={24} color="white" />
</View>
<View className="flex-1">
<Text className="mb-1 text-sm font-semibold text-primary">
Your wallet is your identity
</Text>
<Text className="text-xs leading-5 text-primary">
Make sure you have access to your wallet address. This will be used to verify your
transactions and build your reputation.
</Text>
</View>
</View>

{/* Terms & Conditions Checkbox */}
<TouchableOpacity
onPress={() => handleTermsAcceptedChange(!termsAccepted)}
className="mb-6 flex-row items-start"
activeOpacity={0.7}
accessibilityLabel="Accept terms and conditions"
accessibilityRole="checkbox">
<View
className={`h-5 w-5 rounded ${
termsAccepted ? 'bg-primary' : 'border-2 border-gray-300 bg-white'
} mr-3 mt-0.5 items-center justify-center`}>
{termsAccepted && <Ionicons name="checkmark" size={16} color="white" />}
</View>
<Text className="flex-1 text-sm text-textMuted">
By creating an account, you agree to our{' '}
<Text className="font-semibold text-primary">Terms of Service</Text> and{' '}
<Text className="font-semibold text-primary">Privacy Policy</Text>
</Text>
</TouchableOpacity>

{/* Create Account Button */}
<TouchableOpacity
onPress={createAccount}
disabled={!isFormValid() || isSubmitting}
className={`flex-row items-center justify-center rounded-xl py-4 ${
isFormValid() && !isSubmitting ? 'bg-cta' : 'bg-gray-300'
}`}
activeOpacity={0.8}
accessibilityLabel="Create account button">
{isSubmitting ? (
<>
<Text className="mr-2 text-base font-semibold text-white">Creating Account...</Text>
<Ionicons name="hourglass" size={18} color="white" />
</>
) : (
<>
<Text
className={`mr-2 text-base font-semibold ${isFormValid() ? 'text-white' : 'text-gray-500'}`}>
Create Account
</Text>
<Ionicons
name="arrow-forward"
size={18}
color={isFormValid() ? colors.white : colors.textSecondary}
/>
</>
)}
</TouchableOpacity>
</View>
</ScrollView>

{/* Success Notification */}
{showSuccess && (
<View className="absolute left-6 right-6 top-20 flex-row items-center rounded-xl bg-success p-4 shadow-lg">
<View className="mr-3 h-10 w-10 items-center justify-center rounded-full bg-white">
<Ionicons name="checkmark-circle" size={28} color={colors.success} />
</View>
<View className="flex-1">
<Text className="mb-1 text-base font-bold text-white">
Account Created Successfully!
</Text>
<Text className="text-sm text-white">Welcome to TrustUp, @{username}</Text>
</View>
</View>
)}
</View>
);
};

export default CreateAccountScreen;
/**
* Users API module
* Covers: GET /users/me and PATCH /users/me
*/

import { apiRequest } from '../lib/apiClient';

// ─── DTOs ────────────────────────────────────────────────────────────────────

export interface UserProfile {
id: string;
wallet: string;
username: string;
displayName: string;
profileImage: string | null;
}

export interface UpdateProfileDto {
username?: string;
displayName?: string;
profileImage?: string | null;
}

// ─── API calls ───────────────────────────────────────────────────────────────

/**
* Fetch the authenticated user's profile.
* Requires a valid access token (attached automatically by apiRequest).
*/
export async function getMe(): Promise<UserProfile> {
return apiRequest<UserProfile>('GET', '/users/me', { authenticated: true });
}

/**
* Update the authenticated user's profile with the provided fields.
*/
export async function updateMe(dto: UpdateProfileDto): Promise<UserProfile> {
return apiRequest<UserProfile>('PATCH', '/users/me', {
authenticated: true,
body: dto,
});
}
Loading