Use Stripe Address and email admin on membership creation

This commit is contained in:
Tony Grosinger 2023-12-25 19:04:15 -08:00
parent 896bb5242a
commit 9287b4da1a
7 changed files with 273 additions and 65 deletions

142
package-lock.json generated
View File

@ -29,6 +29,7 @@
"feed": "^4.2.2",
"next": "13.4.16",
"next-themes": "^0.2.1",
"postmark": "4.0.2",
"react": "18.2.0",
"react-dom": "18.2.0",
"remark-gfm": "^3.0.1",
@ -1300,6 +1301,11 @@
"astring": "bin/astring"
}
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"node_modules/autoprefixer": {
"version": "10.4.14",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz",
@ -1353,6 +1359,16 @@
"node": ">=4"
}
},
"node_modules/axios": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz",
"integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==",
"dependencies": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/axobject-query": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz",
@ -2085,6 +2101,17 @@
"simple-swizzle": "^0.2.2"
}
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/comma-separated-tokens": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
@ -2351,6 +2378,14 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
@ -3446,6 +3481,25 @@
"integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
"dev": true
},
"node_modules/follow-redirects": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz",
"integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/for-each": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
@ -3455,6 +3509,19 @@
"is-callable": "^1.1.3"
}
},
"node_modules/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/fraction.js": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz",
@ -6706,7 +6773,6 @@
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"peer": true,
"engines": {
"node": ">= 0.6"
}
@ -6715,7 +6781,6 @@
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"peer": true,
"dependencies": {
"mime-db": "1.52.0"
},
@ -7721,6 +7786,14 @@
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
},
"node_modules/postmark": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/postmark/-/postmark-4.0.2.tgz",
"integrity": "sha512-2zlCv+KVVQ0KoamXZHE7d+gXzLlr8tPE+PxQmtUaIZhbHzZAq4D6yH2b+ykhA8wYCc5ISodcx8U1aNLenXBs9g==",
"dependencies": {
"axios": "^1.6.2"
}
},
"node_modules/prebuild-install": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz",
@ -7914,6 +7987,11 @@
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/pseudomap": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
@ -11351,6 +11429,11 @@
"resolved": "https://registry.npmjs.org/astring/-/astring-1.8.6.tgz",
"integrity": "sha512-ISvCdHdlTDlH5IpxQJIex7BWBywFWgjJSVdwst+/iQCoEYnyOaQ95+X1JGshuBjGp6nxKUy1jMgE3zPqN7fQdg=="
},
"asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
},
"autoprefixer": {
"version": "10.4.14",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz",
@ -11376,6 +11459,16 @@
"integrity": "sha512-zIURGIS1E1Q4pcrMjp+nnEh+16G56eG/MUllJH8yEvw7asDo7Ac9uhC9KIH5jzpITueEZolfYglnCGIuSBz39g==",
"dev": true
},
"axios": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.2.tgz",
"integrity": "sha512-7i24Ri4pmDRfJTR7LDBhsOTtcm+9kjX5WiY1X3wIisx6G9So3pfMkEiU7emUBe46oceVImccTEM3k6C5dbVW8A==",
"requires": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
"proxy-from-env": "^1.1.0"
}
},
"axobject-query": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz",
@ -11883,6 +11976,14 @@
"simple-swizzle": "^0.2.2"
}
},
"combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"requires": {
"delayed-stream": "~1.0.0"
}
},
"comma-separated-tokens": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
@ -12067,6 +12168,11 @@
"object-keys": "^1.1.1"
}
},
"delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
},
"dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
@ -12896,6 +13002,11 @@
"integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==",
"dev": true
},
"follow-redirects": {
"version": "1.15.3",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz",
"integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q=="
},
"for-each": {
"version": "0.3.3",
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
@ -12905,6 +13016,16 @@
"is-callable": "^1.1.3"
}
},
"form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
}
},
"fraction.js": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz",
@ -15147,14 +15268,12 @@
"mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"peer": true
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
},
"mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"peer": true,
"requires": {
"mime-db": "1.52.0"
}
@ -15835,6 +15954,14 @@
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="
},
"postmark": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/postmark/-/postmark-4.0.2.tgz",
"integrity": "sha512-2zlCv+KVVQ0KoamXZHE7d+gXzLlr8tPE+PxQmtUaIZhbHzZAq4D6yH2b+ykhA8wYCc5ISodcx8U1aNLenXBs9g==",
"requires": {
"axios": "^1.6.2"
}
},
"prebuild-install": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.1.tgz",
@ -15934,6 +16061,11 @@
"resolved": "https://registry.npmjs.org/property-information/-/property-information-6.2.0.tgz",
"integrity": "sha512-kma4U7AFCTwpqq5twzC1YVIDXSqg6qQK6JN0smOw8fgRy1OkMi0CYSzFmsy6dnqSenamAtj0CyXMUJ1Mf6oROg=="
},
"proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"pseudomap": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",

View File

@ -31,6 +31,7 @@
"feed": "^4.2.2",
"next": "13.4.16",
"next-themes": "^0.2.1",
"postmark": "4.0.2",
"react": "18.2.0",
"react-dom": "18.2.0",
"remark-gfm": "^3.0.1",

View File

@ -4,7 +4,7 @@ import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || '');
export async function POST(request: NextRequest): Promise<NextResponse> {
const { amount, payment_intent_id } = await request.json();
const { amount, payment_intent_id, metadata } = await request.json();
if (payment_intent_id) {
try {
@ -18,6 +18,7 @@ export async function POST(request: NextRequest): Promise<NextResponse> {
payment_intent_id,
{
amount: amount,
metadata: metadata || {},
},
);
@ -38,13 +39,14 @@ export async function POST(request: NextRequest): Promise<NextResponse> {
try {
// Create PaymentIntent
const params = {
const params: Stripe.PaymentIntentCreateParams = {
amount: amount,
currency: 'usd',
payment_method_types: ['card'],
automatic_payment_methods: {
enabled: false,
},
metadata: metadata || {},
};
const payment_intent = await stripe.paymentIntents.create(params);
//Return the payment_intent object

View File

@ -0,0 +1,29 @@
import { emailNotification } from '@/lib/email';
import { NextRequest } from 'next/server';
export async function POST(request: NextRequest): Promise<Response> {
const body = await request.json();
if (body.type === 'charge.succeeded') {
const data = body.data.object;
const billing = data.billing_details;
const cityStateZip = `${billing.address.city}, ${billing.address.state} ${billing.address.postal_code}`;
const address = billing.address.line2
? `${billing.address.line1}\n${cityStateZip}`
: `${billing.address.line1}\n${billing.address.line2}\n${cityStateZip}`;
emailNotification({
name: billing.name,
type: data.metadata.type,
amount: data.amount,
email: billing.email,
phone: billing.phone,
address,
});
} else {
console.log('Another type of event');
console.log(body);
}
return new Response(null, { status: 200 });
}

View File

@ -1,8 +1,8 @@
"use client"
import React, { useEffect, useState, FormEvent } from 'react';
import { Elements } from '@stripe/react-stripe-js';
import { Appearance, StripeElementsOptions, loadStripe } from '@stripe/stripe-js';
import { AddressElement, Elements } from '@stripe/react-stripe-js';
import { Appearance, StripeAddressElementOptions, StripeElementsOptions, loadStripe } from '@stripe/stripe-js';
import {
PaymentElement,
useStripe,
@ -12,6 +12,25 @@ import { RadioGroup } from '@headlessui/react'
const stripe = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY || '');
const addressOptions: StripeAddressElementOptions = {
mode: 'billing',
allowedCountries: ['US'],
fields: {
phone: 'always',
},
validation: {
phone: {
required: 'always'
}
},
defaultValues: {
address: {
state: 'WA',
country: 'US',
},
}
};
const membershipLevels = [
{ id: 1, title: 'Individual', description: 'One vote', price: 20 },
{ id: 2, title: 'Household', description: 'Two votes', price: 30 },
@ -40,9 +59,7 @@ function CheckoutForm({
const [selectedAdditionalDonation, setSelectedAdditionalDonation] = useState<number | null>(null);
const [customAmount, setCustomAmount] = useState('');
const [email, setEmail] = useState('');
const [phone, setPhone] = useState('');
const [street, setStreet] = useState('');
const [zip, setZip] = useState('');
const [offsetFees, setOffsetFees] = useState(true);
const [totalAmount, setTotalAmount] = useState(300);
const [message, setMessage] = useState<string>('');
const [isLoading, setIsLoading] = useState(false);
@ -94,6 +111,10 @@ function CheckoutForm({
}
}
if (offsetFees) {
subtotal = Math.ceil(subtotal * 1.03)
}
setTotalAmount(subtotal);
fetch('api/stripe_intent', {
@ -102,9 +123,12 @@ function CheckoutForm({
body: JSON.stringify({
amount: subtotal * 100,
payment_intent_id: paymentIntentID,
metadata: {
'type': selectedMembershipLevel.title,
},
}),
});
}, [paymentIntentID, selectedMembershipLevel, selectedAdditionalDonation, customAmount])
}, [paymentIntentID, selectedMembershipLevel, selectedAdditionalDonation, customAmount, offsetFees, email])
const handleSubmit = async (e: FormEvent) => {
e.preventDefault();
@ -123,7 +147,8 @@ function CheckoutForm({
receipt_email: email,
payment_method_data: {
billing_details: {
name: 'Billing user',
// Other details are filled automatically by address form.
email,
},
},
},
@ -270,54 +295,7 @@ function CheckoutForm({
placeholder="you@example.com"
/>
</div>
<div className="rounded-md mb-3 px-3 pb-1.5 pt-2.5 shadow-sm ring-1 ring-inset ring-gray-300 focus-within:ring-2 focus-within:ring-indigo-600">
<label htmlFor="name" className="block text-xs font-medium text-gray-900">
Phone
</label>
<input
type="tel"
name="phone"
id="phone"
value={phone}
onChange={(e) => setPhone(e.target.value)}
className="block w-full border-0 p-0 text-gray-900 placeholder:text-gray-400 focus:ring-0 sm:text-sm sm:leading-6"
placeholder="360-376-XXXX"
/>
</div>
<fieldset>
<div className="mb-3 -space-y-px rounded-md bg-white shadow-sm">
<div className="flex -space-x-px">
<div className="w-2/3 min-w-0 flex-1">
<label htmlFor="street-address" className="sr-only">
Street Address
</label>
<input
type="text"
name="street-address"
id="street-address"
value={street}
onChange={(e) => setStreet(e.target.value)}
className="relative block w-full rounded-none rounded-l-md border-0 bg-transparent py-1.5 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:z-10 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
placeholder="Street"
/>
</div>
<div className="min-w-0 flex-1">
<label htmlFor="zipcode" className="sr-only">
Zipcode
</label>
<input
type="text"
name="zipcode"
id="zipcode"
value={zip}
onChange={(e) => setZip(e.target.value)}
className="relative block w-full rounded-none rounded-r-md border-0 bg-transparent py-1.5 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:z-10 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6"
placeholder="98XXX"
/>
</div>
</div>
</div>
</fieldset>
<AddressElement options={addressOptions} />
</div>
<div className="space-y-3">
@ -334,6 +312,24 @@ function CheckoutForm({
{/* TODO: Automatically renew toggle? */}
<div className="relative flex gap-x-3">
<div className="flex h-6 items-center">
<input
id="offsetFees"
name="offsetFees"
type="checkbox"
checked={offsetFees}
onChange={(e) => setOffsetFees(e.target.checked)}
className="h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600"
/>
</div>
<div className="text-sm leading-6">
<label htmlFor="offsetFees" className="font-medium text-gray-900">
Help offset credit card fees
</label>
</div>
</div>
<button
className="mt-6 w-full rounded-md flex justify-center border border-transparent bg-indigo-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
disabled={isLoading || !stripe || !elements}
@ -347,7 +343,7 @@ function CheckoutForm({
</button>
{/* Show any error or success messages */}
{message && <div id="payment-message">{message}</div>}
{message && <div className="text-red-500" id="payment-message">{message}</div>}
</form>
</>
);
@ -362,7 +358,7 @@ export function ClubPayment() {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
amount: 30000,
amount: 2000,
payment_intent_id: '',
}),
})

View File

@ -7,7 +7,17 @@ export const metadata: Metadata = {
description: 'Thanks for becoming a member.',
}
export default function ThankYou() {
export default async function ThankYou({
searchParams
}: {
searchParams: { [key: string]: string | string[] | undefined }
}) {
const { redirect_status } = searchParams;
if (redirect_status !== 'succeeded') {
// TODO: Display error
}
return (
<SimpleLayout
title="Thanks for becoming a member."

38
src/lib/email.ts Normal file
View File

@ -0,0 +1,38 @@
import { ServerClient } from 'postmark';
const fromAddr = process.env.FROM_ADDRESS || '';
const toAddr = process.env.ADMIN_ADDRESS || '';
const serverToken = process.env.POSTMARK_SERVER_TOKEN || '';
const client = new ServerClient(serverToken);
export function emailNotification({
name,
type,
amount,
email,
phone,
address,
}: {
name: string;
type: string;
amount: number;
email: string;
phone: string;
address: string;
}) {
client.sendEmail({
From: fromAddr,
To: toAddr,
Subject: 'New WSCC Membership',
TextBody: [
'New WSCC member:',
'',
'Name: ' + name,
'Type: ' + type,
'Amount: ' + amount,
'Email: ' + email,
'Phone: ' + phone,
'Address: ' + address,
].join('\n'),
});
}