Vipps Donations #33

Merged
olaven merged 18 commits from vipps-demo into main 2024-08-24 16:55:08 +00:00
5 changed files with 80 additions and 41 deletions
Showing only changes of commit 20eee29adc - Show all commits

View File

@ -1,16 +1,13 @@
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
import { Input } from "../components/Input.tsx"; import { Input } from "../components/Input.tsx";
import { validateEmail } from "../lib/utils.ts";
export const DonationSection = function () { export const DonationSection = function () {
const [emailInput, setEmailInput] = useState<string>(""); const [emailInput, setEmailInput] = useState<string>("");
const [emailValid, setEmailValid] = useState(false); const [emailValid, setEmailValid] = useState(false);
useEffect(() => { useEffect(() => {
const valid = emailInput.match( const valid = validateEmail(emailInput);
// https://emailregex.com/
// deno-lint-ignore no-control-regex
/(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/,
);
setEmailValid(!!valid); setEmailValid(!!valid);
}, [emailInput]); }, [emailInput]);
@ -45,7 +42,7 @@ export const DonationSection = function () {
</Input> </Input>
<a <a
// don't navigate if email is not valid // don't navigate if email is not valid
href={emailInput ? "/api/trigger-donation/vipps" : undefined} href={emailInput ? `/api/trigger-donation/vipps?email=${emailInput}` : undefined}
onClick={(e) => { onClick={(e) => {
if (!emailValid) { if (!emailValid) {
e.preventDefault(); e.preventDefault();

View File

@ -36,3 +36,13 @@ function response(body: string, status: number, type: "json" | "xml") {
}, },
}); });
} }
export function validateEmail(input: string) {
input.match(
// https://emailregex.com/
// deno-lint-ignore no-control-regex
/(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/,
);
return !!input;
}

View File

@ -41,41 +41,13 @@ const getAccessToken = async function () {
return data.access_token as string; return data.access_token as string;
}; };
/** export const createAgreement = async function (email: string) {
curl -X POST https://apitest.vipps.no/recurring/v3/agreements/ \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR-ACCESS-TOKEN" \
-H "Ocp-Apim-Subscription-Key: YOUR-SUBSCRIPTION-KEY" \
-H "Merchant-Serial-Number: YOUR-MSN" \
-H 'Idempotency-Key: YOUR-IDEMPOTENCY-KEY' \
-H "Vipps-System-Name: acme" \
-H "Vipps-System-Version: 3.1.2" \
-H "Vipps-System-Plugin-Name: acme-webshop" \
-H "Vipps-System-Plugin-Version: 4.5.6" \
-d '{
"interval": {
"unit" : "WEEK",
"count": 2
},
"pricing": {
"amount": 1000,
"currency": "NOK"
},
"merchantRedirectUrl": "https://example.com/redirect-url",
"merchantAgreementUrl": "https://example.com/agreement-url",
"phoneNumber": "12345678",
"productName": "Test product"
}'
*/
export const createAgreement = async function () {
const token = await getAccessToken(); const token = await getAccessToken();
const idempotencyKey = crypto.randomUUID();
const response = await fetch(`${config.baseUrl}/recurring/v3/agreements/`, { const response = await fetch(`${config.baseUrl}/recurring/v3/agreements/`, {
method: "POST", method: "POST",
headers: { headers: {
authorization: `Bearer ${token}`, authorization: `Bearer ${token}`,
"Idempotency-Key": idempotencyKey, "Idempotency-Key": email,
...standardVippsHeaders, ...standardVippsHeaders,
}, },
body: JSON.stringify({ body: JSON.stringify({
@ -105,3 +77,21 @@ export const createAgreement = async function () {
return new Error(errorMessage); return new Error(errorMessage);
} }
}; };
export const getAgreement = async function (agreementId: string) {
const token = await getAccessToken();
const response = await fetch(`${config.baseUrl}/recurring/v3/agreements/${agreementId}`, {
headers: {
authorization: `Bearer ${token}`,
...standardVippsHeaders,
},
});
if (response.status === STATUS_CODE.OK) {
return response.json();
} else {
const errorMessage = `Failed to get Vipps agreement ${response.status} ${await response.text()}`;
console.error(errorMessage);
return new Error(errorMessage);
}
};

View File

@ -1,10 +1,25 @@
import { getHostName } from "../../../lib/utils.ts"; import { getHostName, validateEmail } from "../../../lib/utils.ts";
import { createAgreement } from "../../../lib/vipps/vipps.ts"; import { createAgreement } from "../../../lib/vipps/vipps.ts";
export const handler = async function (_req: Request): Promise<Response> { export const handler = async function (req: Request): Promise<Response> {
const agreement = await createAgreement(); const email = new URLSearchParams(req.url.split("?")[1] || "").get("email");
const errorPage = `${getHostName()}/donations-error`;
if (!email) {
console.error("Missing email in query params", req.url);
return Response.redirect(errorPage);
}
const valid = validateEmail(email);
if (!valid) {
console.error("Invalid email server side", email);
return Response.redirect(errorPage);
}
const agreement = await createAgreement(email);
if (agreement instanceof Error) { if (agreement instanceof Error) {
return Response.redirect(`${getHostName()}/donations-error`); console.error("Failed to create Vipps agreement", agreement);
return Response.redirect(errorPage);
} }
return Response.redirect(agreement.vippsConfirmationUrl); return Response.redirect(agreement.vippsConfirmationUrl);

View File

@ -1,4 +1,31 @@
export default function () { import { PageProps } from "$fresh/server.ts";
import { getAgreement } from "../lib/vipps/vipps.ts";
export const handler: Handlers<Props> = {
async GET(request, ctx) {
// TODO: somehow get the agreement ID
// TODO: implement cancel page
const agreementId = "123";
const agreement = await getAgreement(agreementId);
if (agreement instanceof Error) {
console.error("Failed to get Vipps agreement", agreement);
return ctx.redirect("/donations-error");
}
return ctx.render({ agreement });
},
};
export default function ({ data }: PageProps<{
// TODO: confirm interface when you know it
agreement: {
status: string;
id: string;
};
status: string;
}>) {
return ( return (
<div className="my-16 text-center flex flex-col items-center min-h-screen "> <div className="my-16 text-center flex flex-col items-center min-h-screen ">
<h1 className="text-6xl mb-8"> <h1 className="text-6xl mb-8">