Skip to content

Commit

Permalink
refactor: context + status context
Browse files Browse the repository at this point in the history
  • Loading branch information
dolcalmi committed May 15, 2024
1 parent bb2b46e commit e089029
Show file tree
Hide file tree
Showing 15 changed files with 264 additions and 119 deletions.
8 changes: 0 additions & 8 deletions apps/pay/app/checkout/[hash]/hash.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,6 @@
text-transform: capitalize;
}

.payBtnContainer {
display: flex;
align-items: center;
justify-content: center;
column-gap: 0.5rem;
margin-top: 1.5em;
}

@media (max-width: 768px) {
.paymentContainer {
box-shadow: none;
Expand Down
57 changes: 23 additions & 34 deletions apps/pay/app/checkout/[hash]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,59 +1,48 @@
import { NextPage } from "next"
import { headers } from "next/headers"
import Image from "react-bootstrap/Image"

import styles from "./hash.module.css"

import { fetchInvoiceByHash } from "@/app/graphql/queries/invoice-by-hash"

import Invoice from "@/components/invoice"
import CancelInvoiceButton from "@/components/invoice/cancel-button"
import { decodeInvoice } from "@/components/utils"
import StatusActions from "@/components/invoice/status-actions"
import CheckoutLayoutContainer from "@/components/layouts/checkout-layout"
import PrintButton from "@/components/invoice/print-button"

import { InvoiceStatusProvider } from "@/context/invoice-status-context"

import { baseLogger } from "@/lib/logger"

const CheckoutPage: NextPage<{ params: { hash: string } }> = async (context) => {
const headersList = headers()
const returnUrl = headersList.get("x-return-url")

const { hash } = context.params
const invoice = await fetchInvoiceByHash({ hash })
if (invoice instanceof Error || !invoice.paymentRequest) {
if (invoice instanceof Error || !invoice.paymentRequest || !invoice.status) {
baseLogger.error({ hash }, "Error getting invoice for hash")
return <div>Error getting invoice for hash: {hash}</div>
}

const showPendingActions = invoice && invoice.status === "PENDING"
const showPaidActions = invoice && invoice.status === "PAID"
const decodedInvoice = decodeInvoice(invoice.paymentRequest)
if (!decodedInvoice) {
baseLogger.error({ invoice }, "Error decoding invoice for hash")
return <div>Error decoding invoice for hash: {hash}</div>
}

return (
<CheckoutLayoutContainer>
<div className={styles.paymentContainer}>
<div className={styles.headerContainer}>
<p className={styles.title}>Pay Invoice</p>
</div>
<Invoice
title="Pay Invoice"
status={invoice.status || "PENDING"}
paymentRequest={invoice.paymentRequest}
returnUrl={returnUrl}
/>
{showPaidActions && (
<div className={styles.payBtnContainer}>
<PrintButton />
<CancelInvoiceButton returnUrl={returnUrl} type="primary">
Return to merchant
</CancelInvoiceButton>
</div>
)}
{showPendingActions && (
<div className={styles.payBtnContainer}>
<CancelInvoiceButton returnUrl={returnUrl} type="secondary">
<Image src="/icons/close.svg" alt="Back" width="20" height="20"></Image>
Cancel
</CancelInvoiceButton>
<InvoiceStatusProvider invoice={decodedInvoice} initialStatus={invoice.status}>
<CheckoutLayoutContainer>
<div className={styles.paymentContainer}>
<div className={styles.headerContainer}>
<p className={styles.title}>Pay Invoice</p>
</div>
)}
</div>
</CheckoutLayoutContainer>
<Invoice title="Pay Invoice" returnUrl={returnUrl} />
<StatusActions returnUrl={returnUrl} />
</div>
</CheckoutLayoutContainer>
</InvoiceStatusProvider>
)
}

Expand Down
50 changes: 50 additions & 0 deletions apps/pay/components/invoice/expiration-label.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"use client"

import { useEffect, useState } from "react"

import styles from "./index.module.css"
import { type ExpirationLabelProps } from "./index.types"

function ExpirationLabel({ expirationDate }: ExpirationLabelProps) {
const [seconds, setSeconds] = useState(0)

const setRemainingSeconds = () => {
const currentTime = new Date()
const expirationTime = new Date(expirationDate * 1000)
const elapsedTime = expirationTime.getTime() - currentTime.getTime()
let remainingSeconds = Math.ceil(elapsedTime / 1000)
if (remainingSeconds <= 0) {
remainingSeconds = 0
}
setSeconds(remainingSeconds)
}

useEffect(() => {
const interval = setInterval(() => setRemainingSeconds(), 1000)

return () => clearInterval(interval)
})

return (
<span className={styles.expirationLabel}>{formatInvoiceExpirationTime(seconds)}</span>
)
}
export default ExpirationLabel

const formatInvoiceExpirationTime = (seconds: number): string => {
if (seconds <= 0) {
return "Expired"
}

if (seconds >= 3600) {
const hours = Math.floor(seconds / 3600)
return `Expires in ~${hours} Hour${hours > 1 ? "s" : ""}`
}

if (seconds >= 60) {
const minutes = Math.floor(seconds / 60)
return `Expires in ~${minutes} Minute${minutes > 1 ? "s" : ""}`
}

return `Expires in ${seconds} Second${seconds > 1 ? "s" : ""}`
}
8 changes: 8 additions & 0 deletions apps/pay/components/invoice/index.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,14 @@
margin-right: 10px; /* Adjust as needed */
}

.statusActionsContainer {
display: flex;
align-items: center;
justify-content: center;
column-gap: 0.5rem;
margin-top: 1.5em;
}

.primaryBtn {
background: var(--primary3);
color: #fff;
Expand Down
84 changes: 18 additions & 66 deletions apps/pay/components/invoice/index.tsx
Original file line number Diff line number Diff line change
@@ -1,63 +1,47 @@
"use client"

import React, { useState, useEffect, useRef } from "react"
import copy from "copy-to-clipboard"
import Image from "react-bootstrap/Image"
import { QRCode } from "react-qrcode-logo"
import Tooltip from "react-bootstrap/Tooltip"
import { useScreenshot } from "use-react-screenshot"
import React, { useState, useEffect, useRef } from "react"
import OverlayTrigger from "react-bootstrap/OverlayTrigger"

import Receipt from "./receipt"
import styles from "./index.module.css"

import ExpirationLabel from "./expiration-label"
import { type InvoiceProps } from "./index.types"

import Receipt from "./receipt"

import { useInvoiceStatusContext } from "@/context/invoice-status-context"
import { Share } from "@/components/share"
import { decodeInvoice } from "@/components/utils"

export default function Invoice({ title, paymentRequest, status }: InvoiceProps) {
const [seconds, setSeconds] = useState(0)
export default function Invoice({ title }: InvoiceProps) {
const { invoice, status } = useInvoiceStatusContext()
const [copied, setCopied] = useState(false)
const [shareState, setShareState] = useState<"not-set">()
const [errorMessage, setErrorMessage] = useState<string | undefined>()
const [image, takeScreenShot] = useScreenshot()
const qrImageRef = useRef(null)

const invoice = decodeInvoice(paymentRequest)
const memo =
invoice?.tags?.find((t) => t.tagName === "description")?.data?.toString() || ""

if (!invoice || !invoice.satoshis) {
setErrorMessage("Invalid invoice.")
}

useEffect(() => {
if (!invoice || !invoice.timeExpireDate || status === "PAID") {
return
}

if (status === "EXPIRED") {
setErrorMessage("Invoice has expired.")
return
}
}, [status])

const timerStartTime = new Date(invoice.timeExpireDate * 1000)
const interval = setInterval(() => {
const currentTime = new Date()
const elapsedTime = timerStartTime.getTime() - currentTime.getTime()
let remainingSeconds = Math.ceil(elapsedTime / 1000)
if (remainingSeconds <= 0) {
remainingSeconds = 0
setErrorMessage("Invoice has expired.")
clearInterval(interval)
}
setSeconds(remainingSeconds)
}, 1000)

return () => clearInterval(interval)
}, [status, invoice])
if (errorMessage || !invoice || !invoice.satoshis) {
return (
<div className={styles.error}>
<Image src="/icons/cancel-icon.svg" alt="success icon" width="104" height="104" />
<p>{errorMessage || "Invalid Invoice"}</p>
</div>
)
}

const copyInvoice = () => {
if (!invoice?.paymentRequest) {
Expand All @@ -70,34 +54,22 @@ export default function Invoice({ title, paymentRequest, status }: InvoiceProps)
}, 3000)
}

const handleShareClick = () => setShareState("not-set")
const getImage = () => takeScreenShot(qrImageRef.current)
const shareData = {
title,
text: `Use the link embedded below to pay the invoice. Powered by: https://galoy.io`,
url: typeof window !== "undefined" ? window.location.href : "",
}

const handleShareClick = () => {
setShareState("not-set")
}

if (errorMessage || !invoice || !invoice.satoshis) {
return (
<div className={styles.error}>
<Image src="/icons/cancel-icon.svg" alt="success icon" width="104" height="104" />
<p>{errorMessage || "Invalid Invoice"}</p>
</div>
)
}

return (
<div>
<div className={styles.invoiceContainer}>
<div className={styles.amountContainer}>
<p>{invoice.satoshis} sats</p>
</div>
<div className={styles.timerContainer}>
<p>{memo}</p>
<p>{invoice.memo}</p>
</div>
<div>
{status === "PAID" && (
Expand Down Expand Up @@ -154,9 +126,7 @@ export default function Invoice({ title, paymentRequest, status }: InvoiceProps)
</button>
)}
</OverlayTrigger>
<span className={styles.expirationLabel}>
{formatInvoiceExpirationTime(seconds)}
</span>
<ExpirationLabel expirationDate={invoice.timeExpireDate || 0} />
<Share
shareData={shareData}
getImage={getImage}
Expand All @@ -181,21 +151,3 @@ export default function Invoice({ title, paymentRequest, status }: InvoiceProps)
</div>
)
}

const formatInvoiceExpirationTime = (seconds: number): string => {
if (seconds <= 0) {
return "Expired"
}

if (seconds >= 3600) {
const hours = Math.floor(seconds / 3600)
return `Expires in ~${hours} Hour${hours > 1 ? "s" : ""}`
}

if (seconds >= 60) {
const minutes = Math.floor(seconds / 60)
return `Expires in ~${minutes} Minute${minutes > 1 ? "s" : ""}`
}

return `Expires in ${seconds} Second${seconds > 1 ? "s" : ""}`
}
12 changes: 9 additions & 3 deletions apps/pay/components/invoice/index.types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,23 @@ import { type Invoice } from "../utils"

export type InvoiceProps = {
title: string
paymentRequest: string
status: string
returnUrl: string | null
}

export type CancelInvoiceButtonProps = {
export type ReturnInvoiceButtonProps = {
returnUrl: string | null
type: "primary" | "secondary"
children: React.ReactNode
}

export type ExpirationLabelProps = {
expirationDate: number
}

export type StatusActionsProps = {
returnUrl: string | null
}

export type ReceiptProps = {
invoice: Invoice
amount?: number | undefined
Expand Down
1 change: 1 addition & 0 deletions apps/pay/components/invoice/print-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ function PrintButton() {
</button>
)
}

export default PrintButton
2 changes: 1 addition & 1 deletion apps/pay/components/invoice/receipt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import styles from "./index.module.css"
import { type ReceiptProps } from "./index.types"

import GaloyIcon from "@/components/galoy-icon"
import { formattedDate, formattedTime } from "@/utils/date-util"

import { getLocaleConfig } from "@/utils/utils"
import { formattedDate, formattedTime } from "@/utils/date-util"

function Receipt({ amount, currency, invoice, status }: ReceiptProps) {
if (!invoice) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
import { useRouter } from "next/navigation"

import styles from "./index.module.css"
import { type CancelInvoiceButtonProps } from "./index.types"
import { type ReturnInvoiceButtonProps } from "./index.types"

function CancelInvoiceButton({ returnUrl, type, children }: CancelInvoiceButtonProps) {
function ReturnInvoiceButton({ returnUrl, type, children }: ReturnInvoiceButtonProps) {
const router = useRouter()

const cancelHandler = () => {
Expand All @@ -24,4 +24,4 @@ function CancelInvoiceButton({ returnUrl, type, children }: CancelInvoiceButtonP
</button>
)
}
export default CancelInvoiceButton
export default ReturnInvoiceButton
Loading

0 comments on commit e089029

Please sign in to comment.