-
Kildekode
+
);
}
diff --git a/docs/what.md b/docs/what.md
index 48f31c1..25bbaf7 100644
--- a/docs/what.md
+++ b/docs/what.md
@@ -7,8 +7,4 @@ egen app fremfor å bygge oppunder åpne standarder som RSS.
Søk på NRK-podcasten du vil høre på. Kopier deretter URL-en under bildet. Lim denne inn i akkurat den
podcastspilleren du måtte foretrekke! Se [her](https://help.omnystudio.com/en/articles/5222518-podcast-apps-that-support-add-rss-feed)
-for en oversikt over hvordan det gjøres i populære podcastspillere.
-
-## Penger og sånt
-
-Denne siden har mye trafikk! Det er jeg veldig takknemlig for. Imidlertid koster det derfor en del penger å ha det kjørende. _Dersom du har anledning til det_ (og bare da!), oppfordrer jeg til å [slenge over en kaffeslant eller to](/donations) slik at NRSS fortsatt kan være gratis tilgjengelig for de som ikke har det.
+for en oversikt over hvordan det gjøres i populære podcastspillere.
\ No newline at end of file
diff --git a/fresh.gen.ts b/fresh.gen.ts
index dd4b900..6e5aed5 100644
--- a/fresh.gen.ts
+++ b/fresh.gen.ts
@@ -5,9 +5,13 @@
import * as $_app from "./routes/_app.tsx";
import * as $api_feeds_seriesId_ from "./routes/api/feeds/[seriesId].ts";
import * as $api_feeds_seriesId_episodeId_chapters from "./routes/api/feeds/[seriesId]/[episodeId]/chapters.ts";
-import * as $donations from "./routes/donations.tsx";
+import * as $api_trigger_donation_vipps from "./routes/api/trigger-donation/vipps.ts";
+import * as $donations_cancel from "./routes/donations-cancel.tsx";
+import * as $donations_error from "./routes/donations-error.tsx";
+import * as $donations_success from "./routes/donations-success.tsx";
import * as $index from "./routes/index.tsx";
import * as $CopyButton from "./islands/CopyButton.tsx";
+import * as $DonationSection from "./islands/DonationSection.tsx";
import { type Manifest } from "$fresh/server.ts";
const manifest = {
@@ -15,11 +19,15 @@ const manifest = {
"./routes/_app.tsx": $_app,
"./routes/api/feeds/[seriesId].ts": $api_feeds_seriesId_,
"./routes/api/feeds/[seriesId]/[episodeId]/chapters.ts": $api_feeds_seriesId_episodeId_chapters,
- "./routes/donations.tsx": $donations,
+ "./routes/api/trigger-donation/vipps.ts": $api_trigger_donation_vipps,
+ "./routes/donations-cancel.tsx": $donations_cancel,
+ "./routes/donations-error.tsx": $donations_error,
+ "./routes/donations-success.tsx": $donations_success,
"./routes/index.tsx": $index,
},
islands: {
"./islands/CopyButton.tsx": $CopyButton,
+ "./islands/DonationSection.tsx": $DonationSection,
},
baseUrl: import.meta.url,
} satisfies Manifest;
diff --git a/islands/DonationSection.tsx b/islands/DonationSection.tsx
new file mode 100644
index 0000000..dc06091
--- /dev/null
+++ b/islands/DonationSection.tsx
@@ -0,0 +1,96 @@
+import { useEffect, useState } from "preact/hooks";
+import { Input } from "../components/Input.tsx";
+
+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])+)\])/,
+ );
+ setEmailValid(!!valid);
+ }, [emailInput]);
+
+ return (
+ <>
+
+
+
+ Økonomisk Støtte
+
+
+ Jeg ønsker at NRSS skal være gratis tilgjengelig for alle. Imidlertid koster det penger og tid å drifte og
+ vedlikeholde en nettside. Dersom du har råd til det (og bare da!) setter jeg stor pris på om du vil støtte
+ prosjektet med et valgfritt, månedlig beløp via Vipps.
+
+
+
+
+
+ Eposten brukes utelukkende som referanse dersom du ønsker å avslutte støtten. Du kommer ikke til å motta
+ noen eposter.
+
+
+
+
+
+ Du kan når som helst avslutte støtten fra{" "}
+
+ denne siden
+ .
+
+
+
+
+ Opplever du problemer med betalingen, eller har du andre spørsmål? Ta{" "}
+
+ kontakt på mail
+ .
+
+
+ >
+ );
+};
diff --git a/lib/storage.ts b/lib/storage.ts
index 6b8e2db..c90d0d6 100644
--- a/lib/storage.ts
+++ b/lib/storage.ts
@@ -24,20 +24,20 @@ function seriesKey(series: { id: string }) {
return ["series", series.id];
}
-async function read(options: { id: string }): Promise {
+async function readSeries(options: { id: string }): Promise {
const { id } = options;
const key = seriesKey({ id });
const read = await kv.get(key);
return read.value;
}
-async function write(series: Series): Promise {
+async function writeSeries(series: Series): Promise {
const key = seriesKey(series);
const stored = await kv.set(key, series);
return stored.ok;
}
export const storage = {
- read,
- write,
+ readSeries,
+ writeSeries,
};
diff --git a/lib/utils.ts b/lib/utils.ts
index 586c836..a8190ba 100644
--- a/lib/utils.ts
+++ b/lib/utils.ts
@@ -5,8 +5,14 @@ export function getHostName() {
if (deploymentId) {
return `https://nrss-${deploymentId}.deno.dev`;
} else {
- // assume env
- return "http://localhost:8000";
+ 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";
+ }
}
}
diff --git a/lib/vipps/vipps.ts b/lib/vipps/vipps.ts
new file mode 100644
index 0000000..e80b455
--- /dev/null
+++ b/lib/vipps/vipps.ts
@@ -0,0 +1,107 @@
+import "jsr:@std/dotenv/load";
+import { getHostName } from "../utils.ts";
+import { STATUS_CODE } from "$fresh/server.ts";
+
+const config = {
+ clientId: Deno.env.get("VIPPS_CLIENT_ID"),
+ clientSecret: Deno.env.get("VIPPS_CLIENT_SECRET"),
+ ocpApimSubscriptionKeyPrimary: Deno.env.get("VIPPS_OCP_APIM_SUBSCRIPTION_KEY_PRIMARY"),
+ ocpApimSubscriptionKeySecondary: Deno.env.get("VIPPS_OCM_APIM_SUBSCRIPTION_KEY_SECONDARY"),
+ testMobileNumber: Deno.env.get("VIPPS_TEST_MOBILE_NUMBER"),
+ msn: Deno.env.get("VIPPS_MSN"),
+ baseUrl: Deno.env.get("VIPPS_API_BASE_URL"),
+};
+
+// ensure all environment variables are set
+for (const [key, value] of Object.entries(config)) {
+ if (!value) {
+ throw new Error(`Missing environment variable: ${key}`);
+ }
+}
+
+const standardVippsHeaders = {
+ "Content-Type": "application/json",
+ "Ocp-Apim-Subscription-Key": config.ocpApimSubscriptionKeyPrimary,
+ "Merchant-Serial-Number": config.msn,
+ "Vipps-System-Name": "Krets AS",
+} as const;
+
+const getAccessToken = async function () {
+ console.log(`Fetching ${`${config.baseUrl}/accesstoken/get`}`);
+ const response = await fetch(`${config.baseUrl}/accesstoken/get`, {
+ method: "POST",
+ headers: {
+ "client_id": config.clientId,
+ "client_secret": config.clientSecret,
+ ...standardVippsHeaders,
+ },
+ });
+
+ const data = await response.json();
+ 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 () {
+ 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,
+ ...standardVippsHeaders,
+ },
+ body: JSON.stringify({
+ "interval": {
+ "unit": "MONTH",
+ "count": 1,
+ },
+ "pricing": {
+ "suggestedMaxAmount": 5000, // 50,- NOK
+ "currency": "NOK",
+ "type": "VARIABLE",
+ },
+ "merchantRedirectUrl": `${getHostName()}/donations-success`,
+ "merchantAgreementUrl": `${getHostName()}/donations`,
+ "productName": "Månedlig støtte til NRSS",
+ }),
+ });
+
+ if (response.status === STATUS_CODE.Created) {
+ const body = response.json();
+ return body as unknown as {
+ vippsConfirmationUrl: string;
+ };
+ } else {
+ const errorMessage = `Failed to create Vipps agreement ${response.status} ${await response.text()}`;
+ console.error(errorMessage);
+ return new Error(errorMessage);
+ }
+};
diff --git a/routes/_app.tsx b/routes/_app.tsx
index 8c4c566..81afab4 100644
--- a/routes/_app.tsx
+++ b/routes/_app.tsx
@@ -1,4 +1,6 @@
import { PageProps } from "$fresh/server.ts";
+import Footer from "../components/Footer.tsx";
+import Header from "../components/Header.tsx";
export default function App({ Component }: PageProps) {
return (
@@ -14,7 +16,9 @@ export default function App({ Component }: PageProps) {
NRSS
+
+