diff --git a/islands/DonationSection.tsx b/islands/DonationSection.tsx index dc06091..44003a0 100644 --- a/islands/DonationSection.tsx +++ b/islands/DonationSection.tsx @@ -1,16 +1,13 @@ import { useEffect, useState } from "preact/hooks"; import { Input } from "../components/Input.tsx"; +import { validateEmail } from "../lib/utils.ts"; export const DonationSection = function () { const [emailInput, setEmailInput] = useState(""); const [emailValid, setEmailValid] = useState(false); useEffect(() => { - const valid = emailInput.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])+)\])/, - ); + const valid = validateEmail(emailInput); setEmailValid(!!valid); }, [emailInput]); @@ -45,7 +42,7 @@ export const DonationSection = function () { { if (!emailValid) { e.preventDefault(); diff --git a/lib/utils.ts b/lib/utils.ts index a8190ba..625bb10 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -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; +} diff --git a/lib/vipps/vipps.ts b/lib/vipps/vipps.ts index e80b455..9f64c31 100644 --- a/lib/vipps/vipps.ts +++ b/lib/vipps/vipps.ts @@ -41,41 +41,13 @@ const getAccessToken = async function () { return data.access_token as 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 () { +export const createAgreement = async function (email: string) { const token = await getAccessToken(); - const idempotencyKey = crypto.randomUUID(); const response = await fetch(`${config.baseUrl}/recurring/v3/agreements/`, { method: "POST", headers: { authorization: `Bearer ${token}`, - "Idempotency-Key": idempotencyKey, + "Idempotency-Key": email, ...standardVippsHeaders, }, body: JSON.stringify({ @@ -105,3 +77,21 @@ export const createAgreement = async function () { 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); + } +}; diff --git a/routes/api/trigger-donation/vipps.ts b/routes/api/trigger-donation/vipps.ts index 0b373fc..6bce44a 100644 --- a/routes/api/trigger-donation/vipps.ts +++ b/routes/api/trigger-donation/vipps.ts @@ -1,10 +1,25 @@ -import { getHostName } from "../../../lib/utils.ts"; +import { getHostName, validateEmail } from "../../../lib/utils.ts"; import { createAgreement } from "../../../lib/vipps/vipps.ts"; -export const handler = async function (_req: Request): Promise { - const agreement = await createAgreement(); +export const handler = async function (req: Request): Promise { + 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) { - return Response.redirect(`${getHostName()}/donations-error`); + console.error("Failed to create Vipps agreement", agreement); + return Response.redirect(errorPage); } return Response.redirect(agreement.vippsConfirmationUrl); diff --git a/routes/donations-success.tsx b/routes/donations-success.tsx index a85e9da..5ad5eef 100644 --- a/routes/donations-success.tsx +++ b/routes/donations-success.tsx @@ -1,4 +1,31 @@ -export default function () { +import { PageProps } from "$fresh/server.ts"; +import { getAgreement } from "../lib/vipps/vipps.ts"; + +export const handler: Handlers = { + 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 (