feat(tip): refine single-screen payment layout
This commit is contained in:
+98
-111
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, type CSSProperties } from "react";
|
||||
import { Sparkles } from "lucide-react";
|
||||
|
||||
import { BackButton, CharacterAvatar } from "@/app/_components";
|
||||
import { MobileShell } from "@/app/_components/core";
|
||||
@@ -25,10 +24,7 @@ import { useUserState } from "@/stores/user/user-context";
|
||||
import { TipCheckoutButton } from "./tip-checkout-button";
|
||||
import { TipGiftProductSelector } from "./tip-gift-product-selector";
|
||||
import { TipProductImage } from "./tip-product-image";
|
||||
import {
|
||||
formatGiftPrice,
|
||||
getGiftImageSources,
|
||||
} from "./tip-screen.helpers";
|
||||
import { getGiftImageSources } from "./tip-screen.helpers";
|
||||
import { TipSuccessView } from "./tip-success-view";
|
||||
import { useTipSupportPrompt } from "./use-tip-support-prompt";
|
||||
import styles from "./tip-screen.module.css";
|
||||
@@ -214,8 +210,6 @@ export function TipScreen({
|
||||
}
|
||||
>
|
||||
<div className={styles.bgImage} aria-hidden="true" />
|
||||
<div className={styles.bgGlowOne} aria-hidden="true" />
|
||||
<div className={styles.bgGlowTwo} aria-hidden="true" />
|
||||
|
||||
<header className={styles.header}>
|
||||
<BackButton
|
||||
@@ -224,118 +218,111 @@ export function TipScreen({
|
||||
ariaLabel="Back to character home"
|
||||
analyticsKey="tip.back_to_splash"
|
||||
/>
|
||||
<span className={styles.headerPill}>{character.copy.tipHeader}</span>
|
||||
</header>
|
||||
|
||||
<section className={styles.hero} aria-labelledby="tip-title">
|
||||
<div className={styles.avatarRing}>
|
||||
<CharacterAvatar
|
||||
src={character.assets.avatar}
|
||||
alt={character.displayName}
|
||||
size="100%"
|
||||
imageSize={88}
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
<p className={styles.eyebrow}>A little sweetness for today</p>
|
||||
<h1 id="tip-title" className={styles.title}>
|
||||
{character.copy.tipTitle}
|
||||
</h1>
|
||||
<p
|
||||
className={`${styles.subtitle} ${
|
||||
supportPrompt.isReady ? styles.subtitleReady : ""
|
||||
}`}
|
||||
>
|
||||
{supportPrompt.prompt}
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{payment.isLoadingPlans ? (
|
||||
<section
|
||||
className={`${styles.productCard} ${styles.productSkeleton}`}
|
||||
aria-label="Loading gifts"
|
||||
aria-busy="true"
|
||||
>
|
||||
<div className={styles.skeletonImage} />
|
||||
<div className={styles.skeletonCopy}>
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
</div>
|
||||
</section>
|
||||
) : selectedProduct ? (
|
||||
<section className={styles.productCard} aria-label="Gift products">
|
||||
<div className={styles.coffeeStage}>
|
||||
<TipProductImage
|
||||
key={selectedProduct.planId}
|
||||
sources={getGiftImageSources(selectedProduct, selectedCategory)}
|
||||
alt={selectedProduct.planName}
|
||||
className={styles.coffeeImage}
|
||||
<div className={styles.headerIdentity}>
|
||||
<div className={styles.headerAvatar}>
|
||||
<CharacterAvatar
|
||||
src={character.assets.avatar}
|
||||
alt={character.displayName}
|
||||
size="100%"
|
||||
imageSize={44}
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
<h1 id="tip-title" className={styles.headerTitle}>
|
||||
{character.copy.tipTitle}
|
||||
</h1>
|
||||
</div>
|
||||
<span className={styles.headerBalance} aria-hidden="true" />
|
||||
</header>
|
||||
|
||||
<div className={styles.productCopy}>
|
||||
<span className={styles.productBadge}>
|
||||
<Sparkles size={14} aria-hidden="true" />
|
||||
{selectedCategory?.name ?? "Gift"}
|
||||
</span>
|
||||
<h2 className={styles.productName}>{selectedProduct.planName}</h2>
|
||||
<p className={styles.productPrice}>
|
||||
{formatGiftPrice(
|
||||
selectedProduct.amountCents,
|
||||
selectedProduct.currency,
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<p
|
||||
className={`${styles.supportPrompt} ${
|
||||
supportPrompt.isReady ? styles.supportPromptReady : ""
|
||||
}`}
|
||||
>
|
||||
{supportPrompt.prompt}
|
||||
</p>
|
||||
|
||||
<TipGiftProductSelector
|
||||
disabled={isSelectionDisabled}
|
||||
products={visibleProducts}
|
||||
onChange={handleProductChange}
|
||||
selectedPlanId={payment.selectedPlanId}
|
||||
/>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{showCatalogError || showEmptyCatalog ? (
|
||||
<section className={styles.catalogStatus} role="status">
|
||||
<p>
|
||||
{showCatalogError
|
||||
? "We could not load gifts for this character."
|
||||
: "This character does not have any gifts available yet."}
|
||||
</p>
|
||||
{showCatalogError ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
paymentDispatch({ type: "PaymentCatalogRetryRequested" })
|
||||
}
|
||||
<div className={styles.purchaseFlow}>
|
||||
{payment.isLoadingPlans ? (
|
||||
<section
|
||||
className={`${styles.productCard} ${styles.productSkeleton}`}
|
||||
aria-label="Loading gifts"
|
||||
aria-busy="true"
|
||||
>
|
||||
<div className={styles.skeletonImage} />
|
||||
<div className={styles.skeletonList}>
|
||||
<span />
|
||||
<span />
|
||||
<span />
|
||||
</div>
|
||||
</section>
|
||||
) : selectedProduct ? (
|
||||
<>
|
||||
<section
|
||||
className={styles.productCard}
|
||||
aria-label="Gift products"
|
||||
>
|
||||
Try again
|
||||
</button>
|
||||
) : null}
|
||||
</section>
|
||||
) : null}
|
||||
<div className={styles.coffeeStage}>
|
||||
<TipProductImage
|
||||
key={selectedProduct.planId}
|
||||
sources={getGiftImageSources(
|
||||
selectedProduct,
|
||||
selectedCategory,
|
||||
)}
|
||||
alt={selectedProduct.planName}
|
||||
className={styles.coffeeImage}
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
|
||||
<PaymentMethodSelector
|
||||
config={paymentMethodConfig}
|
||||
value={payment.payChannel}
|
||||
disabled={isPaymentBusy || !selectedProduct}
|
||||
caption="GCash by default in the Philippines"
|
||||
className={styles.paymentMethodSlot}
|
||||
analyticsKey="tip.payment_method"
|
||||
onChange={handlePaymentMethodChange}
|
||||
/>
|
||||
<TipGiftProductSelector
|
||||
disabled={isSelectionDisabled}
|
||||
products={visibleProducts}
|
||||
onChange={handleProductChange}
|
||||
selectedPlanId={payment.selectedPlanId}
|
||||
/>
|
||||
</section>
|
||||
|
||||
<div className={styles.checkoutSlot}>
|
||||
<TipCheckoutButton
|
||||
giftCategory={payment.selectedGiftCategory}
|
||||
giftPlanId={payment.selectedPlanId || null}
|
||||
disabled={!canCreateOrder}
|
||||
onOrder={handleOrder}
|
||||
returnPath={returnPath}
|
||||
/>
|
||||
<PaymentMethodSelector
|
||||
config={paymentMethodConfig}
|
||||
value={payment.payChannel}
|
||||
density="compact"
|
||||
disabled={isPaymentBusy}
|
||||
className={styles.paymentMethodSlot}
|
||||
analyticsKey="tip.payment_method"
|
||||
onChange={handlePaymentMethodChange}
|
||||
/>
|
||||
|
||||
<div className={styles.checkoutSlot}>
|
||||
<TipCheckoutButton
|
||||
giftCategory={payment.selectedGiftCategory}
|
||||
giftPlanId={payment.selectedPlanId || null}
|
||||
disabled={!canCreateOrder}
|
||||
onOrder={handleOrder}
|
||||
returnPath={returnPath}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : showCatalogError || showEmptyCatalog ? (
|
||||
<section className={styles.catalogStatus} role="status">
|
||||
<p>
|
||||
{showCatalogError
|
||||
? "We could not load gifts for this character."
|
||||
: "This character does not have any gifts available yet."}
|
||||
</p>
|
||||
{showCatalogError ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
paymentDispatch({ type: "PaymentCatalogRetryRequested" })
|
||||
}
|
||||
>
|
||||
Try again
|
||||
</button>
|
||||
) : null}
|
||||
</section>
|
||||
) : null}
|
||||
</div>
|
||||
</main>
|
||||
</MobileShell>
|
||||
|
||||
Reference in New Issue
Block a user