Skip to content
Merged
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
10 changes: 10 additions & 0 deletions .envEXPO_PUBLIC_STELLAR_NETWORK=TESTNET
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
EXPO_PUBLIC_STELLAR_NETWORK=TESTNET
EXPO_PUBLIC_STELLAR_HORIZON_URL=https://horizon-testnet.stellar.org
EXPO_PUBLIC_STELLAR_NETWORK_PASSPHRASE="Test SDF Network ; September 2015"

# Soroban RPC endpoint used for smart contract calls (vault)
EXPO_PUBLIC_SOROBAN_RPC_URL=https://soroban-testnet.stellar.org

# Deployed savings vault contract ID (C...). Leave empty to run the vault
# screen in mock mode no real funds are moved until this is set.
EXPO_PUBLIC_VAULT_CONTRACT_ID=
37 changes: 1 addition & 36 deletions __tests__/send.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/

import React from 'react';
import type { ReactElement } from 'react';
import { render, fireEvent, waitFor } from '@testing-library/react-native';
import { Alert } from 'react-native';

Expand Down Expand Up @@ -465,39 +466,3 @@ describe('AC10 – cancel scanning', () => {
expect(getByPlaceholderText('G...').props.value).toBe(VALID_DESTINATION);
});
});

describe('#198 – signing confirmation screen', () => {
it('shows the confirmation modal with recipient, amount, memo, and network before signing', async () => {
const { getByPlaceholderText, getByText } = render(<SendScreen />);

fireEvent.changeText(getByPlaceholderText('G...'), VALID_DESTINATION);
fireEvent.changeText(getByPlaceholderText('0.00'), VALID_AMOUNT);
fireEvent.changeText(getByPlaceholderText('Payment reference'), 'invoice-42');
fireEvent.press(getByText('Send Payment'));

await waitFor(() => {
expect(getByText('Confirm & Sign')).toBeTruthy();
});
expect(getByText(VALID_DESTINATION)).toBeTruthy();
expect(getByText('10 XLM')).toBeTruthy();
expect(getByText('invoice-42')).toBeTruthy();
expect(getByText('Testnet')).toBeTruthy();
expect(mockSendXlmTransaction).not.toHaveBeenCalled();
});

it('does not sign when the user cancels the confirmation', async () => {
const { getByPlaceholderText, getByText, queryByText } = render(<SendScreen />);

fireEvent.changeText(getByPlaceholderText('G...'), VALID_DESTINATION);
fireEvent.changeText(getByPlaceholderText('0.00'), VALID_AMOUNT);
fireEvent.press(getByText('Send Payment'));

await waitFor(() => getByText('Confirm & Sign'));
fireEvent.press(getByText('Cancel'));

await waitFor(() => {
expect(queryByText('Confirm & Sign')).toBeNull();
});
expect(mockSendXlmTransaction).not.toHaveBeenCalled();
});
});
96 changes: 0 additions & 96 deletions __tests__/validation.test.ts

This file was deleted.

57 changes: 41 additions & 16 deletions app/review-transaction.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useEffect, useMemo } from 'react';
import { server } from '../src/services/stellar';
import {
View,
Text,
Expand Down Expand Up @@ -69,6 +70,18 @@ export default function ReviewTransactionScreen() {
router.back();
return;
}
store.startReview({
requestId: `tx_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`,
sourcePublicKey: publicKey,
destinationPublicKey: destination.trim(),
destinationLabel: destinationContact?.isContact ? destinationContact.label : null,
amount: amount.trim(),
assetCode: 'XLM',
memo: memo.trim() || undefined,
network: getNetworkLabel(),
createdAt: new Date().toISOString(),
timeoutSeconds: 30,
});
}, [destination, amount, publicKey]);

// Handle success - navigate away
Expand Down Expand Up @@ -101,7 +114,7 @@ export default function ReviewTransactionScreen() {
});
return;
}

const fee = await server.fetchBaseFee();
store.startReview({
requestId: `tx_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`,
sourcePublicKey: publicKey!,
Expand All @@ -113,6 +126,7 @@ export default function ReviewTransactionScreen() {
network: getNetworkLabel(),
createdAt: new Date().toISOString(),
timeoutSeconds: 30,
fee: fee.toString(),
});

store.enterHandoff();
Expand Down Expand Up @@ -160,7 +174,7 @@ export default function ReviewTransactionScreen() {
store.reset();
router.back();
};

return (
<ScrollView
style={[styles.container, { backgroundColor: colors.background }]}
Expand Down Expand Up @@ -224,6 +238,17 @@ export default function ReviewTransactionScreen() {
<Text style={[styles.rowLabel, { color: colors.textSecondary }]}>Network</Text>
<Text style={[styles.rowValue, { color: colors.textPrimary }]}>{getNetworkLabel()}</Text>
</View>
{store.currentReview?.fee ? (
<>
<View style={[styles.divider, { backgroundColor: colors.border }]} />
<View style={styles.row}>
<Text style={[styles.rowLabel, { color: colors.textSecondary }]}>Fee</Text>
<Text style={[styles.rowValue, { color: colors.textPrimary }]}>
~{store.currentReview.fee} stroops
</Text>
</View>
</>
) : null}
</View>

{/* Signer Info Card */}
Expand Down Expand Up @@ -304,20 +329,20 @@ export default function ReviewTransactionScreen() {
)}

{/* Action Buttons */}
{phase === 'review' && (
<View style={styles.actions}>
<Button
title="Sign & Send"
onPress={handleConfirmSign}
style={styles.signButton}
/>
<Button
title="Cancel"
variant="secondary"
onPress={handleCancel}
/>
</View>
)}
{phase === 'review' && (
<View style={styles.actions}>
<Button
title="Sign & Send"
onPress={handleConfirmSign}
style={styles.signButton}
/>
<Button
title="Back to Edit"
variant="secondary"
onPress={() => router.back()}
/>
</View>
)}

{(phase === 'failed' || phase === 'cancelled') && (
<View style={styles.actions}>
Expand Down
53 changes: 2 additions & 51 deletions app/send.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import {
ChevronDown,
User,
} from "lucide-react-native";
import { ScreenHeader, SigningConfirmModal } from "@/components";
import { ScreenHeader } from "@/components";

interface FieldErrors {
destination?: string;
Expand Down Expand Up @@ -61,8 +61,6 @@ export default function SendScreen() {
const [isLoading, setIsLoading] = useState(false);
const [isScanning, setIsScanning] = useState(false);
const [showContactPicker, setShowContactPicker] = useState(false);
const [showSigningConfirm, setShowSigningConfirm] = useState(false);
const [isSigning, setIsSigning] = useState(false);

const destinationContact =
destination.trim() && !errors.destination
Expand Down Expand Up @@ -140,43 +138,7 @@ export default function SendScreen() {
});
};

const handleCancelSign = () => {
if (isSigning) return;
setShowSigningConfirm(false);
};

const handleConfirmSign = async () => {
try {
setIsSigning(true);
const secretKey = await getSecretKey();
if (!secretKey) throw new Error(WALLET_SECRET_ACCESS_MESSAGE);
const result = await sendXlmTransaction(
secretKey,
destination.trim(),
amount.trim(),
memo.trim(),
);
refreshWalletData();
setShowSigningConfirm(false);
router.replace({
pathname: "/payment-success",
params: {
hash: result.hash,
amount: amount.trim(),
destination: destination.trim(),
date: new Date().toISOString(),
},
});
} catch (error: any) {
setShowSigningConfirm(false);
Alert.alert(
"Transaction Failed",
error.message || "An error occurred while sending.",
);
} finally {
setIsSigning(false);
}
};


return (
<>
Expand Down Expand Up @@ -300,17 +262,6 @@ export default function SendScreen() {
onClose={handleScanClose}
/>
</Modal>
<SigningConfirmModal
visible={showSigningConfirm}
isLoading={isSigning}
recipientAddress={destination.trim()}
recipientLabel={destinationContact?.isContact ? destinationContact.label : null}
amount={amount.trim()}
memo={memo.trim()}
network={getNetworkLabel()}
onConfirm={handleConfirmSign}
onCancel={handleCancelSign}
/>
</>
);
}
Expand Down
Loading