434 lines
14 KiB
TypeScript
434 lines
14 KiB
TypeScript
import React, { useRef, useState } from "react";
|
|
import {
|
|
Text,
|
|
View,
|
|
ScrollView,
|
|
TextInput,
|
|
ActivityIndicator,
|
|
} from "react-native";
|
|
import { Button } from "~/components/ui/button";
|
|
import { router, useLocalSearchParams } from "expo-router";
|
|
import BackButton from "~/components/ui/backButton";
|
|
import { useAuthWithProfile } from "~/lib/hooks/useAuthWithProfile";
|
|
import useTransactions from "~/lib/hooks/useTransactions";
|
|
import { ROUTES } from "~/lib/routes";
|
|
import ScreenWrapper from "~/components/ui/ScreenWrapper";
|
|
import { useTranslation } from "react-i18next";
|
|
import ModalToast from "~/components/ui/toast";
|
|
import { TicketService } from "~/lib/services/ticketService";
|
|
import { awardPoints } from "~/lib/services/pointsService";
|
|
import { applyReferral } from "~/lib/services/referralService";
|
|
|
|
export default function TransDetail() {
|
|
const { t } = useTranslation();
|
|
const params = useLocalSearchParams<{
|
|
transactionId?: string;
|
|
amount?: string;
|
|
type?: string;
|
|
recipientName?: string;
|
|
date?: string;
|
|
status?: string;
|
|
note?: string;
|
|
flowType?: string;
|
|
ticketTierId?: string;
|
|
eventId?: string;
|
|
ticketCount?: string;
|
|
fromHistory?: string;
|
|
}>();
|
|
|
|
const { user } = useAuthWithProfile();
|
|
|
|
const [toastVisible, setToastVisible] = useState(false);
|
|
const [toastTitle, setToastTitle] = useState("");
|
|
const [toastDescription, setToastDescription] = useState<string | undefined>(
|
|
undefined
|
|
);
|
|
const [toastVariant, setToastVariant] = useState<
|
|
"success" | "error" | "warning" | "info"
|
|
>("info");
|
|
const toastTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
|
|
const [isProcessing, setIsProcessing] = useState(false);
|
|
const [referralCode, setReferralCode] = useState("");
|
|
|
|
const showToast = (
|
|
title: string,
|
|
description?: string,
|
|
variant: "success" | "error" | "warning" | "info" = "info"
|
|
) => {
|
|
if (toastTimeoutRef.current) {
|
|
clearTimeout(toastTimeoutRef.current);
|
|
}
|
|
|
|
setToastTitle(title);
|
|
setToastDescription(description);
|
|
setToastVariant(variant);
|
|
setToastVisible(true);
|
|
|
|
toastTimeoutRef.current = setTimeout(() => {
|
|
setToastVisible(false);
|
|
toastTimeoutRef.current = null;
|
|
}, 2500);
|
|
};
|
|
|
|
// Parse amount from cents to dollars
|
|
const amountInCents = parseInt(params.amount || "0");
|
|
const amountInDollars = amountInCents / 100;
|
|
|
|
// Calculate processing fee (1.25% of the amount)
|
|
const processingFeeInCents = Math.round(amountInCents * 0.0125);
|
|
const processingFeeInDollars = processingFeeInCents / 100;
|
|
|
|
// Calculate total (amount + processing fee)
|
|
const totalInCents = amountInCents + processingFeeInCents;
|
|
const totalInDollars = totalInCents / 100;
|
|
|
|
const isEventTicket = params.type === "event_ticket";
|
|
const fromHistory = params.fromHistory === "true";
|
|
|
|
// Format date
|
|
const formatDate = (dateString?: string) => {
|
|
if (!dateString) return t("transdetail.dateUnknown");
|
|
const date = new Date(dateString);
|
|
return date.toLocaleDateString("en-US", {
|
|
month: "2-digit",
|
|
day: "2-digit",
|
|
year: "numeric",
|
|
});
|
|
};
|
|
|
|
// Get transaction description based on type
|
|
const getTransactionDescription = () => {
|
|
switch (params.type) {
|
|
case "send":
|
|
return t("transdetail.descriptionSend", {
|
|
recipientName: params.recipientName,
|
|
});
|
|
case "receive":
|
|
return t("transdetail.descriptionReceive", {
|
|
recipientName: params.recipientName,
|
|
});
|
|
case "add_cash":
|
|
return t("transdetail.descriptionAddCash");
|
|
case "cash_out":
|
|
return t("transdetail.descriptionCashOut");
|
|
case "event_ticket":
|
|
return `Ticket purchase for ${params.recipientName || "event"}`;
|
|
default:
|
|
return t("transdetail.descriptionDefault");
|
|
}
|
|
};
|
|
|
|
const handlePrimaryAction = async () => {
|
|
if (isEventTicket) {
|
|
if (!user) {
|
|
showToast(
|
|
t("transconfirm.toastErrorTitle"),
|
|
"You must be logged in to buy tickets.",
|
|
"error"
|
|
);
|
|
return;
|
|
}
|
|
|
|
const ticketTierId = params.ticketTierId;
|
|
const eventId = params.eventId;
|
|
const ticketCount = params.ticketCount ? parseInt(params.ticketCount) : 1;
|
|
|
|
if (!ticketTierId || !eventId || !ticketCount || isNaN(ticketCount)) {
|
|
showToast(
|
|
t("transconfirm.toastErrorTitle"),
|
|
"Missing ticket information.",
|
|
"error"
|
|
);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
setIsProcessing(true);
|
|
const token = await user.getIdToken();
|
|
await TicketService.buyTicket(token, {
|
|
ticketTierId,
|
|
eventId,
|
|
ticketCount,
|
|
});
|
|
|
|
try {
|
|
await awardPoints("purchase_ticket");
|
|
} catch (error) {
|
|
console.warn(
|
|
"[TransDetail] Failed to award purchase ticket points",
|
|
error
|
|
);
|
|
}
|
|
|
|
// Apply referral for event purchase if a code was provided
|
|
if (referralCode.trim() && eventId) {
|
|
try {
|
|
const referralResult = await applyReferral({
|
|
referralCode: referralCode.trim(),
|
|
uid: user.uid,
|
|
referralReason: "event",
|
|
contextId: eventId,
|
|
});
|
|
|
|
console.log(
|
|
"[TransDetail] Event referral apply result",
|
|
referralResult
|
|
);
|
|
|
|
if (!referralResult.success) {
|
|
console.warn(
|
|
"[TransDetail] Failed to apply event referral:",
|
|
referralResult.error
|
|
);
|
|
showToast(
|
|
t("transconfirm.toastErrorTitle"),
|
|
referralResult.error || "Failed to apply referral code",
|
|
"error"
|
|
);
|
|
}
|
|
} catch (referralError) {
|
|
console.error(
|
|
"[TransDetail] Error while applying event referral code",
|
|
referralError
|
|
);
|
|
showToast(
|
|
t("transconfirm.toastErrorTitle"),
|
|
"Failed to apply referral code",
|
|
"error"
|
|
);
|
|
}
|
|
}
|
|
|
|
router.replace({
|
|
pathname: "/(screens)/taskcomp",
|
|
params: {
|
|
amount: amountInDollars.toFixed(2),
|
|
message: `Ticket for ${
|
|
params.recipientName || "event"
|
|
} purchased successfully.`,
|
|
flowType: "event_ticket",
|
|
},
|
|
});
|
|
} catch (error) {
|
|
console.error("[TransDetail] Error buying ticket", error);
|
|
|
|
let apiMessage: string | undefined;
|
|
if (error instanceof Error) {
|
|
try {
|
|
const parsed = JSON.parse(error.message);
|
|
if (parsed && typeof parsed.message === "string") {
|
|
apiMessage = parsed.message;
|
|
}
|
|
} catch {}
|
|
}
|
|
|
|
showToast(
|
|
t("transconfirm.toastErrorTitle"),
|
|
apiMessage || "Could not complete ticket purchase. Please try again.",
|
|
"error"
|
|
);
|
|
} finally {
|
|
setIsProcessing(false);
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (!fromHistory && referralCode.trim() && user && params.transactionId) {
|
|
try {
|
|
setIsProcessing(true);
|
|
const referralResult = await applyReferral({
|
|
referralCode: referralCode.trim(),
|
|
uid: user.uid,
|
|
referralReason: "transaction",
|
|
contextId: params.transactionId,
|
|
});
|
|
|
|
console.log(
|
|
"[TransDetail] Transaction referral apply result",
|
|
referralResult
|
|
);
|
|
|
|
if (!referralResult.success) {
|
|
console.warn(
|
|
"[TransDetail] Failed to apply transaction referral:",
|
|
referralResult.error
|
|
);
|
|
showToast(
|
|
t("transconfirm.toastErrorTitle"),
|
|
referralResult.error || "Failed to apply referral code",
|
|
"error"
|
|
);
|
|
}
|
|
} catch (referralError) {
|
|
console.error(
|
|
"[TransDetail] Error while applying transaction referral code",
|
|
referralError
|
|
);
|
|
showToast(
|
|
t("transconfirm.toastErrorTitle"),
|
|
"Failed to apply referral code",
|
|
"error"
|
|
);
|
|
} finally {
|
|
setIsProcessing(false);
|
|
}
|
|
}
|
|
|
|
router.replace(ROUTES.HOME);
|
|
router.push(ROUTES.SEND_OR_REQUEST_MONEY);
|
|
};
|
|
|
|
return (
|
|
<ScreenWrapper edges={[]}>
|
|
<View className="flex-1 w-full justify-between">
|
|
<ScrollView
|
|
contentContainerStyle={{
|
|
justifyContent: "center",
|
|
paddingBottom: 24,
|
|
}}
|
|
className="w-full flex-1"
|
|
>
|
|
<View>
|
|
<BackButton />
|
|
</View>
|
|
|
|
<View
|
|
className="flex flex-col py-5 pt-20 items-center"
|
|
style={{ gap: 4 }}
|
|
>
|
|
<Text className="text-xl font-dmsans-bold text-primary">
|
|
{t("transdetail.title")}
|
|
</Text>
|
|
</View>
|
|
|
|
<View className="bg-secondary-foreground mx-3 rounded py-10 mt-5">
|
|
<View className="flex items-center py-5" style={{ gap: 8 }}>
|
|
<Text className="text-2xl font-dmsans-bold text-primary">
|
|
${amountInDollars.toFixed(2)}
|
|
</Text>
|
|
<Text
|
|
className="text-sm text-gray-500 font-dmsans"
|
|
style={{ textAlign: "center" }}
|
|
>
|
|
{getTransactionDescription()}
|
|
</Text>
|
|
</View>
|
|
<View
|
|
className="flex px-2 py-5 border-t border-b border-gray-300 m-3"
|
|
style={{ gap: 8 }}
|
|
>
|
|
<Text className="text-sm font-dmsans-bold mt-5">
|
|
{t("transdetail.sectionTitle")}
|
|
</Text>
|
|
<View className="flex flex-row justify-between w-full">
|
|
<Text className="font-dmsans">
|
|
{t("transdetail.dateLabel")}
|
|
</Text>
|
|
<Text className="font-dmsans" style={{ textAlign: "right" }}>
|
|
{formatDate(params.date)}
|
|
</Text>
|
|
</View>
|
|
|
|
<View className="flex flex-row justify-between w-full">
|
|
<Text className="font-dmsans">
|
|
{t("transdetail.statusLabel")}
|
|
</Text>
|
|
<Text className="font-dmsans" style={{ textAlign: "right" }}>
|
|
{params.status || t("transdetail.statusUnknown")}
|
|
</Text>
|
|
</View>
|
|
|
|
{params.note && (
|
|
<View className="flex flex-row justify-between w-full">
|
|
<Text className="font-dmsans">
|
|
{t("transdetail.noteLabel")}
|
|
</Text>
|
|
<Text className="font-dmsans" style={{ textAlign: "right" }}>
|
|
{params.note}
|
|
</Text>
|
|
</View>
|
|
)}
|
|
|
|
<View className="flex flex-row justify-between w-full">
|
|
<Text className="font-dmsans">
|
|
{t("transdetail.processingFeeLabel")}
|
|
</Text>
|
|
<Text className="font-dmsans" style={{ textAlign: "right" }}>
|
|
${processingFeeInDollars.toFixed(2)}
|
|
</Text>
|
|
</View>
|
|
|
|
<View className="flex flex-row justify-between w-full">
|
|
<Text className="font-dmsans">
|
|
{t("transdetail.subtotalLabel")}
|
|
</Text>
|
|
<Text className="font-dmsans" style={{ textAlign: "right" }}>
|
|
${amountInDollars.toFixed(2)}
|
|
</Text>
|
|
</View>
|
|
|
|
<View className="flex flex-row justify-between w-full border-t border-gray-300 pt-5">
|
|
<Text className="font-dmsans-bold text-base">
|
|
{t("transdetail.totalLabel")}
|
|
</Text>
|
|
<Text
|
|
className="font-dmsans text-base"
|
|
style={{ textAlign: "right" }}
|
|
>
|
|
${totalInDollars.toFixed(2)}
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Referral code input (only shown when not coming from History) */}
|
|
{!fromHistory && (
|
|
<View className="px-6 mt-6">
|
|
<Text className="text-base font-dmsans text-black mb-2">
|
|
{t("transdetail.referralCodeLabel", "Referral Code")}
|
|
</Text>
|
|
<TextInput
|
|
value={referralCode}
|
|
onChangeText={setReferralCode}
|
|
placeholder="Enter referral code"
|
|
placeholderTextColor="#9CA3AF"
|
|
className="w-full rounded-2xl px-4 py-3 bg-[#FFF5E9] border border-[#D9E2D5] font-dmsans text-base text-black"
|
|
/>
|
|
</View>
|
|
)}
|
|
</ScrollView>
|
|
|
|
<View className="w-full px-5 pb-8" style={{ gap: 12 }}>
|
|
<Button
|
|
className="bg-primary rounded-full"
|
|
onPress={handlePrimaryAction}
|
|
disabled={isProcessing}
|
|
>
|
|
{isProcessing ? (
|
|
<View
|
|
className="flex-row items-center justify-center"
|
|
style={{ gap: 8 }}
|
|
>
|
|
<ActivityIndicator size="small" color="#FFFFFF" />
|
|
<Text className="font-dmsans text-white">Processing...</Text>
|
|
</View>
|
|
) : (
|
|
<Text className="font-dmsans text-white">
|
|
{isEventTicket ? "Pay" : t("transdetail.sendAgainButton")}
|
|
</Text>
|
|
)}
|
|
</Button>
|
|
</View>
|
|
</View>
|
|
<ModalToast
|
|
visible={toastVisible}
|
|
title={toastTitle}
|
|
description={toastDescription}
|
|
variant={toastVariant}
|
|
/>
|
|
</ScreenWrapper>
|
|
);
|
|
}
|