Cancellation implemented
This commit is contained in:
parent
20eee29adc
commit
5b3f804391
|
|
@ -1,5 +1,5 @@
|
|||
import { declaration, serialize, Tag, tag } from "serialize-xml";
|
||||
import { getHostName } from "./utils.ts";
|
||||
import { getHostUrl } from "./utils.ts";
|
||||
import { Episode, Series } from "./storage.ts";
|
||||
|
||||
function assembleFeed(series: Series): string {
|
||||
|
|
@ -71,7 +71,7 @@ function assembleEpisode(episode: Episode, seriesId: Series["id"]): Tag {
|
|||
tag("podcast:chapters", "", [
|
||||
[
|
||||
"url",
|
||||
`${getHostName()}/api/feeds/${seriesId}/${episode.id}/chapters`,
|
||||
`${getHostUrl()}/api/feeds/${seriesId}/${episode.id}/chapters`,
|
||||
],
|
||||
["type", "application/json+chapters"],
|
||||
]),
|
||||
|
|
|
|||
|
|
@ -19,10 +19,12 @@ export type Series = {
|
|||
};
|
||||
|
||||
export type VippsAgreement = {
|
||||
// the user identifier / email
|
||||
id: string;
|
||||
// the agreement id from Vipps
|
||||
agreementId: string;
|
||||
userEmail: string;
|
||||
createdAt: Date;
|
||||
validAt: Date | null;
|
||||
revokedAt: Date | null;
|
||||
};
|
||||
|
||||
|
|
|
|||
16
lib/utils.ts
16
lib/utils.ts
|
|
@ -1,18 +1,16 @@
|
|||
import { STATUS_CODE } from "$fresh/server.ts";
|
||||
|
||||
export function getHostName() {
|
||||
export function getHostUrl() {
|
||||
const deploymentId = Deno.env.get("DENO_DEPLOYMENT_ID");
|
||||
const tunnelUrl = Deno.env.get("TUNNEL_URL");
|
||||
if (deploymentId) {
|
||||
return `https://nrss-${deploymentId}.deno.dev`;
|
||||
} else if (tunnelUrl) {
|
||||
console.debug(`Using tunnel URL: ${tunnelUrl}`);
|
||||
return tunnelUrl;
|
||||
} else {
|
||||
const proxyUrl = Deno.env.get("PROXY_URL");
|
||||
if (proxyUrl) {
|
||||
console.debug(`Using proxy URL ${proxyUrl}`);
|
||||
return proxyUrl;
|
||||
} else {
|
||||
console.debug(`Assuming localhost`);
|
||||
return "http://localhost:8000";
|
||||
}
|
||||
console.debug(`Assuming localhost`);
|
||||
return "http://localhost:8000";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import "jsr:@std/dotenv/load";
|
||||
import { getHostName } from "../utils.ts";
|
||||
import { getHostUrl } from "../utils.ts";
|
||||
import { STATUS_CODE } from "$fresh/server.ts";
|
||||
|
||||
const config = {
|
||||
|
|
@ -47,7 +47,7 @@ export const createAgreement = async function (email: string) {
|
|||
method: "POST",
|
||||
headers: {
|
||||
authorization: `Bearer ${token}`,
|
||||
"Idempotency-Key": email,
|
||||
"Idempotency-Key": `${email}-${Date.now()}`,
|
||||
...standardVippsHeaders,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
|
|
@ -60,8 +60,9 @@ export const createAgreement = async function (email: string) {
|
|||
"currency": "NOK",
|
||||
"type": "VARIABLE",
|
||||
},
|
||||
"merchantRedirectUrl": `${getHostName()}/donations-success`,
|
||||
"merchantAgreementUrl": `${getHostName()}/donations`,
|
||||
// email is used to identify the user in the success page
|
||||
"merchantRedirectUrl": `${getHostUrl()}/donations-success?email=${email}`,
|
||||
"merchantAgreementUrl": `${getHostUrl()}`,
|
||||
"productName": "Månedlig støtte til NRSS",
|
||||
}),
|
||||
});
|
||||
|
|
@ -70,6 +71,7 @@ export const createAgreement = async function (email: string) {
|
|||
const body = response.json();
|
||||
return body as unknown as {
|
||||
vippsConfirmationUrl: string;
|
||||
agreementId: string;
|
||||
};
|
||||
} else {
|
||||
const errorMessage = `Failed to create Vipps agreement ${response.status} ${await response.text()}`;
|
||||
|
|
@ -95,3 +97,26 @@ export const getAgreement = async function (agreementId: string) {
|
|||
return new Error(errorMessage);
|
||||
}
|
||||
};
|
||||
|
||||
export const cancelAgreement = async function (agreementId: string) {
|
||||
const token = await getAccessToken();
|
||||
const response = await fetch(`${config.baseUrl}/recurring/v3/agreements/${agreementId}`, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
authorization: `Bearer ${token}`,
|
||||
"Idempotency-Key": `${agreementId}-${Date.now()}`,
|
||||
...standardVippsHeaders,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
"status": "STOPPED",
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.status === STATUS_CODE.NoContent) {
|
||||
return true;
|
||||
} else {
|
||||
const errorMessage = `Failed to cancel Vipps agreement ${response.status} ${await response.text()}`;
|
||||
console.error(errorMessage);
|
||||
return new Error(errorMessage);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
import { getHostName, validateEmail } from "../../../lib/utils.ts";
|
||||
import { getHostUrl, validateEmail } from "../../../lib/utils.ts";
|
||||
import { storage } from "../../../lib/storage.ts";
|
||||
import { createAgreement } from "../../../lib/vipps/vipps.ts";
|
||||
|
||||
export const handler = async function (req: Request): Promise<Response> {
|
||||
const email = new URLSearchParams(req.url.split("?")[1] || "").get("email");
|
||||
const errorPage = `${getHostName()}/donations-error`;
|
||||
const errorPage = `${getHostUrl()}/donations-error`;
|
||||
|
||||
if (!email) {
|
||||
console.error("Missing email in query params", req.url);
|
||||
|
|
@ -22,5 +23,15 @@ export const handler = async function (req: Request): Promise<Response> {
|
|||
return Response.redirect(errorPage);
|
||||
}
|
||||
|
||||
// associate the agreement with the user here
|
||||
// so it can be updated in the success page
|
||||
await storage.writeVippsAgreement({
|
||||
id: email,
|
||||
agreementId: agreement.agreementId,
|
||||
createdAt: new Date(),
|
||||
validAt: null,
|
||||
revokedAt: null,
|
||||
});
|
||||
|
||||
return Response.redirect(agreement.vippsConfirmationUrl);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,50 @@
|
|||
import { Handlers, PageProps } from "$fresh/server.ts";
|
||||
import { Button } from "../components/Button.tsx";
|
||||
import { Input } from "../components/Input.tsx";
|
||||
import { storage } from "../lib/storage.ts";
|
||||
import * as vipps from "../lib/vipps/vipps.ts";
|
||||
import { validateEmail } from "../lib/utils.ts";
|
||||
|
||||
export default function () {
|
||||
type Props = {
|
||||
email: string;
|
||||
error: string | null;
|
||||
cancelled: boolean;
|
||||
};
|
||||
|
||||
export const handler: Handlers<Props> = {
|
||||
async GET(request, ctx) {
|
||||
const url = new URL(request.url);
|
||||
const email = url.searchParams.get("email");
|
||||
|
||||
if (!email) {
|
||||
return ctx.render({ email: null, error: null, cancelled: false });
|
||||
}
|
||||
|
||||
if (!validateEmail(email)) {
|
||||
console.error("Ugyldig e-post server-side", email);
|
||||
return ctx.render({ email: null, error: "Ugyldig e-post", cancelled: false });
|
||||
}
|
||||
|
||||
const agreement = await storage.readVippsAgreement({ id: email });
|
||||
if (!agreement) {
|
||||
console.error("Mangler avtale for e-post", email);
|
||||
return ctx.render({ email: null, error: `Ingen abonnement funnet for "${email}"`, cancelled: false });
|
||||
}
|
||||
|
||||
const vippsResponse = await vipps.cancelAgreement(agreement.agreementId);
|
||||
if (vippsResponse instanceof Error) {
|
||||
return ctx.render({ email, error: "Kunne ikke kansellere Vipps-abonnement", cancelled: false });
|
||||
}
|
||||
await storage.writeVippsAgreement({
|
||||
...agreement,
|
||||
revokedAt: new Date(),
|
||||
});
|
||||
|
||||
return ctx.render({ email, error: null, cancelled: true });
|
||||
},
|
||||
};
|
||||
|
||||
export default function ({ data }: PageProps<Props>) {
|
||||
return (
|
||||
<div
|
||||
className={"p-4 mx-auto max-w-screen-md text-center"}
|
||||
|
|
@ -10,10 +54,36 @@ export default function () {
|
|||
>
|
||||
Avslutt støtte til NRSS
|
||||
</h1>
|
||||
<Input
|
||||
placeholder={"ditt mobillnummer"}
|
||||
>
|
||||
</Input>
|
||||
{data.error && (
|
||||
<p className={"text-red-500"}>
|
||||
En feil oppsto: "{data.error}". Ta gjerne{" "}
|
||||
<a className="text-blue-600 underline" href="mailto:olav@sundfoer.com">
|
||||
kontakt på mail
|
||||
</a>.
|
||||
</p>
|
||||
)}
|
||||
{data.cancelled && (
|
||||
<p>
|
||||
Støtten er nå avsluttet. Tusen takk for at du har støttet NRSS!
|
||||
</p>
|
||||
)}
|
||||
{!data.cancelled && (
|
||||
<form>
|
||||
<Input
|
||||
type={"email"}
|
||||
placeholder={"din@epost.no"}
|
||||
required
|
||||
name={"email"}
|
||||
id={"email"}
|
||||
>
|
||||
</Input>
|
||||
<Button
|
||||
type={"submit"}
|
||||
>
|
||||
Avslutt
|
||||
</Button>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,37 @@
|
|||
import { PageProps } from "$fresh/server.ts";
|
||||
import { getAgreement } from "../lib/vipps/vipps.ts";
|
||||
import { Handlers, PageProps } from "$fresh/server.ts";
|
||||
import { storage } from "../lib/storage.ts";
|
||||
import { validateEmail } from "../lib/utils.ts";
|
||||
|
||||
export const handler: Handlers<Props> = {
|
||||
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");
|
||||
const url = new URL(request.url);
|
||||
const email = url.searchParams.get("email");
|
||||
if (!email) {
|
||||
console.error("Missing email in query params", request.url);
|
||||
return Response.redirect("/donations-error");
|
||||
}
|
||||
|
||||
return ctx.render({ agreement });
|
||||
if (!validateEmail(email)) {
|
||||
console.error("Invalid email server side", email);
|
||||
return Response.redirect("/donations-error");
|
||||
}
|
||||
|
||||
const agreement = await storage.readVippsAgreement({ id: email });
|
||||
if (!agreement) {
|
||||
console.error("Missing agreement for email", email);
|
||||
return Response.redirect("/donations-error");
|
||||
}
|
||||
|
||||
const validatedAgreement = await storage.writeVippsAgreement({
|
||||
...agreement,
|
||||
validAt: new Date(),
|
||||
});
|
||||
|
||||
return ctx.render({ agreement: validatedAgreement });
|
||||
},
|
||||
};
|
||||
|
||||
export default function ({ data }: PageProps<{
|
||||
// TODO: confirm interface when you know it
|
||||
agreement: {
|
||||
status: string;
|
||||
id: string;
|
||||
|
|
|
|||
Loading…
Reference in New Issue