feat(tip): add coffee tipping page
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
import {
|
||||
getPaymentUrl,
|
||||
getStripeClientSecret,
|
||||
isEzpayPayment,
|
||||
launchEzpayRedirect,
|
||||
} from "@/lib/payment/payment_launch";
|
||||
import { ROUTES } from "@/router/routes";
|
||||
import {
|
||||
usePaymentDispatch,
|
||||
usePaymentState,
|
||||
} from "@/stores/payment/payment-context";
|
||||
import { AppEnvUtil, Logger } from "@/utils";
|
||||
|
||||
import { StripePaymentDialog } from "../subscription/components/stripe-payment-dialog";
|
||||
import dialogStyles from "../subscription/components/stripe-payment-dialog.module.css";
|
||||
import styles from "./tip-screen.module.css";
|
||||
|
||||
const log = new Logger("TipCheckoutButton");
|
||||
|
||||
export interface TipCheckoutButtonProps {
|
||||
disabled?: boolean;
|
||||
isAuthLoading?: boolean;
|
||||
onOrder: () => void;
|
||||
}
|
||||
|
||||
export function TipCheckoutButton({
|
||||
disabled = false,
|
||||
isAuthLoading = false,
|
||||
onOrder,
|
||||
}: TipCheckoutButtonProps) {
|
||||
const payment = usePaymentState();
|
||||
const paymentDispatch = usePaymentDispatch();
|
||||
const launchedNonceRef = useRef(0);
|
||||
const [hiddenStripeClientSecret, setHiddenStripeClientSecret] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
const [isConfirmingEzpay, setIsConfirmingEzpay] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!payment.payParams || payment.launchNonce <= launchedNonceRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
launchedNonceRef.current = payment.launchNonce;
|
||||
const clientSecret = getStripeClientSecret(payment.payParams);
|
||||
if (clientSecret) return;
|
||||
|
||||
const paymentUrl = getPaymentUrl(payment.payParams);
|
||||
if (paymentUrl) {
|
||||
const isEzpay = isEzpayPayment(payment.payParams);
|
||||
|
||||
if (!AppEnvUtil.isProduction() && isEzpay) {
|
||||
log.debug("[tip-checkout] ezpay confirmation required", {
|
||||
orderId: payment.currentOrderId,
|
||||
paymentUrl,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (isEzpay) {
|
||||
void launchEzpayRedirect({
|
||||
orderId: payment.currentOrderId,
|
||||
paymentUrl,
|
||||
subscriptionType: "tip",
|
||||
onFailed: (errorMessage) =>
|
||||
paymentDispatch({ type: "PaymentLaunchFailed", errorMessage }),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
window.location.href = paymentUrl;
|
||||
return;
|
||||
}
|
||||
|
||||
paymentDispatch({
|
||||
type: "PaymentLaunchFailed",
|
||||
errorMessage:
|
||||
"Payment parameters did not include a supported URL or Stripe client secret.",
|
||||
});
|
||||
}, [
|
||||
payment.currentOrderId,
|
||||
payment.launchNonce,
|
||||
payment.payParams,
|
||||
paymentDispatch,
|
||||
]);
|
||||
|
||||
const isLoading =
|
||||
isAuthLoading || payment.isCreatingOrder || payment.isPollingOrder;
|
||||
const stripeClientSecret = payment.payParams
|
||||
? getStripeClientSecret(payment.payParams)
|
||||
: null;
|
||||
const shouldShowStripeDialog =
|
||||
stripeClientSecret !== null &&
|
||||
stripeClientSecret !== hiddenStripeClientSecret &&
|
||||
!payment.isPaid;
|
||||
const ezpayPaymentUrl = payment.payParams
|
||||
? getPaymentUrl(payment.payParams)
|
||||
: null;
|
||||
const shouldShowEzpayConfirmDialog =
|
||||
!AppEnvUtil.isProduction() &&
|
||||
payment.payParams &&
|
||||
isEzpayPayment(payment.payParams) &&
|
||||
ezpayPaymentUrl &&
|
||||
payment.currentOrderId
|
||||
? true
|
||||
: false;
|
||||
const label = payment.isPollingOrder
|
||||
? "Processing payment..."
|
||||
: payment.isCreatingOrder
|
||||
? "Creating order..."
|
||||
: payment.isPaid
|
||||
? "Thanks for the coffee"
|
||||
: "Order and Buy";
|
||||
|
||||
const handleStripeClose = () => {
|
||||
if (stripeClientSecret) {
|
||||
setHiddenStripeClientSecret(stripeClientSecret);
|
||||
}
|
||||
if (payment.orderStatus !== "paid") {
|
||||
paymentDispatch({ type: "PaymentReset" });
|
||||
}
|
||||
};
|
||||
|
||||
const handleStripeConfirmed = () => {
|
||||
if (stripeClientSecret) {
|
||||
setHiddenStripeClientSecret(stripeClientSecret);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEzpayConfirm = () => {
|
||||
if (!payment.currentOrderId || !ezpayPaymentUrl) return;
|
||||
setIsConfirmingEzpay(true);
|
||||
void launchEzpayRedirect({
|
||||
orderId: payment.currentOrderId,
|
||||
paymentUrl: ezpayPaymentUrl,
|
||||
subscriptionType: "tip",
|
||||
onFailed: (errorMessage) => {
|
||||
setIsConfirmingEzpay(false);
|
||||
paymentDispatch({ type: "PaymentLaunchFailed", errorMessage });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleEzpayCancel = () => {
|
||||
log.debug("[tip-checkout] ezpay confirmation cancelled", {
|
||||
orderId: payment.currentOrderId,
|
||||
});
|
||||
setIsConfirmingEzpay(false);
|
||||
paymentDispatch({ type: "PaymentReset" });
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.checkoutButton}
|
||||
disabled={disabled || isLoading}
|
||||
onClick={onOrder}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
{payment.errorMessage ? (
|
||||
<p className={styles.checkoutError} role="alert">
|
||||
{payment.errorMessage}
|
||||
</p>
|
||||
) : null}
|
||||
{shouldShowEzpayConfirmDialog ? (
|
||||
<EzpayRedirectConfirmDialog
|
||||
orderId={payment.currentOrderId ?? ""}
|
||||
isConfirming={isConfirmingEzpay}
|
||||
onCancel={handleEzpayCancel}
|
||||
onConfirm={handleEzpayConfirm}
|
||||
/>
|
||||
) : null}
|
||||
{shouldShowStripeDialog ? (
|
||||
<StripePaymentDialog
|
||||
clientSecret={stripeClientSecret}
|
||||
returnPath={ROUTES.tip}
|
||||
onClose={handleStripeClose}
|
||||
onConfirmed={handleStripeConfirmed}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
interface EzpayRedirectConfirmDialogProps {
|
||||
orderId: string;
|
||||
isConfirming: boolean;
|
||||
onCancel: () => void;
|
||||
onConfirm: () => void;
|
||||
}
|
||||
|
||||
function EzpayRedirectConfirmDialog({
|
||||
orderId,
|
||||
isConfirming,
|
||||
onCancel,
|
||||
onConfirm,
|
||||
}: EzpayRedirectConfirmDialogProps) {
|
||||
return (
|
||||
<div
|
||||
className={dialogStyles.overlay}
|
||||
role="alertdialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="tip-ezpay-redirect-title"
|
||||
>
|
||||
<div className={dialogStyles.dialog}>
|
||||
<div className={dialogStyles.header}>
|
||||
<h2 id="tip-ezpay-redirect-title" className={dialogStyles.title}>
|
||||
Continue to GCash?
|
||||
</h2>
|
||||
<p className={dialogStyles.content}>
|
||||
Your coffee order is ready. Continue to GCash to finish the
|
||||
payment.
|
||||
</p>
|
||||
<p className={dialogStyles.content}>Order No. {orderId}</p>
|
||||
</div>
|
||||
<div className={dialogStyles.actions}>
|
||||
<button
|
||||
type="button"
|
||||
className={`${dialogStyles.button} ${dialogStyles.secondary}`}
|
||||
disabled={isConfirming}
|
||||
onClick={onCancel}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`${dialogStyles.button} ${dialogStyles.primary}`}
|
||||
disabled={isConfirming}
|
||||
onClick={onConfirm}
|
||||
>
|
||||
{isConfirming ? "Opening..." : "Continue"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user