Skip to content

Commit 8e3ae63

Browse files
authored
Merge pull request #420 from kernvalley/feature/419
Testing PR
2 parents c51511f + 1994bf1 commit 8e3ae63

File tree

8 files changed

+1063
-50
lines changed

8 files changed

+1063
-50
lines changed

_headers

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
Feature-Policy: geolocation 'self';
1616
Link: </css/index.min.css>; rel=preload; as=style; referrerpolicy=no-referrrer;
1717
Link: </js/index.min.js>; rel=preload; as=script; referrerpolicy=no-referrrer;
18-
Content-Security-Policy: default-src 'self'; img-src *; script-src 'self' cdn.kernvalley.us unpkg.com/ www.google-analytics.com www.googletagmanager.com cdn.polyfill.io/v3/polyfill.min.js; style-src 'self' cdn.kernvalley.us unpkg.com/; connect-src 'self' cdn.kernvalley.us api.kernvalley.us apps.kernvalley.us api.github.com/users/ api.openweathermap.org/data/2.5/weather www.google-analytics.com/ www.googletagmanager.com/gtag/; font-src cdn.kernvalley.us; media-src *; frame-src www.youtube-nocookie.com maps.kernvalley.us/embed; form-action 'self'; manifest-src 'self'; worker-src 'self'; reflected-xss block; upgrade-insecure-requests; block-all-mixed-content; disown-opener;
18+
Content-Security-Policy: default-src 'self'; img-src *; script-src 'self' cdn.kernvalley.us unpkg.com/ www.google-analytics.com www.googletagmanager.com cdn.polyfill.io/v3/polyfill.min.js js.stripe.com/v3/; style-src 'self' cdn.kernvalley.us unpkg.com/; connect-src 'self' cdn.kernvalley.us api.kernvalley.us apps.kernvalley.us api.github.com/users/ api.openweathermap.org/data/2.5/weather www.google-analytics.com/ www.googletagmanager.com/gtag/; font-src cdn.kernvalley.us; media-src *; frame-src www.youtube-nocookie.com maps.kernvalley.us/embed js.stripe.com/v3/; form-action 'self'; manifest-src 'self'; worker-src 'self'; reflected-xss block; upgrade-insecure-requests; block-all-mixed-content; disown-opener;
1919

2020
/reset
2121
Referrer-Policy: no-referrer

_redirects

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
https://whiskeyflat.netlify.com/* https://whiskeyflatdays.com/:splat 301!
22
http://whiskeyflat.netlify.com/* https://whiskeyflatdays.com/:splat 301!
33
/contact https://contact.kernvalley.us?subject=Whiskey%20Flat%20Days
4-
/manifest.json /webapp.webmanifest 301
5-
/maps/* /map/:splat 301
4+
/manifest.json /webapp.webmanifest 301
5+
/maps/* /map/:splat 301
6+
/api/* /.netlify/functions/:splat 200

api/stripe.js

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
/* eslint-env node */
2+
const methods = ['GET', 'POST', 'OPTIONS'];
3+
4+
async function calculateOrderAmount(items) {
5+
return 100 * items.length;
6+
}
7+
8+
exports.handler = async function handler(event) {
9+
switch(event.httpMethod) {
10+
case 'OPTIONS':
11+
return {
12+
statusCode: 204,
13+
headers: {
14+
Options: methods.join(', '),
15+
}
16+
};
17+
18+
case 'GET':
19+
if (typeof process.env.STRIPE_PUBLIC === 'string') {
20+
return {
21+
statusCode: 200,
22+
headers: { 'Content-Type': 'application/json' },
23+
body: JSON.stringify({
24+
key: process.env.STRIPE_PUBLIC,
25+
})
26+
};
27+
} else {
28+
return {
29+
statusCode: 500,
30+
headers: { 'Content-Type': 'application/json' },
31+
body: JSON.stringify({
32+
error: {
33+
message: 'Missing Stripe Public Key',
34+
status: 500,
35+
}
36+
})
37+
};
38+
}
39+
40+
case 'POST':
41+
console.log(event.headers['content-type']);
42+
if (event.headers['content-type'] !== 'application/json') {
43+
return {
44+
statusCode: 400,
45+
error: {
46+
message: 'Not JSON',
47+
status: 400,
48+
}
49+
};
50+
} else if (typeof process.env.STRIPE_SECRET === 'string') {
51+
try {
52+
const items = JSON.parse(event.body);
53+
54+
if (! Array.isArray(items) || items.length === 0) {
55+
throw new TypeError('Expected an array of items');
56+
}
57+
58+
const { Stripe } = await import('stripe');
59+
const stripe = Stripe(process.env.STRIPE_SECRET);
60+
const paymentIntent = await stripe.paymentIntents.create({
61+
amount: await calculateOrderAmount(items),
62+
currency: 'usd',
63+
automatic_payment_methods: {
64+
enabled: true,
65+
},
66+
});
67+
68+
return {
69+
statusCode: 200,
70+
headers: { 'Content-Type': 'application/json' },
71+
body: JSON.stringify({
72+
clientSecret: paymentIntent.client_secret,
73+
})
74+
};
75+
} catch(err) {
76+
return {
77+
statusCode: 500,
78+
headers: {
79+
'Content-Type': 'application/json',
80+
},
81+
body: JSON.stringify({
82+
error: {
83+
message: 'An unknown error occured',
84+
status: 500,
85+
},
86+
})
87+
};
88+
}
89+
} else {
90+
return {
91+
statusCode: 500,
92+
headers: { 'Content-Type': 'application/json' },
93+
body: JSON.stringify({
94+
error: {
95+
message: 'No Stripe API key set',
96+
status: 500,
97+
},
98+
})
99+
};
100+
}
101+
102+
default:
103+
return {
104+
statusCode: 405,
105+
headers: {
106+
'Content-Type': 'application/json',
107+
'Options': methods.join(', '),
108+
},
109+
body: JSON.stringify({
110+
error: {
111+
message: `Unsupported HTTP Method: ${event.httpMethod}`,
112+
status: 405,
113+
}
114+
})
115+
};
116+
}
117+
};

js/store.js

Lines changed: 59 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,65 @@
11
import { on, create } from 'https://cdn.kernvalley.us/js/std-js/dom.js';
2+
import { initElements } from './stripe.js';
23

3-
on('button.add-to-cart','click', () => {
4-
const dialog = create('dialog', {
5-
events: { close: ({ target }) => target.remove() },
6-
children: [
7-
create('p', {
8-
classList: ['status-box','info'],
9-
text: 'WFD Store is in demo mode. Purchases are currently not enabled.'
10-
}),
11-
create('div', {
12-
classList: ['center'],
13-
children: [
14-
create('button', {
15-
classList: ['btn', 'btn-reject'],
16-
text: 'Close',
17-
events: { click: ({ target }) => target.closest('dialog').close() },
18-
})
19-
]
20-
})
21-
],
22-
});
4+
async function getCart() {
5+
return [{ foo: 'bar' }];
6+
}
237

24-
document.body.append(dialog);
25-
dialog.showModal();
26-
});
8+
if (location.pathname.startsWith('/store/checkout')) {
9+
getCart().then(items => {
10+
initElements({
11+
items,
12+
base: 'payment-form',
13+
}).catch(console.error);
14+
});
15+
} else {
16+
on('button.add-to-cart','click', () => {
17+
const dialog = create('dialog', {
18+
events: { close: ({ target }) => target.remove() },
19+
children: [
20+
create('p', {
21+
classList: ['status-box','info'],
22+
text: 'WFD Store is in demo mode. Purchases are currently not enabled.'
23+
}),
24+
create('div', {
25+
classList: ['center'],
26+
children: [
27+
create('button', {
28+
classList: ['btn', 'btn-reject'],
29+
text: 'Close',
30+
events: { click: ({ target }) => target.closest('dialog').close() },
31+
})
32+
]
33+
})
34+
],
35+
});
2736

28-
on('.product-listing .product-img', 'click', ({ target }) => {
29-
const dialog = create('dialog', {
30-
events: { close: ({ target }) => target.remove() },
31-
children: [
32-
create('div', {
33-
classList: ['center'],
34-
children: [target.cloneNode()],
35-
}),
36-
create('div', {
37-
classList: ['center'],
38-
children: [
39-
create('button', {
40-
classList: ['btn', 'btn-reject'],
41-
text: 'Close',
42-
events: { click: ({ target }) => target.closest('dialog').close() },
43-
}),
44-
]
45-
})
46-
],
37+
document.body.append(dialog);
38+
dialog.showModal();
4739
});
4840

49-
document.body.append(dialog);
50-
dialog.showModal();
51-
});
41+
on('.product-listing .product-img', 'click', ({ target }) => {
42+
const dialog = create('dialog', {
43+
events: { close: ({ target }) => target.remove() },
44+
children: [
45+
create('div', {
46+
classList: ['center'],
47+
children: [target.cloneNode()],
48+
}),
49+
create('div', {
50+
classList: ['center'],
51+
children: [
52+
create('button', {
53+
classList: ['btn', 'btn-reject'],
54+
text: 'Close',
55+
events: { click: ({ target }) => target.closest('dialog').close() },
56+
}),
57+
]
58+
})
59+
],
60+
});
61+
62+
document.body.append(dialog);
63+
dialog.showModal();
64+
});
65+
}

js/stripe.js

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { getJSON } from 'https://cdn.kernvalley.us/js/std-js/http.js';
2+
import { loadScript } from 'https://cdn.kernvalley.us/js/std-js/loader.js';
3+
import { map } from 'https://cdn.kernvalley.us/js/std-js/dom.js';
4+
5+
const STRIPE = 'https://js.stripe.com/v3/';
6+
const ENDPOINT = '/api/stripe';
7+
8+
export const getStripeKey = (async ({ signal } = {}) => {
9+
const { key, error } = await getJSON(ENDPOINT, { signal });
10+
if (typeof error !== 'undefined') {
11+
throw new Error(error.message);
12+
} else if (typeof key !== 'string' || key.length === 0) {
13+
throw new Error('Error loading stripe key');
14+
} else {
15+
return key;
16+
}
17+
}).once();
18+
19+
export const getSecret = async (body = [{}], { signal } = {}) => {
20+
const resp = await fetch(ENDPOINT, {
21+
method: 'POST',
22+
headers: new Headers({ 'Content-Type': 'application/json' }),
23+
body: JSON.stringify(body),
24+
signal,
25+
});
26+
27+
if (resp.ok) {
28+
const { clientSecret } = await resp.json();
29+
return clientSecret;
30+
} else {
31+
throw new Error('Error creating payment request');
32+
}
33+
};
34+
35+
export const loadStripe = (() => loadScript(STRIPE)).once();
36+
37+
export const getStripe = (async ({ signal } = {}) => {
38+
const [key] = await Promise.all([getStripeKey({ signal }), loadStripe()]);
39+
40+
if (! ('Stripe' in globalThis)) {
41+
throw new Error('Stripe failed to load');
42+
} else {
43+
return globalThis.Stripe(key);
44+
}
45+
}).once();
46+
47+
export const getElements = (async (items = [], {
48+
appearance = { theme: 'stripe' },
49+
signal,
50+
} = {}) => {
51+
const [stripe, clientSecret] = await Promise.all([
52+
getStripe({ signal }),
53+
getSecret(items, { signal }),
54+
]);
55+
56+
return stripe.elements({ appearance, clientSecret });
57+
}).once();
58+
59+
export const createElement = async (elements, { type, selector, style, events }) => {
60+
const el = elements.create(type, { style });
61+
62+
if (typeof events === 'object' && ! Object.is(events, null)) {
63+
Object.entries(events).forEach(([event, callback]) => el.on(event, callback));
64+
}
65+
66+
el.mount(selector);
67+
return el;
68+
};
69+
70+
export const initElements = async ({
71+
base = document.body,
72+
items,
73+
events,
74+
styles,
75+
theme,
76+
signal,
77+
} = {}) => {
78+
const elements = await getElements(items, { theme, signal });
79+
80+
return await Promise.all(map('[data-stripe-element][id]', el => {
81+
const type = el.dataset.stripeElement;
82+
83+
return createElement(elements, {
84+
type: type,
85+
selector: `#${el.id}`,
86+
events: typeof events === 'undefined' ? undefined : events[type],
87+
style: typeof styles === 'undefined' ? undefined : styles[type],
88+
});
89+
}, { base: typeof base === 'string' ? document.forms[base] : base }));
90+
};

netlify.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
base = "./"
44
publish = "./_site"
55
command = "npm run build && npm run build:site"
6+
functions = "api"
67
[dev]
78
base = "./"
89
publish = "./_site"

0 commit comments

Comments
 (0)