Complex Server Action for onSubmit()
Answered
David L. Bowman posted this in #help-forum
Thank you very much for your help. I'm trying to create a somewhat complex onSubmit, which does three things:
1. Create a
2. Get a
3. Redirect the user to a URL with the token.
For #1,
For #2
#3
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 🙂
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()
}
116 Replies
heya, since you're working with server actions, returning a
you probably could go by returning a
for setting the token, perhaps you can make use of
Response
wouldn't work because it's already handled by nextyou 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).we're starting to engage in things i sadly don't understand well 😄
oh you're new in next?
i'm sadly not, but i just haven't really done this before.
by sadly, i mean, i should know how to do this 😄
its okay, i've had a few hiccoughs with it as well 😅
let me try the useRouter for a second.
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
};
}
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")
}
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
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)
did you use
useRouter()
on the server action?sure did !
that's a no no... kk
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()
)It is a client component.
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.you aren't supposed to use
useRouter()
inside a closure 🤦it's a regular hook like the others (
useEffect
, useState
, ...)i was importing the wrong
useRouter
...no,
useRouter
should be used outside of onSubmit()
okay, this now works, but it's not accepting the token, i really appreciate your help ❤️
should be placed on the component-level, the same indentation as the other hooks
oh cool 👍
it's not saving the cookies i suppose?
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
Do you know hwo to toss the token into the URL?
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?
so this is from their docs? and you're trying to do it on the server instead of what they suggested i suppose?
honestly, their docs are rather terrible. The form method did work.
in short, i collect info, generate a token, redirect.
bit confused about the flow here
so, after the redirect to
authorize.net
, it requires a token?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.
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?https://developer.authorize.net/api/reference/features/accept-hosted.html
I kinda made it up after reading this.
I kinda made it up after reading this.
honestly this flow is rather unfamiliar to me
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
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.
ohh so you're trying to redirect the user to
https://test.authorize.net/payment/payment?token=....
?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.
mhm i think i get what you mean, the
<form>
example above is what you're trying to achieve with server actions?exactly!
and the POST to
https://test.authorize.net/payment/payment
redirects the user?honestly, it doens't even redirect. i think they iframe their own payment session there using the token.
from what i could tell, you probably should do the
fetch
on the client rather than in the serverquite 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}`,
});
}
so this gave me a payment token w/ the payload, but I got an error 😄
odd, is there any more detailed error?
i suspect there's some cors shenaingans that came into play
i go tthis to work
by writing cursed code
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
oh lol that works as well
i can't say i'm proud of this lol
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
is that their page?
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
oh cool
i really apprecaite your help.
but can't you use stripe's dev test instead of this weird payment gateway lol?
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.
oh so it's for a demo?
yeah, i build an entire electronic health record (HIPAA compliant) and connect to a 3rd party medical provider network.
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
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.
mhm
now i have this 200 line form T_T
sounds like a pain man 💀
it's fun though.
👍 exploring the unknowns is always fun
i'm a freelancer, i charge a lot, and i get to learn new things 😄
thank you btw, i was really lost here.
no prob, i was also confused mid way, so sorry bout that 😅
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
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 😄
plz do, i'm secretly good at this.
not the code, but do message me plz.
👍 thank you so much