Skip to content

Commit

Permalink
fix: when action property is set auto-POSTs payment info to URL
Browse files Browse the repository at this point in the history
Previously, using `action` sent url encoded form data, which was broken
(`[object Object]`). Now, it uses Fetch API to post JSON data.
  • Loading branch information
bennypowers committed Jan 21, 2020
1 parent 4d0a58d commit 35f0502
Show file tree
Hide file tree
Showing 10 changed files with 323 additions and 199 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ There are 11 properties for each state that you can set which will be read into

| Property | Attribute | Modifiers | Type | Default | Description |
|---------------------|--------------------|-----------|---------------------------------------|-----------|--------------------------------------------------|
| `action` | `action` | | `String` | | The URL to the form action. Example '/charges'.<br />If blank or undefined will not submit charges immediately. |
| `action` | `action` | | `string` | | If set, when Stripe returns the payment info (PaymentMethod, Source, or Token),<br />the element will POST JSON data to this URL with an object containing<br />a key equal to the value of the `generate` property. |
| `billingDetails` | | | `stripe.BillingDetails` | {} | billing_details object sent to create the payment representation. (optional) |
| `brand` | `brand` | readonly | `String` | null | The card brand detected by Stripe |
| `card` | `card` | readonly | `stripe.Element` | null | The Stripe card object. |
Expand Down
48 changes: 24 additions & 24 deletions custom-elements.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@
},
{
"name": "action",
"description": "The URL to the form action. Example '/charges'.\nIf blank or undefined will not submit charges immediately.",
"type": "String"
"description": "If set, when Stripe returns the payment info (PaymentMethod, Source, or Token),\nthe element will POST JSON data to this URL with an object containing\na key equal to the value of the `generate` property.",
"type": "string"
},
{
"name": "generate",
Expand Down Expand Up @@ -233,8 +233,8 @@
{
"name": "action",
"attribute": "action",
"description": "The URL to the form action. Example '/charges'.\nIf blank or undefined will not submit charges immediately.",
"type": "String"
"description": "If set, when Stripe returns the payment info (PaymentMethod, Source, or Token),\nthe element will POST JSON data to this URL with an object containing\na key equal to the value of the `generate` property.",
"type": "string"
},
{
"name": "generate",
Expand Down Expand Up @@ -317,6 +317,26 @@
"name": "stripe-ready-changed",
"description": "The new value of stripe-ready"
},
{
"name": "stripe-error",
"description": "The validation error, or the error returned from stripe.com"
},
{
"name": "stripe-payment-intent",
"description": "The PaymentIntent received from stripe.com"
},
{
"name": "stripe-payment-method",
"description": "The PaymentMethod received from stripe.com"
},
{
"name": "stripe-source",
"description": "The Source received from stripe.com"
},
{
"name": "stripe-token",
"description": "The Token received from stripe.com"
},
{
"name": "error-changed",
"description": "The new value of error"
Expand All @@ -341,26 +361,6 @@
"name": "source-changed",
"description": "The new value of source"
},
{
"name": "stripe-error",
"description": "The validation error, or the error returned from stripe.com"
},
{
"name": "stripe-payment-intent",
"description": "The PaymentIntent received from stripe.com"
},
{
"name": "stripe-payment-method",
"description": "The PaymentMethod received from stripe.com"
},
{
"name": "stripe-source",
"description": "The Source received from stripe.com"
},
{
"name": "stripe-token",
"description": "The Token received from stripe.com"
},
{
"name": "token-changed",
"description": "The new value of token"
Expand Down
55 changes: 42 additions & 13 deletions src/StripeBase.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,27 @@ import { LitNotify } from '@morbidick/lit-element-notify';
import bound from 'bound-decorator';

import { ReadOnlyPropertiesMixin } from './lib/read-only-properties';
import { camel, dash } from './lib/strings';
import { dash } from './lib/strings';
import { isRepresentation } from './lib/predicates';
import { stripeMethod } from './stripe-method-decorator';
import { throwBadResponse } from './lib/fetch';

/** @typedef {stripe.PaymentIntentResponse|stripe.PaymentMethodResponse|stripe.SetupIntentResponse|stripe.TokenResponse|stripe.SourceResponse} PaymentResponse */
/** @typedef {{ owner: stripe.OwnerData }} SourceData */

/**
* @fires 'stripe-error' - The validation error, or the error returned from stripe.com
* @fires 'stripe-payment-intent' - The PaymentIntent received from stripe.com
* @fires 'stripe-payment-method' - The PaymentMethod received from stripe.com
* @fires 'stripe-source' - The Source received from stripe.com
* @fires 'stripe-token' - The Token received from stripe.com
*
* @fires 'error-changed' - The new value of error
* @fires 'has-error-changed' - The new value of has-error
* @fires 'payment-intent-changed' - The new value of payment-intent
* @fires 'payment-method-changed' - The new value of payment-method
* @fires 'publishable-key-changed' - The new value of publishable-key
* @fires 'source-changed' - The new value of source
* @fires 'stripe-error' - The validation error, or the error returned from stripe.com
* @fires 'stripe-payment-intent' - The PaymentIntent received from stripe.com
* @fires 'stripe-payment-method' - The PaymentMethod received from stripe.com
* @fires 'stripe-source' - The Source received from stripe.com
* @fires 'stripe-token' - The Token received from stripe.com
* @fires 'token-changed' - The new value of token
*/
export class StripeBase extends ReadOnlyPropertiesMixin(LitNotify(LitElement)) {
Expand Down Expand Up @@ -83,9 +85,22 @@ export class StripeBase extends ReadOnlyPropertiesMixin(LitNotify(LitElement)) {
/* SETTINGS */

/**
* The URL to the form action. Example '/charges'.
* If blank or undefined will not submit charges immediately.
* @type {String}
* If set, when Stripe returns the payment info (PaymentMethod, Source, or Token),
* the element will POST JSON data to this URL with an object containing
* a key equal to the value of the `generate` property.
* @example
* ```html
* <stripe-elements
* publishable-key="pk_test_XXXXXXXXXXXXX"
* generate="token"
* action="/payment"
* ></stripe-elements>
* ```
* will POST to `/payment` with JSON body `{ "token": { ... } }`
* ```js
* stripeElements.submit();
* ```
* @type {string}
*/
@property({ type: String }) action;

Expand Down Expand Up @@ -281,6 +296,23 @@ export class StripeBase extends ReadOnlyPropertiesMixin(LitNotify(LitElement)) {
await this.set({ elements, error, stripe });
}

/**
* POSTs the payment info represenation to the endpoint at `/action`
* @private
*/
async postRepresentation() {
const token = this.token || undefined;
const source = this.source || undefined;
const paymentMethod = this.paymentMethod || undefined;
const body = JSON.stringify({ token, source, paymentMethod });
const headers = { 'Content-Type': 'application/json' };
const method = 'POST';
return fetch(this.action, { body, headers, method })
.then(throwBadResponse)
.then(success => this.fire('stripe-payment-success', success))
.catch(error => this.set({ error }));
}

/**
* @param {String} name
* @private
Expand All @@ -291,10 +323,7 @@ export class StripeBase extends ReadOnlyPropertiesMixin(LitNotify(LitElement)) {
/* istanbul ignore if */
if (!value) return;
this.fire(`stripe-${dash(name)}`, value);
const formField = this.form.querySelector(`[name=${camel(`stripe-${dash(name)}`)}]`);
formField.removeAttribute('disabled');
formField.value = value;
if (this.action) this.form.submit();
if (this.action) this.postRepresentation();
}

/** @private */
Expand Down
41 changes: 9 additions & 32 deletions src/StripeElements.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { LitNotify } from '@morbidick/lit-element-notify';
import { html, property } from 'lit-element';

import { ifDefined } from 'lit-html/directives/if-defined';
import { render } from 'lit-html';
import bound from 'bound-decorator';

import { StripeBase } from './StripeBase';
Expand All @@ -19,6 +18,9 @@ const removeAllMounts = host =>
const slotTemplate =
html`<slot slot="stripe-card" name="stripe-card"></slot>`;

const mountPointTemplate = ({ stripeMountId }) =>
html`<div id="${ifDefined(stripeMountId)}" class="stripe-elements-mount"></div>`;

const stripeElementsCustomCssTemplate = document.createElement('template');
stripeElementsCustomCssTemplate.id = 'stripe-elements-custom-css-properties';
stripeElementsCustomCssTemplate.innerHTML = `
Expand Down Expand Up @@ -56,15 +58,6 @@ function applyCustomCss() {
}
}

const stripeCardTemplate = ({ action, id, paymentMethod, source, token }) => html`
<form action="${ifDefined(action || undefined)}" method="post">
<div id="${ifDefined(id)}" class="stripe-mount" aria-label="Credit or Debit Card"></div>
<input ?disabled="${!paymentMethod}" type="hidden" name="stripePaymentMethod" value="${ifDefined(paymentMethod || undefined)}">
<input ?disabled="${!source}" type="hidden" name="stripeSource" value="${ifDefined(source || undefined)}">
<input ?disabled="${!token}" type="hidden" name="stripeToken" value="${ifDefined(token || undefined)}">
</form>
`;

const allowedStyles = [
'color',
'fontFamily',
Expand Down Expand Up @@ -304,19 +297,6 @@ export class StripeElements extends LitNotify(StripeBase) {
*/
shadowHosts = [];

/**
* The internal form element
* @type {HTMLFormElement}
* @protected
*/
get form() {
if (window.ShadyDOM) return this.querySelector('form');
let slot = this.shadowRoot.querySelector('slot');
// eslint-disable-next-line no-loops/no-loops
while (slot instanceof HTMLSlotElement && ([slot] = slot.assignedElements())) continue;
return slot.querySelector('form');
}

/**
* Stripe Element mount point
* @type {Element}
Expand Down Expand Up @@ -450,7 +430,7 @@ export class StripeElements extends LitNotify(StripeBase) {
this.shadowHosts = [this];
while (host = host.getRootNode().host) this.shadowHosts.push(host); // eslint-disable-line prefer-destructuring, no-loops/no-loops

const { shadowHosts, stripeMountId: id, action, token } = this;
const { shadowHosts, stripeMountId } = this;

// Prepare the shallowest breadcrumb slot at document level
const hosts = [...shadowHosts];
Expand All @@ -460,14 +440,11 @@ export class StripeElements extends LitNotify(StripeBase) {
div.slot = 'stripe-card';
root.appendChild(div);
}
const container = root.querySelector('[slot="stripe-card"]');

// hedge against shenanigans
const isDomCorrupt = container.querySelector('form') && !document.querySelector(`.stripe-mount[aria-label="Credit or Debit Card"]`);
const renderTemplate = isDomCorrupt ? render : appendTemplate;
const container = root.querySelector('[slot="stripe-card"]');

// Render the form to the document, so that Stripe.js can mount
renderTemplate(stripeCardTemplate({ action, id, token }), container);
appendTemplate(mountPointTemplate({ stripeMountId }), container);

// Append breadcrumb slots to each shadowroot in turn,
// from the document down to the <stripe-elements> instance.
Expand All @@ -479,9 +456,8 @@ export class StripeElements extends LitNotify(StripeBase) {
* @private
*/
initShadyDOMMount() {
const { action, token } = this;
const id = this.stripeMountId;
const mountTemplate = stripeCardTemplate({ action, id, token });
const { stripeMountId } = this;
const mountTemplate = mountPointTemplate({ stripeMountId });
appendTemplate(mountTemplate, this);
}

Expand All @@ -499,6 +475,7 @@ export class StripeElements extends LitNotify(StripeBase) {

await this.set({ element, card: element });

/* istanbul ignore if */
if (!this.stripeMount) throw new Error('Stripe Mount missing');

element.mount(this.stripeMount);
Expand Down
11 changes: 11 additions & 0 deletions src/lib/fetch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* Throws an error if the response is not OK.)
* @param {Response} response
* @resolves {Response}
* @rejects {Error}
*/
export async function throwBadResponse(response) {
const { ok, statusText } = response;
if (!ok) throw new Error(statusText);
return response;
}
Loading

0 comments on commit 35f0502

Please sign in to comment.