Skip to content
This repository was archived by the owner on Dec 17, 2025. It is now read-only.
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 15 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
# Authorize.net App

## Setup

- The `example` Checkout UI relies on the "Authorize transactions instead of charging" setting in the Saleor Dashboard -> Configuration -> Channels -> [channel] settings.

- The Saleor transaction id is stored in Authorize transaction `order.description`. Ideally, we would store it in `refId` but that field is max. 20 characters long and the Saleor transaction id is longer than that.

### Connecting sandbox PayPal to sandbox Authorize.net

1. Create [PayPal Developer Account](https://developer.paypal.com). No need to hook up a credit card.
2. While logged in, open [Sandbox test accounts](https://developer.paypal.com/dashboard/accounts).
3. Choose "personal" (I thought it's going to be "business", but it didn't work 🤷).
4. Log in to your Authorize.net sandbox account.
5. Go to _Account_ -> _Digital Payment Solutions_ -> _PayPal_ -> _Sign up_.
6. Fill in the form with the _personal_ PayPal sandbox account credentials.

## Development

### Build
Expand Down Expand Up @@ -27,9 +42,3 @@ pnpm run dev

> [!IMPORTANT]
> Both the example and the app need to be [tunneled](https://docs.saleor.io/docs/3.x/developer/extending/apps/developing-with-tunnels).

## Important

- The `example` Checkout UI relies on the "Authorize transactions instead of charging" setting in the Saleor Dashboard -> Configuration -> Channels -> [channel] settings.

- The Saleor transaction id is stored in Authorize transaction `order.description`. Ideally, we would store it in `refId` but that field is max. 20 characters long and the Saleor transaction id is longer than that.
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
Loading