Skip to content

Commit

Permalink
Add loader when processing payment and check sent amount
Browse files Browse the repository at this point in the history
  • Loading branch information
Tymmmy committed Apr 19, 2024
1 parent 7ce0b6d commit 6c57ade
Show file tree
Hide file tree
Showing 6 changed files with 5,645 additions and 4,107 deletions.
18 changes: 18 additions & 0 deletions app/components/loader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { motion } from "framer-motion";

export function Loader() {
return (
<div className="absolute inset-0 flex flex-col items-center justify-center gap-y-10">
<motion.div
className="h-24 w-24 rounded-full border-[16px] border-gray-200 border-t-green-1"
animate={{ rotate: 360 }}
transition={{
repeat: Infinity,
ease: "linear",
duration: 1,
}}
/>
<h2 className="text-2xl uppercase">Processing payment...</h2>
</div>
);
}
2 changes: 1 addition & 1 deletion app/entry.server.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { RemixServer } from "@remix-run/react";
import isbot from "isbot";
import { renderToPipeableStream } from "react-dom/server";

const ABORT_DELAY = 5_000;
const ABORT_DELAY = 10_000;

export default function handleRequest(
request: Request,
Expand Down
75 changes: 67 additions & 8 deletions app/lib/open-payments.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
} from "@interledger/open-payments";
import { createId } from "@paralleldrive/cuid2";
import { randomUUID } from "crypto";
import { formatAmount } from "~/utils/helpers";

async function createClient() {
return await createAuthenticatedClient({
Expand All @@ -30,7 +31,6 @@ export async function fetchQuote(
): Promise<QuoteResponse> {
const opClient = await createClient();
const walletAddress = await getWalletAddress(args.walletAddress, opClient);
// const receiver = await getWalletAddress(args.receiver, opClient);

const amountObj = {
value: BigInt(args.amount * 10 ** walletAddress.assetScale).toString(),
Expand Down Expand Up @@ -226,9 +226,9 @@ export interface Amount {

type CreateOutgoingPaymentParams = {
walletAddress: WalletAddress;
debitAmount: Amount;
receiveAmount: Amount;
nonce: string;
debitAmount?: Amount;
receiveAmount?: Amount;
nonce?: string;
paymentId: string;
opClient: AuthenticatedClient;
};
Expand Down Expand Up @@ -269,7 +269,7 @@ async function createOutgoingPaymentGrant(
finish: {
method: "redirect",
uri: `${process.env.REDIRECT_URL}?paymentId=${paymentId}`,
nonce: nonce,
nonce: nonce || "",
},
},
}
Expand All @@ -293,7 +293,7 @@ export async function finishPayment(
accessToken: string,
receiver: string,
isRequestPayment?: boolean
): Promise<void> {
): Promise<{ url: string; accessToken: string }> {
const opClient = await createClient();

const continuation = await opClient.grant.continue(
Expand All @@ -306,10 +306,12 @@ export async function finishPayment(
}
);

await opClient.outgoingPayment
const url = new URL(walletAddress.id).origin;

const outgoingPayment = await opClient.outgoingPayment
.create(
{
url: new URL(walletAddress.id).origin,
url: url,
accessToken: continuation.access_token.value,
},
{
Expand All @@ -335,6 +337,63 @@ export async function finishPayment(
throw new Error("Could not complete incoming payment.");
});
}

return {
url: outgoingPayment.id,
accessToken: continuation.access_token.value,
};
}

function timeout(delay: number) {
return new Promise((res) => setTimeout(res, delay));
}

export type PaymentResultType = {
message: string;
color: "red" | "green";
error: boolean;
};

export async function checkOutgoingPayment(
url: string,
accessToken: string
): Promise<PaymentResultType> {
const opClient = await createClient();
await timeout(9000);

// get outgoing payment, to check if there was enough balance
const checkOutgoingPaymentResponse = await opClient.outgoingPayment
.get({
url: url,
accessToken: accessToken,
})
.then((op) => {
let paymentResult: PaymentResultType;
if (Number(op.sentAmount.value) >= Number(op.receiveAmount.value)) {
paymentResult = {
message: "Payment successful",
color: "green",
error: false,
};
} else if (Number(op.sentAmount.value) === 0) {
paymentResult = {
message: "Payment failed. Check your balance and try again.",
color: "red",
error: true,
};
} else {
const amountSent = formatAmount(op.sentAmount);
paymentResult = {
message: `Payment failed. Only ${amountSent.amountWithCurrency} was sent. Check your balance and try again.`,
color: "red",
error: true,
};
}

return paymentResult;
});

return checkOutgoingPaymentResponse;
}

export async function getRequestPaymentDetails(
Expand Down
179 changes: 98 additions & 81 deletions app/routes/finish.tsx
Original file line number Diff line number Diff line change
@@ -1,64 +1,80 @@
import { useForm } from "@conform-to/react";
import { type LoaderFunctionArgs, json, redirect } from "@remix-run/node";
import { Form, useActionData, useLoaderData } from "@remix-run/react";
import { type LoaderFunctionArgs, redirect, defer } from "@remix-run/node";
import { Await, Form, useActionData, useLoaderData } from "@remix-run/react";
import { cx } from "class-variance-authority";
import { Suspense } from "react";
import { Header } from "~/components/header";
import { FinishCheck, FinishError } from "~/components/icons";
import { Loader } from "~/components/loader";
import { Button } from "~/components/ui/button";
import { useDialogContext } from "~/lib/context/dialog";
import { finishPayment } from "~/lib/open-payments.server";
import {
type PaymentResultType,
finishPayment,
checkOutgoingPayment,
} from "~/lib/open-payments.server";
import { destroySession, getSession } from "~/session";

export async function loader({ request }: LoaderFunctionArgs) {
const searchParams = new URL(request.url).searchParams;

const paymentId = searchParams.get("paymentId");
const interactRef = searchParams.get("interact_ref");
const result = searchParams.get("result");
const paymentId = searchParams.get("paymentId") || "";
const interactRef = searchParams.get("interact_ref") || "";
const result = searchParams.get("result") || "";

let paymentResult: PaymentResultType = {
message: "",
color: "red",
error: false,
};
let notDefer = false;

if (result === "grant_rejected") {
return json({
isRejected: true,
paymentResult = {
message: "Payment was successfully declined",
color: "red",
error: false,
} as const);
}

if (!paymentId || !interactRef) {
return json({
isRejected: true,
message: "An Error occured",
};
notDefer = true;
} else if (!paymentId || !interactRef) {
paymentResult = {
message: "An Error occured. Please try again.",
color: "red",
error: true,
} as const);
};
notDefer = true;
}

const session = await getSession(request.headers.get("Cookie"));
const quote = session.get("quote");
const grant = session.get("payment-grant");
const walletAddressInfo = session.get("wallet-address");
const isRequestPayment = session.get("isRequestPayment");
if (!notDefer) {
const session = await getSession(request.headers.get("Cookie"));
const quote = session.get("quote");
const grant = session.get("payment-grant");
const walletAddressInfo = session.get("wallet-address");
const isRequestPayment = session.get("isRequestPayment");

if (quote === undefined) {
throw new Error("Payment session expired.");
}
if (quote === undefined) {
throw new Error("Payment session expired.");
}

await finishPayment(
grant,
quote,
walletAddressInfo.walletAddress,
interactRef,
quote.incomingPaymentGrantToken,
quote.receiver,
isRequestPayment
);
const finishPaymentResponse = await finishPayment(
grant,
quote,
walletAddressInfo.walletAddress,
interactRef,
quote.incomingPaymentGrantToken,
quote.receiver,
isRequestPayment
);

return json({
isRejected: false,
message: "Payment successful",
color: "green",
error: false,
} as const);
const checkOutgoingPaymentPromise = checkOutgoingPayment(
finishPaymentResponse.url,
finishPaymentResponse.accessToken
);

return defer({ checkOutgoingPayment: checkOutgoingPaymentPromise });
}

return defer({ checkOutgoingPayment: Promise.resolve(paymentResult) });
}

export default function Finish() {
Expand All @@ -77,48 +93,49 @@ export default function Finish() {
<>
<Header />
<div className="flex justify-center items-center flex-col h-full px-5 gap-8">
{data.error ? (
<>
<FinishError />
<div className="text-destructive uppercase sm:text-2xl font-medium text-center">
{data.message}
</div>
<Form method="POST" {...form.props}>
<div className="flex gap-2">
<Button variant="outline_destructive" size="sm" type="submit">
Try again
</Button>
<Button variant="outline" size="sm" type="submit">
Home
</Button>
</div>
</Form>
</>
) : data.color === "red" ? (
<>
<FinishCheck color="red" />
<div className="text-destructive uppercase sm:text-2xl font-medium text-center">
{data.message}
</div>{" "}
<Form method="POST" {...form.props}>
<Button variant="outline" size="sm" type="submit">
Home
</Button>
</Form>
</>
) : (
<>
<FinishCheck />
<div className="text-green-1 uppercase sm:text-2xl font-medium text-center">
{data.message}
</div>
<Form method="POST" {...form.props}>
<Button variant="outline" size="sm" type="submit">
Home
</Button>
</Form>
</>
)}
<Suspense fallback={<Loader />}>
<Await
resolve={data.checkOutgoingPayment}
errorElement={<FinishError />}
>
{(outgoingPaymentCheck) => (
<>
{outgoingPaymentCheck.error ? (
<>
<FinishError />
<div className="text-destructive uppercase sm:text-2xl font-medium text-center">
{outgoingPaymentCheck.message}
</div>
<Form method="POST" {...form.props}>
<Button variant="outline" size="sm" type="submit">
Home
</Button>
</Form>
</>
) : (
<>
<FinishCheck color={outgoingPaymentCheck.color} />
<div
className={cx(
"uppercase sm:text-2xl font-medium text-center",
outgoingPaymentCheck.color === "red"
? "text-destructive"
: "text-green-1"
)}
>
{outgoingPaymentCheck.message}
</div>
<Form method="POST" {...form.props}>
<Button variant="outline" size="sm" type="submit">
Home
</Button>
</Form>
</>
)}
</>
)}
</Await>
</Suspense>
</div>
</>
);
Expand Down
5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"@remix-run/serve": "^2.8.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"framer-motion": "^11.1.7",
"isbot": "^3.8.0",
"nprogress": "^0.2.0",
"react": "^18.2.0",
Expand All @@ -50,8 +51,6 @@
"typescript": "^5.4.2"
},
"engines": {
"pnpm": "^8.15.4",
"node": ">=20.0.0"
},
"packageManager": "[email protected]"
}
}
Loading

0 comments on commit 6c57ade

Please sign in to comment.