Vipps Donations #33
|
|
@ -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();
|
||||||
|
|
|
||||||
10
lib/utils.ts
10
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;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue