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
6 changes: 5 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1 +1,5 @@
{}
{
"githubPullRequests.ignoredPullRequestBranches": [
"main"
]
}
13 changes: 13 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# TODO - Connect Create Account flow to backend and navigate to Pay Screen

- [ ] Implement navigation stack in `App.tsx` (Create Account -> Pay) using `RootStackParamList`
- [ ] Add API/http client using `EXPO_PUBLIC_API_URL` and Authorization helper
- [ ] Add secure token storage (accessToken/refreshToken) helper
- [ ] Implement `POST /api/v1/auth/nonce` and `POST /api/v1/auth/verify` in `use-create-account.ts`
- [ ] Implement wallet nonce signing + signature submission aligned with backend VerifyRequestDto (add signing integration if missing)
- [ ] Implement profile sync via `GET /api/v1/users/me` and `PATCH /api/v1/users/me` as needed
- [ ] Remove mock `setTimeout + Alert` success; replace with real success + errors + loading state
- [ ] Wire `CreateAccountScreen` navigation types and navigate to `Pay Screen` after success
- [ ] Lint + typecheck + run tests
- [ ] Ensure PR template can be completed: document nonce/verify, token storage, users/me sync, wallet signing, navigation to Pay

29 changes: 29 additions & 0 deletions api/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { httpRequest } from './httpClient';

export interface NonceResponseDto {
nonce: string;
expiresAt?: string;
}

export interface VerifyRequestDto {
wallet: string;
nonce: string;
signature: string;
// If backend expects additional fields, add them here.
}

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

export const authApi = {
requestNonce: (wallet: string) => {
return httpRequest<NonceResponseDto>('POST', '/auth/nonce', { wallet });
},

verify: (payload: VerifyRequestDto) => {
return httpRequest<VerifyResponseDto>('POST', '/auth/verify', payload);
},
};

87 changes: 87 additions & 0 deletions api/httpClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { EXPO_PUBLIC_API_URL } from '../constants/env';
import { getAccessToken } from '../auth/tokenStore';

export type HttpMethod = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE';

export class ApiError extends Error {
status?: number;
details?: unknown;

constructor(message: string, status?: number, details?: unknown) {
super(message);
this.name = 'ApiError';
this.status = status;
this.details = details;
}
}

async function buildHeaders(extraHeaders?: Record<string, string>) {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
...(extraHeaders ?? {}),
};

const token = await getAccessToken();
if (token) {
headers.Authorization = `Bearer ${token}`;
}

return headers;
}

function joinUrl(base: string, path: string) {
if (!base) return path;
if (base.endsWith('/')) {
base = base.slice(0, -1);
}
if (!path.startsWith('/')) {
path = `/${path}`;
}
return `${base}${path}`;
}

export async function httpRequest<T>(
method: HttpMethod,
path: string,
body?: unknown,
extraHeaders?: Record<string, string>
): Promise<T> {
const url = joinUrl(EXPO_PUBLIC_API_URL, path);

const headers = await buildHeaders(extraHeaders);

let response: Response;
try {
response = await fetch(url, {
method,
headers,
body: body === undefined ? undefined : JSON.stringify(body),
});
} catch (e) {
throw new ApiError('Network error. Please check your connection.', undefined, e);
}

if (!response.ok) {
let details: unknown = undefined;
try {
details = await response.json();
} catch {
// ignore
}

const message =
typeof details === 'object' && details && 'message' in (details as any)
? String((details as any).message)
: `Request failed with status ${response.status}`;

throw new ApiError(message, response.status, details);
}

const text = await response.text();
if (!text) return undefined as T;

return JSON.parse(text) as T;
}



28 changes: 28 additions & 0 deletions api/users.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { httpRequest } from './httpClient';

export interface UserProfileDto {
walletAddress: string;
username: string;
displayName: string;
profileImage?: string | null;
// Add fields if backend includes more.
}

export interface GetMeResponseDto {
user: UserProfileDto;
}

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

export const usersApi = {
getMe: () => httpRequest<GetMeResponseDto>('GET', '/users/me'),

updateMe: (payload: UpdateMeRequestDto) =>
httpRequest<GetMeResponseDto | undefined>('PATCH', '/users/me', payload),
};

55 changes: 55 additions & 0 deletions auth/signing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type { VerifyRequestDto } from '../api/auth';
import { Keypair } from '@stellar/stellar-sdk';

export type SignNonceFn = (wallet: string, nonce: string) => Promise<string>;

/**
* Lobstr signing (best-effort placeholder).
*
* IMPORTANT:
* - Proper Lobstr signing requires invoking the Lobstr wallet via deep links / wallet SDK.
* - This repo does not currently contain that integration.
*
* What this does now:
* - If the app already has a Stellar secret key available (it should NOT), it will sign.
* - Otherwise it throws a clear error telling you that the wallet deep-link/integration is missing.
*
* Replace this module once the Lobstr callback/deep-link flow is known.
*/
export const signNonceWithWallet: SignNonceFn = async (wallet, nonce) => {
// Nonce signing requires the user to sign via their wallet.
// Since we don't have that integration in this repo yet, fail loudly.
// (Never log or persist secrets.)
const maybeSecret = (globalThis as any).__TRUSTUP_STELLAR_SECRET as string | undefined;
if (!maybeSecret) {
throw new Error(
'Lobstr wallet signing integration is missing in this repo. '
+ 'Implement the Lobstr deep-link / callback flow to produce the signature for auth/verify.'
);
}

// WARNING: Using secret keys in the frontend is insecure.
// This branch is only here to make the flow testable in dev.
const kp = Keypair.fromSecret(maybeSecret);
if (kp.publicKey() !== wallet) {
throw new Error('Provided wallet address does not match the configured signing key.');
}

// Sign the nonce as bytes (ed25519)
const signature = kp.sign(Buffer.from(nonce, 'utf8')).toString('base64');
return signature;
};

export async function buildVerifyPayload(params: {
wallet: string;
nonce: string;
signature: string;
}): Promise<VerifyRequestDto> {
return {
wallet: params.wallet,
nonce: params.nonce,
signature: params.signature,
};
}


30 changes: 30 additions & 0 deletions auth/tokenStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import * as SecureStore from 'expo-secure-store';

const ACCESS_TOKEN_KEY = 'trustup_access_token';
const REFRESH_TOKEN_KEY = 'trustup_refresh_token';

export async function setTokens(params: {
accessToken: string;
refreshToken?: string;
}): Promise<void> {
await SecureStore.setItemAsync(ACCESS_TOKEN_KEY, params.accessToken);
if (params.refreshToken) {
await SecureStore.setItemAsync(REFRESH_TOKEN_KEY, params.refreshToken);
}
}

export async function getAccessToken(): Promise<string | null> {
return SecureStore.getItemAsync(ACCESS_TOKEN_KEY);
}

export async function getRefreshToken(): Promise<string | null> {
return SecureStore.getItemAsync(REFRESH_TOKEN_KEY);
}

export async function clearTokens(): Promise<void> {
await SecureStore.deleteItemAsync(ACCESS_TOKEN_KEY);
await SecureStore.deleteItemAsync(REFRESH_TOKEN_KEY);
}



10 changes: 10 additions & 0 deletions constants/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Centralized environment access.
// Expo exposes env vars at build-time.
//
// Note: this repo may not have `expo-constants` installed, so we rely on
// the common Expo behavior where EXPO_PUBLIC_* values are injected.
export const EXPO_PUBLIC_API_URL: string = (process.env
.EXPO_PUBLIC_API_URL as string | undefined) ?? '';



Loading