diff options
| author | Joe Carstairs <65492573+Sycamost@users.noreply.github.com> | 2023-11-29 18:15:14 +0000 |
|---|---|---|
| committer | Joe Carstairs <65492573+Sycamost@users.noreply.github.com> | 2023-11-29 18:15:14 +0000 |
| commit | 9ced895772ebecb2fa330ae2eecd42060abf2a7b (patch) | |
| tree | b08b6a08fd39a11eaef2a2e2b92a661ad00247b0 | |
| parent | 960b019a018c2faf8da1c4a4ade6c12b3c21e5bd (diff) | |
| -rw-r--r-- | README.md | 25 | ||||
| -rw-r--r-- | api/_postgres.ts | 4 | ||||
| -rw-r--r-- | api/create-subscription.ts | 26 | ||||
| -rw-r--r-- | src/components/LallansIssue.astro | 4 | ||||
| -rw-r--r-- | src/components/ProductPayPalButtons.astro (renamed from src/components/PayPalButtons.astro) | 10 | ||||
| -rw-r--r-- | src/components/Scotsoun.astro | 4 | ||||
| -rw-r--r-- | src/components/SubscriptionPayPalButtons.astro | 112 | ||||
| -rw-r--r-- | src/pages/[locale]/created-subscription.astro | 7 | ||||
| -rw-r--r-- | src/pages/[locale]/index.astro | 4 |
9 files changed, 185 insertions, 11 deletions
@@ -101,8 +101,8 @@ Follow this guidance alongside the [general guidance](#general-guidance). ## Infrastructure -The deployment infrastructure for the website is in four parts: the domains, the SSL certificates, -the hosting, and the CI/CD. +The deployment infrastructure for the website is in five parts: the domains, the SSL certificates, +the hosting, the CI/CD and the database. ### Domains @@ -151,3 +151,24 @@ deploy, and you've checked that everything looks good on [make a Pull Request to merge deployment-staging into deployment-production](https://github.com/Sycamost/lallans-wabsteid-astro/compare/deployment-production...deployment-staging). Once that Pull Request is approved by a code owner, merge it in and it should update production in under five minutes. + +### Database + +We have a database. It is a +[Postgres database hosted on Vercel](https://vercel.com/docs/storage/vercel-postgres/quickstart). + +It has two tables: + +- subscriptions + - id: VARCHAR(255) + - email_address: VARCHAR(255) +- dev_subscriptions + - id: VARCHAR(255) + - email_address: VARCHAR(255) + +One table is there so that users can look up their subscriptions, and thereby +retrieve the subscription ID so that they can amend or cancel their subscription. +The second table is there to test out the Postgres integration in staging +with dummy data. + +The easiest way to view, update, create and delete tables is on the Vercel dashboard. diff --git a/api/_postgres.ts b/api/_postgres.ts new file mode 100644 index 0000000..70b25c8 --- /dev/null +++ b/api/_postgres.ts @@ -0,0 +1,4 @@ +import env from './_env'; + +export const tablePrefix = env.ENVIRONMENT === 'prod' ? '' : 'dev_'; +export const tableSubscriptions = `${tablePrefix}subscriptions`; diff --git a/api/create-subscription.ts b/api/create-subscription.ts new file mode 100644 index 0000000..70b1499 --- /dev/null +++ b/api/create-subscription.ts @@ -0,0 +1,26 @@ +import type { VercelRequest, VercelResponse } from '@vercel/node'; + +import { sql } from '@vercel/postgres'; +import { tableSubscriptions } from '_postgres'; + +export default async function handler(request: VercelRequest, response: VercelResponse) { + const { subscriptionId, emailAddress }: Partial<RequestBody> = request.body; + + if (!subscriptionId) throw new Error('Missing parameter: subscriptionId.'); + if (!emailAddress) throw new Error('Missing parameter: emailAddress.'); + + try { + const result = await sql` + INSERT INTO ${tableSubscriptions} (id, email_address) + VALUES (${subscriptionId}, ${emailAddress}) + ;`; + return response.status(200).json({ result }); + } catch (error) { + return response.status(500).json({ error }); + } +} + +type RequestBody = { + subscriptionId: string, + emailAddress: string, +}; diff --git a/src/components/LallansIssue.astro b/src/components/LallansIssue.astro index c8bc6eb..9fe686a 100644 --- a/src/components/LallansIssue.astro +++ b/src/components/LallansIssue.astro @@ -7,7 +7,7 @@ import type LallansIssue from '$types/LallansIssue'; import { Image } from 'astro:assets'; import LallansIssueContributions from './LallansIssueContributions.astro'; import LallansIssueContributors from './LallansIssueContributors.astro'; -import PayPalButtons from './PayPalButtons.astro'; +import ProductPayPalButtons from './ProductPayPalButtons.astro'; interface Props { issue: LallansIssue; @@ -27,7 +27,7 @@ const t = translate(locale); <div class="product__action-block"> <Image alt="" src={issue.img.width274} /> - <PayPalButtons + <ProductPayPalButtons successPageUrl={relativePath(Astro.url.pathname, `payment-success-confirmation`)} productDescription={`Lallans ${issue.issueNumber} | ${issue.issueName}`} shortDescription={`Lallans ${issue.issueNumber}`} diff --git a/src/components/PayPalButtons.astro b/src/components/ProductPayPalButtons.astro index cdea5cb..952e6a8 100644 --- a/src/components/PayPalButtons.astro +++ b/src/components/ProductPayPalButtons.astro @@ -15,7 +15,7 @@ const PAYPAL_CLIENT_ID = env.ENVIRONMENT === 'prod' ? env.PAYPAL_LIVE_CLIENT_ID : env.PAYPAL_SANDBOX_CLIENT_ID; --- -<paypal-buttons +<product-paypal-buttons data-paypal-client-id={PAYPAL_CLIENT_ID} data-success-page-url={successPageUrl} data-product-description={productDescription} @@ -23,14 +23,14 @@ const PAYPAL_CLIENT_ID = data-total-price={totalPrice} > <div id="paypal-button-container"></div> -</paypal-buttons> +</product-paypal-buttons> <script> import { PayPalNamespace, loadScript } from '@paypal/paypal-js'; const backendUrl = '/api'; - class PayPalButtons extends HTMLElement { + class ProductPayPalButtons extends HTMLElement { constructor() { super(); @@ -140,11 +140,11 @@ const PAYPAL_CLIENT_ID = getStringDataAttribute(key: string): string { const value = this.dataset[key]; if (!value || typeof value !== 'string') { - throw new Error(`data-${key} attribute not provided to PayPalButtons component.`); + throw new Error(`data-${key} attribute not provided to ProductPayPalButtons component.`); } return value; } } - customElements.define('paypal-buttons', PayPalButtons); + customElements.define('product-paypal-buttons', ProductPayPalButtons); </script> diff --git a/src/components/Scotsoun.astro b/src/components/Scotsoun.astro index bbf9730..8252e71 100644 --- a/src/components/Scotsoun.astro +++ b/src/components/Scotsoun.astro @@ -4,7 +4,7 @@ import translate from '$i18n/translate'; import tSite from '$i18n/translations/site'; import tComponent from '$i18n/translations/components/scotsoun'; import type Scotsoun from '$types/Scotsoun'; -import PayPalButtons from './PayPalButtons.astro'; +import ProductPayPalButtons from './ProductPayPalButtons.astro'; import relativePath from '$lib/relativePath'; import { getLocaleFromPathOrThrow } from '$lib/getLocaleFromPath'; @@ -27,7 +27,7 @@ const t = translate(locale); </header> <div class="product__action-block"> <Image src={scotsoun.img.width192} alt="" /> - <PayPalButtons + <ProductPayPalButtons successPageUrl={relativePath(Astro.url.pathname, `payment-success-confirmation`)} productDescription={`SSCD${scotsoun.scotsounId} | ${scotsoun.longName}`} shortDescription={`SSCD${scotsoun.scotsounId}`} diff --git a/src/components/SubscriptionPayPalButtons.astro b/src/components/SubscriptionPayPalButtons.astro new file mode 100644 index 0000000..bc77580 --- /dev/null +++ b/src/components/SubscriptionPayPalButtons.astro @@ -0,0 +1,112 @@ +--- +import env from '$lib/env'; +import type Price from '$types/Price'; + +interface Props { + successPageUrl: string; + productDescription: string; + shortDescription: string; + totalPrice: Price; +} + +const { successPageUrl, productDescription, shortDescription, totalPrice } = Astro.props; + +const PAYPAL_CLIENT_ID = + env.ENVIRONMENT === 'prod' ? env.PAYPAL_LIVE_CLIENT_ID : env.PAYPAL_SANDBOX_CLIENT_ID; +--- + +<subscription-paypal-buttons + data-paypal-client-id={PAYPAL_CLIENT_ID} + data-success-page-url={successPageUrl} + data-failure-page-url={failurePageUrl} +> + <div id="paypal-button-container"></div> +</subscription-paypal-buttons> + +<script> + import { PayPalNamespace, loadScript } from '@paypal/paypal-js'; + + const backendUrl = '/api'; + + class ProductPayPalButtons extends HTMLElement { + constructor() { + super(); + + const paypalClientId = this.getStringDataAttribute('paypalClientId'); + + const paypalOptions = { + clientId: paypalClientId, + locale: 'en_GB', + commit: false, + currency: 'GBP', + intent: 'capture', + }; + + loadScript(paypalOptions) + .catch((err) => { + throw new Error('Failed to load the PayPal JS SDK script. ', err); + }) + .then((paypal) => { + const constructButtons = paypal?.Buttons; + if (paypal && constructButtons) { + const successPageUrl = this.dataset['successPageUrl'] ?? '.'; + this.makePaypalButtons(constructButtons, successPageUrl).render( + '#paypal-button-container' + ); + } + }); + } + + makePaypalButtons( + constructButtons: Required<PayPalNamespace>['Buttons'], + successPageUrl: string + ) { + // TODO: figure out where subscriptionId is coming from! Probably it’s going to be + // generated by PayPal on the backend, and you don’t want it here at all. + + // TODO: get emailAddress from user session. + + return constructButtons({ + async createSubscription() { + try { + const response = await fetch("/api/create-subscription", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ subscriptionId, emailAddress }), + }); + + const data = await response.json(); + + if (data?.id) { + console.info('Subscription successful. Redirecting to confirmation page...'); + window.location.href = successPageUrl; + return data.id; + } else { + console.error( + { callback: "createSubscription", serverResponse: data }, + JSON.stringify(data, null, 2), + ); + console.info('Redirecting to failure page...'); + window.location.href = failurePageUrl; + } + } catch (error) { + console.error('Redirecting to failure page, because of an error creating a subscription.' error); + window.location.href = failurePageUrl; + } + }, + }).render("#paypal-button-container"); // Renders the PayPal button + } + + getStringDataAttribute(key: string): string { + const value = this.dataset[key]; + if (!value || typeof value !== 'string') { + throw new Error(`data-${key} attribute not provided to ProductPayPalButtons component.`); + } + return value; + } + } + + customElements.define('product-paypal-buttons', ProductPayPalButtons); +</script> diff --git a/src/pages/[locale]/created-subscription.astro b/src/pages/[locale]/created-subscription.astro new file mode 100644 index 0000000..f913932 --- /dev/null +++ b/src/pages/[locale]/created-subscription.astro @@ -0,0 +1,7 @@ +--- +import Page from '$layouts'; +--- + +<Page title="TODO"> + <h1>TODO<h1> +</Page> diff --git a/src/pages/[locale]/index.astro b/src/pages/[locale]/index.astro index c67ded0..e5ba6b7 100644 --- a/src/pages/[locale]/index.astro +++ b/src/pages/[locale]/index.astro @@ -64,6 +64,10 @@ const mostRecentNewsItem = await getMostRecentNewsItem(locale); <p set:html={t(tPage, { key: 'why-jyne-para-3' })} /> <h3 set:html={t(tPage, { key: 'hou-jyne' })} /> + + <PayPalButtons + successPageUrl={`/${locale}/created-subscription`} + <p set:html={t(tPage, { key: 'hou-jyne-para-1' })} /> <p set:html={t(tPage, { |
