Skip to content

Commit

Permalink
add paypal
Browse files Browse the repository at this point in the history
  • Loading branch information
peelar committed Jan 26, 2024
1 parent 725adaf commit c0568fd
Show file tree
Hide file tree
Showing 29 changed files with 556 additions and 247 deletions.
23 changes: 16 additions & 7 deletions example/src/accept-hosted-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ import {
TransactionProcessMutationVariables,
} from "../generated/graphql";
import { authorizeNetAppId } from "./lib/common";
import { getCheckoutId } from "./pages/cart";

import { useRouter } from "next/router";
import { checkoutIdUtils } from "./lib/checkoutIdUtils";

const acceptHostedTransactionResponseSchema = z.object({
transId: z.string(),
Expand All @@ -21,14 +22,16 @@ const acceptHostedTransactionResponseSchema = z.object({
const authorizeEnvironmentSchema = z.enum(["sandbox", "production"]);

const acceptHostedTransactionInitializeResponseDataSchema = z.object({
formToken: z.string().min(1),
environment: authorizeEnvironmentSchema,
type: z.literal("acceptHosted"),
data: z.object({
formToken: z.string().min(1),
environment: authorizeEnvironmentSchema,
}),
});

type AcceptHostedData = z.infer<typeof acceptHostedTransactionInitializeResponseDataSchema>;
type AcceptHostedData = z.infer<typeof acceptHostedTransactionInitializeResponseDataSchema>["data"];

export function AcceptHostedForm() {
const checkoutId = getCheckoutId();
const router = useRouter();
const [acceptData, setAcceptData] = React.useState<AcceptHostedData>();
const [transactionId, setTransactionId] = React.useState<string>();
Expand All @@ -43,6 +46,12 @@ export function AcceptHostedForm() {
);

const getAcceptData = React.useCallback(async () => {
const checkoutId = checkoutIdUtils.get();

if (!checkoutId) {
throw new Error("Checkout id not found");
}

const initializeTransactionResponse = await initializeTransaction({
variables: {
checkoutId,
Expand Down Expand Up @@ -76,9 +85,9 @@ export function AcceptHostedForm() {

console.log(data);

const nextAcceptData = acceptHostedTransactionInitializeResponseDataSchema.parse(data);
const { data: nextAcceptData } = acceptHostedTransactionInitializeResponseDataSchema.parse(data);
setAcceptData(nextAcceptData);
}, [initializeTransaction, checkoutId]);
}, [initializeTransaction]);

React.useEffect(() => {
getAcceptData();
Expand Down
25 changes: 25 additions & 0 deletions example/src/lib/checkoutIdUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from "react";

export const checkoutIdUtils = {
set: (id: string) => localStorage.setItem("checkoutId", id),
get: () => {
const checkoutId = localStorage.getItem("checkoutId");

if (!checkoutId) {
throw new Error("Checkout ID not found");
}

return checkoutId;
},
};

export const useGetCheckoutId = () => {
const [checkoutId, setCheckoutId] = React.useState<string | null>(null);

React.useEffect(() => {
const checkoutId = checkoutIdUtils.get();
setCheckoutId(checkoutId);
}, []);

return checkoutId;
};
66 changes: 66 additions & 0 deletions example/src/pages/[transactionId]/paypal/continue/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { useMutation } from "@apollo/client";
import gql from "graphql-tag";
import React from "react";
import {
TransactionProcessMutation,
TransactionProcessMutationVariables,
TransactionProcessDocument,
} from "../../../../../generated/graphql";
import { useRouter } from "next/router";

type Status = "idle" | "loading" | "success";

const PaypalContinuePage = () => {
const [processTransaction] = useMutation<TransactionProcessMutation, TransactionProcessMutationVariables>(
gql(TransactionProcessDocument.toString()),
);
const router = useRouter();
const isCalled = React.useRef(false);
const [status, setStatus] = React.useState<Status>("idle");

const continueTransaction = React.useCallback(
async ({ payerId, transactionId }: { payerId: string; transactionId: string }) => {
setStatus("loading");
const response = await processTransaction({
variables: {
transactionId,
data: {
type: "paypal",
data: {
payerId,
},
},
},
});

isCalled.current = true;

if (response.data?.transactionProcess?.transactionEvent?.type !== "AUTHORIZATION_SUCCESS") {
throw new Error("Transaction failed");
}

setStatus("success");
},
[processTransaction],
);

React.useEffect(() => {
const payerId = router.query.PayerID?.toString();
const rawTransactionId = router.query.transactionId?.toString();
setStatus("idle");

if (payerId && rawTransactionId && !isCalled.current) {
const transactionId = atob(rawTransactionId);
continueTransaction({ payerId, transactionId });
}
}, [continueTransaction, router.query.PayerID, router.query.transactionId]);

return (
<div>
{status === "loading" && <div>Processing transaction...</div>}
{status === "success" && <div>You successfully paid with PayPal 🎺</div>}
</div>
);
};

export default PaypalContinuePage;
16 changes: 3 additions & 13 deletions example/src/pages/cart/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,17 @@ import {
GetCheckoutByIdQuery,
GetCheckoutByIdQueryVariables,
} from "../../../generated/graphql";
import { useGetCheckoutId } from "../../lib/checkoutIdUtils";
import { authorizeNetAppId } from "../../lib/common";
import React from "react";
import { PaymentMethods } from "../../payment-methods";

export function getCheckoutId() {
const checkoutId = typeof sessionStorage === "undefined" ? undefined : sessionStorage.getItem("checkoutId");

if (!checkoutId) {
throw new Error("Checkout ID not found in sessionStorage");
}

return checkoutId;
}

export default function CartPage() {
const checkoutId = getCheckoutId();
const checkoutId = useGetCheckoutId();

const { data: checkoutResponse, loading: checkoutLoading } = useQuery<
GetCheckoutByIdQuery,
GetCheckoutByIdQueryVariables
>(gql(GetCheckoutByIdDocument.toString()), { variables: { id: checkoutId } });
>(gql(GetCheckoutByIdDocument.toString()), { variables: { id: checkoutId ?? "" }, skip: !checkoutId });

const isAuthorizeAppInstalled = checkoutResponse?.checkout?.availablePaymentGateways.some(
(gateway) => gateway.id === authorizeNetAppId,
Expand Down
11 changes: 11 additions & 0 deletions example/src/pages/failure/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import React from "react";

const FailurePage = () => {
return (
<div>
<h1>Something went wrong</h1>
</div>
);
};

export default FailurePage;
3 changes: 2 additions & 1 deletion example/src/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
UpdateDeliveryMutationVariables,
UpdateDeliveryDocument,
} from "../../generated/graphql";
import { checkoutIdUtils } from "../lib/checkoutIdUtils";

export default function Page() {
const { data, loading } = useQuery<ProductListQuery, ProductListQueryVariables>(
Expand Down Expand Up @@ -53,7 +54,7 @@ export default function Page() {

await updateDelivery({ variables: { checkoutId: response.data.checkoutCreate.checkout.id, methodId } });

sessionStorage.setItem("checkoutId", response.data.checkoutCreate.checkout.id);
checkoutIdUtils.set(response.data.checkoutCreate.checkout.id);
return router.push("/cart");
};

Expand Down
10 changes: 7 additions & 3 deletions example/src/pages/success/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,24 @@ import { useMutation } from "@apollo/client";
import gql from "graphql-tag";
import React from "react";
import {
CheckoutCompleteDocument,
CheckoutCompleteMutation,
CheckoutCompleteMutationVariables,
CheckoutCompleteDocument,
} from "../../../generated/graphql";
import { getCheckoutId } from "../cart";
import { useGetCheckoutId } from "../../lib/checkoutIdUtils";

const SuccessPage = () => {
const checkoutId = getCheckoutId();
const checkoutId = useGetCheckoutId();
const [isCompleted, setIsCompleted] = React.useState(false);
const [completeCheckout] = useMutation<CheckoutCompleteMutation, CheckoutCompleteMutationVariables>(
gql(CheckoutCompleteDocument.toString()),
);

const checkoutCompleteHandler = async () => {
if (!checkoutId) {
throw new Error("Checkout id not found");
}

const response = await completeCheckout({
variables: {
checkoutId,
Expand Down
15 changes: 10 additions & 5 deletions example/src/payment-methods.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import {
import { authorizeNetAppId } from "./lib/common";

import { AcceptHostedForm } from "./accept-hosted-form";
import { getCheckoutId } from "./pages/cart";
import { checkoutIdUtils } from "./lib/checkoutIdUtils";
import { PayPalForm } from "./paypal-form";

const acceptHostedPaymentGatewaySchema = z.object({});

Expand All @@ -30,15 +31,19 @@ export const PaymentMethods = () => {
const [isLoading, setIsLoading] = React.useState(false);
const [paymentMethods, setPaymentMethods] = React.useState<PaymentMethods>();

const checkoutId = getCheckoutId();

const [initializePaymentGateways] = useMutation<
PaymentGatewayInitializeMutation,
PaymentGatewayInitializeMutationVariables
>(gql(PaymentGatewayInitializeDocument.toString()));

const getPaymentGateways = React.useCallback(async () => {
setIsLoading(true);
const checkoutId = checkoutIdUtils.get();

if (!checkoutId) {
throw new Error("Checkout id not found");
}

const response = await initializePaymentGateways({
variables: {
appId: authorizeNetAppId,
Expand All @@ -64,7 +69,7 @@ export const PaymentMethods = () => {
}

setPaymentMethods(data);
}, [initializePaymentGateways, checkoutId]);
}, [initializePaymentGateways]);

React.useEffect(() => {
getPaymentGateways();
Expand All @@ -87,7 +92,7 @@ export const PaymentMethods = () => {
)}
{paymentMethods?.paypal !== undefined && (
<li>
<button>PayPal</button>
<PayPalForm />
</li>
)}
</ul>
Expand Down
100 changes: 100 additions & 0 deletions example/src/paypal-form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { useMutation } from "@apollo/client";
import gql from "graphql-tag";
import Script from "next/script";
import React from "react";
import { z } from "zod";
import {
TransactionInitializeDocument,
TransactionInitializeMutation,
TransactionInitializeMutationVariables,
} from "../generated/graphql";
import { checkoutIdUtils } from "./lib/checkoutIdUtils";
import { authorizeNetAppId } from "./lib/common";

declare global {
interface Window {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
paypal: any;
}
}

const paypalTransactionResponseDataSchema = z.object({
secureAcceptanceUrl: z.string().min(1).optional(),
error: z
.object({
message: z.string().min(1),
})
.optional(),
});

/**
* This form uses PayPal's legacy Express Checkout integration
* https://developer.paypal.com/docs/archive/express-checkout/in-context/javascript-advanced-settings/ */

export const PayPalForm = () => {
const [isLoading, setIsLoading] = React.useState(false);
const [initializeTransaction] = useMutation<
TransactionInitializeMutation,
TransactionInitializeMutationVariables
>(gql(TransactionInitializeDocument.toString()));

function onLoad() {
if (typeof window !== "undefined" && window.paypal) {
window.paypal.checkout.setup(" ", {
environment: "sandbox",
container: "paypalButton",
click: () => {
getPayPalAcceptanceUrl();
},
});
}
}

const getPayPalAcceptanceUrl = async () => {
setIsLoading(true);
const checkoutId = checkoutIdUtils.get();

const initializeTransactionResponse = await initializeTransaction({
variables: {
checkoutId,
paymentGateway: authorizeNetAppId,
data: {
type: "paypal",
data: {},
},
},
});

if (initializeTransactionResponse.data?.transactionInitialize?.errors?.length) {
throw new Error("Failed to initialize transaction");
}

const data = initializeTransactionResponse.data?.transactionInitialize?.data;

if (!data) {
throw new Error("Data not found on transaction initialize response");
}

const { secureAcceptanceUrl, error } = paypalTransactionResponseDataSchema.parse(data);

if (error) {
throw new Error(error.message);
}

if (!secureAcceptanceUrl) {
throw new Error("Secure acceptance url not found");
}

setIsLoading(false);
window.open(secureAcceptanceUrl, "_self");
};

return (
<>
{/* We need to load this before we execute any code */}
<Script src="https://www.paypalobjects.com/api/checkout.js" onLoad={onLoad} />

{isLoading ? <p>Loading...</p> : <button id="paypalButton"></button>}
</>
);
};
Loading

0 comments on commit c0568fd

Please sign in to comment.