Next.js Discord

Discord Forum

Complex Server Action for onSubmit()

Answered
David L. Bowman posted this in #help-forum
Open in Discord
Avatar
Thank you very much for your help. I'm trying to create a somewhat complex onSubmit, which does three things:
1. Create a Patient via API.
2. Get a HostedPaymentPageRequest token.
3. Redirect the user to a URL with the token.

    async function onSubmit(patientData: Patient) {
        const formattedDateOfBirth = patientData.dateOfBirth
            .split("-")
            .reverse()
            .join("-")
        patientData.dateOfBirth = formattedDateOfBirth
        createTelgraPatient(patientData)

        const transactionRequest = getTransactionRequest({
            transactionType: "authOnlyTransaction",
            amount: amount.toString(),
            customer: { email },
            billTo: { firstName, lastName },
        })
        const hostedPaymentSettings = getHostedPaymentSettings()
        const hostedPaymentPageRequest = await getHostedPaymentPageRequest(
            transactionRequest,
            hostedPaymentSettings,
        )
        redirectToAuthorizeNet(hostedPaymentPageRequest)
    }


For #1, formattedDateOfBirth and createTelegraPatient (which is a async function) both work.

For #2 getTransactionRequest and getHostedPaymentSettings create typescript objects which is passed into an async getHostedPaymentPageRequest() to generate a token. This works.

#3 redirectToAuthorizeNet() is probably broken.

"use server"

export async function redirectToAuthorizeNet(token: string) {
    const response = await fetch("https://test.authorize.net/payment/payment", {
        method: "POST",
        headers: {
            "Content-Type": "application/x-www-form-urlencoded",
        },
        body: `token=${token}`,
    })

    if (response.ok) {
        return Response.redirect(response.url)
    }
    throw new Error("Failed to redirect to Authorize.net")
}

It's kinda stupid, I just don't know how to direct someone to a URL w/ a token.

I'm getting this error: "Internal error: Error: Only plain objects, and a few built-ins, can be passed to Client Components from Server Components. Classes or null prototypes are not supported."

I'd love your help 🙂
Answered by David L. Bowman
async function onSubmit(patientData: Patient) {
        const formattedDateOfBirth = patientData.dateOfBirth
            .split("-")
            .reverse()
            .join("-")
        patientData.dateOfBirth = formattedDateOfBirth
        createTelgraPatient(patientData)

        const transactionRequest = getTransactionRequest({
            transactionType: "authOnlyTransaction",
            amount: amount.toString(),
            customer: { email },
            billTo: { firstName, lastName },
        })

        const hostedPaymentSettings = getHostedPaymentSettings()
        const hostedPaymentPageRequest = await getHostedPaymentPageRequest(
            transactionRequest,
            hostedPaymentSettings,
        )

        const form = document.createElement("form")
        form.method = "POST"
        form.action = "https://test.authorize.net/payment/payment"

        const input = document.createElement("input")
        input.type = "hidden"
        input.name = "token"
        input.value = hostedPaymentPageRequest

        form.appendChild(input)
        document.body.appendChild(form)
        form.submit()
    }
View full answer

116 Replies

Avatar
heya, since you're working with server actions, returning a Response wouldn't work because it's already handled by next

you probably could go by returning a { redirect: string }, where your code will handle the redirect in the client. (perhaps using a useRouter(), then router.replace(resp.redirect))

for setting the token, perhaps you can make use of cookies().set("my-cookie", { ... })?
or you could choose to use a route.ts file to handle the request and response directly; so you'd be able to keep the same code, but you'll be left to parse the request yourself (and the typesafety).
Avatar
we're starting to engage in things i sadly don't understand well 😄
Avatar
oh you're new in next?
Avatar
i'm sadly not, but i just haven't really done this before.
by sadly, i mean, i should know how to do this 😄
Avatar
its okay, i've had a few hiccoughs with it as well 😅
Avatar
let me try the useRouter for a second.
Avatar
well what i could suggest is returning the redirect right away in an object:
export async function myFunction(...): Promise<{ redirectUrl: string }> {
  const response = ...;
  return {
    redirectUrl: response.url
  };
}
Avatar
isn't that what i have?

"use server"

export async function redirectToAuthorizeNet(token: string) {
    const response = await fetch("https://test.authorize.net/payment/payment", {
        method: "POST",
        headers: {
            "Content-Type": "application/x-www-form-urlencoded",
        },
        body: `token=${token}`,
    })

    if (response.ok) {
        return Response.redirect(response.url)
    }
    throw new Error("Failed to redirect to Authorize.net")
}
Avatar
then after the server action had returned, you could do a router.replace or router.push:
const router = useRouter();
const onSubmit = async () => {
  const resp = await myFunction();
  router.replace(resp.redirectUrl);
};
you're returning a Response here:
    if (response.ok) {
        return Response.redirect(response.url)
    }
server actions are made to simplify returning responses so you wouldn't need to care about NextResponse.json() or anything, you just return an object right away: return { redirectUrl: "..." }
under the hood, next will transform the server action and make it return an actual response, and the code on the client will also auto-magically parse the result that came from that server action call
Avatar
Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.
When I add this inside my onSubmit
        const router = useRouter()
        const resp = await redirectToAuthorizeNet(hostedPaymentPageRequest)
        router.replace(resp.redirectUrl)
Avatar
did you use useRouter() on the server action?
Avatar
sure did !
that's a no no... kk
Avatar
well you aren't supposed to do that
but wait this seemed right
perhaps the component is a server component?
you need to add a "use client" directive on the top of the file to make it a client component (and be able to use useRouter())
Avatar
It is a client component.
Avatar
or you could wrap the logic on another client component
is it?
oh waittt
Avatar
    async function onSubmit(patientData: Patient) {
        // const formattedDateOfBirth = patientData.dateOfBirth
        //     .split("-")
        //     .reverse()
        //     .join("-")
        // patientData.dateOfBirth = formattedDateOfBirth
        // createTelgraPatient(patientData)

        const transactionRequest = getTransactionRequest({
            transactionType: "authOnlyTransaction",
            amount: amount.toString(),
            customer: { email },
            billTo: { firstName, lastName },
        })

        const hostedPaymentSettings = getHostedPaymentSettings()
        const hostedPaymentPageRequest = await getHostedPaymentPageRequest(
            transactionRequest,
            hostedPaymentSettings,
        )

        const router = useRouter()
        const resp = await redirectToAuthorizeNet(hostedPaymentPageRequest)
        router.replace(resp.redirectUrl)
    }
yeah, the onSubmit() is a client-component w/ state.
Avatar
you aren't supposed to use useRouter() inside a closure 🤦
it's a regular hook like the others (useEffect, useState, ...)
Avatar
i was importing the wrong useRouter
Avatar
...no, useRouter should be used outside of onSubmit()
Avatar
okay, this now works, but it's not accepting the token, i really appreciate your help ❤️
Avatar
should be placed on the component-level, the same indentation as the other hooks
oh cool 👍
it's not saving the cookies i suppose?
Avatar
so, this is where i'm actually really bad at coding, and i need to learn more.
let me check.
so the way they want me to do it is w/ a form
import { getHostedPaymentPageRequest } from "@/lib/authorizeNet/getHostedPaymentPageRequest"
import getHostedPaymentSettings from "@/lib/authorizeNet/getHostedSettings"
import { getTransactionRequest } from "@/lib/authorizeNet/getTransactionRequest"

export default async function Payment() {
    const transactionRequest = await getTransactionRequest({ amount: 99 })
    const hostedPaymentSettings = await getHostedPaymentSettings()
    const token = await getHostedPaymentPageRequest(
        transactionRequest,
        hostedPaymentSettings,
    )

    return (
        <>
            <p>{token}</p>
            {typeof token === "string" ? (
                <form
                    method="post"
                    action="https://test.authorize.net/payment/payment"
                    id="formAuthorizeNetTestPage"
                    name="formAuthorizeNetTestPage"
                >
                    <input type="hidden" name="token" value={token} />
                    <button type="submit">Continue to Payment</button>
                </form>
            ) : (
                <h1>Hello World</h1>
            )}
        </>
    )
}
that's because authorize.net is trash.
i can't attach to my main form, because i need the first name and last name to generate the token.
so, my hope is there's a way for me to generate this same type of redirect.
here's my redirectToAuthorizeNet()
"use server"

export async function redirectToAuthorizeNet(token: string) {
    const response = await fetch("https://test.authorize.net/payment/payment", {
        method: "POST",
        headers: {
            "Content-Type": "application/x-www-form-urlencoded",
        },
        body: `token=${token}`,
    })

    if (response.ok) {
        return { redirectUrl: response.url }
    }
    throw new Error("Failed to redirect to Authorize.net")
}


Do you know hwo to toss the token into the URL?
Avatar
so this is from their docs? and you're trying to do it on the server instead of what they suggested i suppose?
Avatar
honestly, their docs are rather terrible. The form method did work.
in short, i collect info, generate a token, redirect.
Avatar
bit confused about the flow here
so, after the redirect to authorize.net, it requires a token?
Avatar
no, i'm sorry, the url is supposed to end up being something like /payment/token=...
but, i forget how to put tokens in URLs T_T
the truth is i don't fully understand what it wants. I do know that form method worked.
but, it required clicking an extra button.
Avatar
the form code above is not your code?
did the redirect work in this case? and after the redirection, authorize.net didn't accept the token?
Avatar
Image
Avatar
honestly this flow is rather unfamiliar to me
Avatar
so, maybe my flow is bad. here's what i'm trying to do.

i'm trying to use Authorize.net's hosted solution called "Accept Hosted." I want to do this, because PCI compliance is scary, and I don't want to write a million lines of code.

So, to create an Accept Hosted session, I have to request a session token, which I get by supplying basic user information and settings. When I use the getHostedPaymentPageRequest it generates that toke, which I am then supposed to redirect them to a url w/ that token, and it creates the session for them.
So, I'm trying to redirect the user to this /payment/payment with the token paramater, and somehow it creates a session where they can enter their CC info.
Avatar
ohh so you're trying to redirect the user to https://test.authorize.net/payment/payment?token=....?
Avatar
the sad reality is I can't bring them there directly. I have to bring them to .../payment/payment and include the token as a payload.
similar to how it happens if you POST. i can't say I fully understand how it works.
Avatar
mhm i think i get what you mean, the <form> example above is what you're trying to achieve with server actions?
Avatar
exactly!
Avatar
and the POST to https://test.authorize.net/payment/payment redirects the user?
Avatar
honestly, it doens't even redirect. i think they iframe their own payment session there using the token.
Avatar
from what i could tell, you probably should do the fetch on the client rather than in the server
quite confusing for me lol, makes me question does form redirect as result of a POST to the url of action?
try doing this and check the networks tab on devtools
async function onSubmit(patientData: Patient) {
    // ...

    const response = await fetch("https://test.authorize.net/payment/payment", {
        method: "POST",
        headers: {
            "Content-Type": "application/x-www-form-urlencoded",
        },
        body: `token=${token}`,
    });
}
Avatar
Image
so this gave me a payment token w/ the payload, but I got an error 😄
Image
Avatar
odd, is there any more detailed error?
i suspect there's some cors shenaingans that came into play
Avatar
i go tthis to work
by writing cursed code
Avatar
async function onSubmit(patientData: Patient) {
        const formattedDateOfBirth = patientData.dateOfBirth
            .split("-")
            .reverse()
            .join("-")
        patientData.dateOfBirth = formattedDateOfBirth
        createTelgraPatient(patientData)

        const transactionRequest = getTransactionRequest({
            transactionType: "authOnlyTransaction",
            amount: amount.toString(),
            customer: { email },
            billTo: { firstName, lastName },
        })

        const hostedPaymentSettings = getHostedPaymentSettings()
        const hostedPaymentPageRequest = await getHostedPaymentPageRequest(
            transactionRequest,
            hostedPaymentSettings,
        )

        const form = document.createElement("form")
        form.method = "POST"
        form.action = "https://test.authorize.net/payment/payment"

        const input = document.createElement("input")
        input.type = "hidden"
        input.name = "token"
        input.value = hostedPaymentPageRequest

        form.appendChild(input)
        document.body.appendChild(form)
        form.submit()
    }
Answer
Avatar
oh lol that works as well
Avatar
i can't say i'm proud of this lol
Avatar
was thinking of this as well lol, but perhaps there's a way of doing with regular fetch that looks a bit better
but oh well if it works, it works
Avatar
we could try 😄
it certainly would look cooler
Image
it's so ugly too.
Avatar
is that their page?
Avatar
it's the authorize.net payment page, sure is.
we're still waiting for approval from Stripe b/c it's a regulated business (healthcare)
so, all we have for now is authorize.net
Avatar
oh cool
Avatar
i really apprecaite your help.
Avatar
but can't you use stripe's dev test instead of this weird payment gateway lol?
Avatar
ehhh, i want to do that, but he has a demo for the software next thursday, and i don't know if we'll be aprpoved by then.
and i need to test some webhooks which require knowing if payment happens b/c it's async payments.
it's complicated. it's non-ideal, but i'm trying to build this for his demo.
Avatar
oh so it's for a demo?
Avatar
yeah, i build an entire electronic health record (HIPAA compliant) and connect to a 3rd party medical provider network.
Avatar
i dunno about how things work there on america, but i'd probably just boot up a barebones payment gateway that acts as a fake payment gateway
Avatar
ahhh, i'm also responsible for the end product too. i'm just trying to be helpful so he can also be ready for the demo. the working product isn't due for another 3 weeks.
so... if we get rejected from stripe, we'll use authorize.net 😄
or square or something.
Avatar
mhm
Avatar
now i have this 200 line form T_T
Avatar
sounds like a pain man 💀
Avatar
Image
it's fun though.
Avatar
👍 exploring the unknowns is always fun
Avatar
i'm a freelancer, i charge a lot, and i get to learn new things 😄
thank you btw, i was really lost here.
Avatar
no prob, i was also confused mid way, so sorry bout that 😅
Avatar
two confused minds are better than one ❤️
if I have time, maybe i can find a way to make it look nicer, but i'm like 80% done w/ the demo, so I'll get the full thing working ugly before it's beautified :sobeautiful:
Avatar
👍
hey uh im a wannabe freelancer, sorry but may I DM you for advices? im completely new to working professionally and havent known people that has any experience in that area 😄
Avatar
plz do, i'm secretly good at this.
not the code, but do message me plz.
Avatar
👍 thank you so much