diff --git a/src/components/BookingBreakdown/BookingBreakdown.example.js b/src/components/BookingBreakdown/BookingBreakdown.example.js
index 5c8855c3fb..271536f584 100644
--- a/src/components/BookingBreakdown/BookingBreakdown.example.js
+++ b/src/components/BookingBreakdown/BookingBreakdown.example.js
@@ -6,7 +6,8 @@ import {
TRANSITION_COMPLETE,
TRANSITION_DECLINE,
TRANSITION_EXPIRE,
- TRANSITION_REQUEST,
+ TRANSITION_REQUEST_PAYMENT,
+ TRANSITION_CONFIRM_PAYMENT,
TX_TRANSITION_ACTOR_CUSTOMER,
} from '../../util/transaction';
import { LINE_ITEM_DAY, LINE_ITEM_NIGHT, LINE_ITEM_UNITS } from '../../util/types';
@@ -27,18 +28,24 @@ const exampleBooking = attributes => {
const exampleTransaction = params => {
const created = new Date(Date.UTC(2017, 1, 1));
+ const confirmed = new Date(Date.UTC(2017, 1, 1, 0, 1));
return {
id: new UUID('example-transaction'),
type: 'transaction',
attributes: {
createdAt: created,
lastTransitionedAt: created,
- lastTransition: TRANSITION_REQUEST,
+ lastTransition: TRANSITION_CONFIRM_PAYMENT,
transitions: [
{
createdAt: created,
by: TX_TRANSITION_ACTOR_CUSTOMER,
- transition: TRANSITION_REQUEST,
+ transition: TRANSITION_REQUEST_PAYMENT,
+ },
+ {
+ createdAt: confirmed,
+ by: TX_TRANSITION_ACTOR_CUSTOMER,
+ transition: TRANSITION_CONFIRM_PAYMENT,
},
],
@@ -205,7 +212,7 @@ export const ProviderSalePreauthorized = {
userRole: 'provider',
unitType: LINE_ITEM_NIGHT,
transaction: exampleTransaction({
- lastTransition: TRANSITION_REQUEST,
+ lastTransition: TRANSITION_CONFIRM_PAYMENT,
payinTotal: new Money(4500, CURRENCY),
payoutTotal: new Money(2500, CURRENCY),
lineItems: [
diff --git a/src/components/BookingBreakdown/BookingBreakdown.test.js b/src/components/BookingBreakdown/BookingBreakdown.test.js
index 90a953729e..8d797c2c9d 100644
--- a/src/components/BookingBreakdown/BookingBreakdown.test.js
+++ b/src/components/BookingBreakdown/BookingBreakdown.test.js
@@ -5,7 +5,7 @@ import { renderDeep } from '../../util/test-helpers';
import { types as sdkTypes } from '../../util/sdkLoader';
import {
TRANSITION_CANCEL,
- TRANSITION_REQUEST,
+ TRANSITION_REQUEST_PAYMENT,
TX_TRANSITION_ACTOR_CUSTOMER,
} from '../../util/transaction';
import { LINE_ITEM_NIGHT } from '../../util/types';
@@ -21,12 +21,12 @@ const exampleTransaction = params => {
attributes: {
createdAt: created,
lastTransitionedAt: created,
- lastTransition: TRANSITION_REQUEST,
+ lastTransition: TRANSITION_REQUEST_PAYMENT,
transitions: [
{
createdAt: created,
by: TX_TRANSITION_ACTOR_CUSTOMER,
- transition: TRANSITION_REQUEST,
+ transition: TRANSITION_REQUEST_PAYMENT,
},
],
diff --git a/src/components/TransactionPanel/PanelHeading.js b/src/components/TransactionPanel/PanelHeading.js
index b41275f9af..86a45dd054 100644
--- a/src/components/TransactionPanel/PanelHeading.js
+++ b/src/components/TransactionPanel/PanelHeading.js
@@ -7,6 +7,8 @@ import { NamedLink } from '../../components';
import css from './TransactionPanel.css';
export const HEADING_ENQUIRED = 'enquired';
+export const HEADING_PAYMENT_PENDING = 'pending-payment';
+export const HEADING_PAYMENT_EXPIRED = 'payment-expired';
export const HEADING_REQUESTED = 'requested';
export const HEADING_ACCEPTED = 'accepted';
export const HEADING_DECLINED = 'declined';
@@ -74,7 +76,7 @@ const CustomerBannedInfoMaybe = props => {
};
const HeadingProvider = props => {
- const { className, id, values, isCustomerBanned } = props;
+ const { className, id, values, isCustomerBanned, children } = props;
return (
@@ -82,6 +84,7 @@ const HeadingProvider = props => {
+ {children}
);
@@ -125,6 +128,45 @@ const PanelHeading = props => {
isCustomerBanned={isCustomerBanned}
/>
);
+ case HEADING_PAYMENT_PENDING:
+ return isCustomer ? (
+
+ ) : (
+
+
+
+
+
+ );
+ case HEADING_PAYMENT_EXPIRED:
+ return isCustomer ? (
+
+ ) : (
+
+ );
case HEADING_REQUESTED:
return isCustomer ? (
{
const txPreauthorized = createTransaction({
id: 'sale-preauthorized',
- lastTransition: TRANSITION_REQUEST,
+ lastTransition: TRANSITION_REQUEST_PAYMENT,
...baseTxAttrs,
});
@@ -92,7 +93,7 @@ describe('TransactionPanel - Sale', () => {
createTxTransition({
createdAt: new Date(Date.UTC(2017, 4, 1)),
by: 'customer',
- transition: TRANSITION_REQUEST,
+ transition: TRANSITION_REQUEST_PAYMENT,
}),
createTxTransition({
createdAt: new Date(Date.UTC(2017, 5, 1)),
@@ -197,7 +198,7 @@ describe('TransactionPanel - Sale', () => {
const transaction = createTransaction({
id: 'sale-tx',
- lastTransition: TRANSITION_REQUEST,
+ lastTransition: TRANSITION_REQUEST_PAYMENT,
total: new Money(16500, 'USD'),
commission: new Money(1000, 'USD'),
booking: createBooking('booking1', {
@@ -252,7 +253,7 @@ describe('TransactionPanel - Order', () => {
const txPreauthorized = createTransaction({
id: 'order-preauthorized',
- lastTransition: TRANSITION_REQUEST,
+ lastTransition: TRANSITION_CONFIRM_PAYMENT,
...baseTxAttrs,
});
@@ -287,7 +288,12 @@ describe('TransactionPanel - Order', () => {
createTxTransition({
createdAt: new Date(Date.UTC(2017, 4, 1)),
by: 'customer',
- transition: TRANSITION_REQUEST,
+ transition: TRANSITION_REQUEST_PAYMENT,
+ }),
+ createTxTransition({
+ createdAt: new Date(Date.UTC(2017, 4, 1, 0, 0, 1)),
+ by: 'customer',
+ transition: TRANSITION_CONFIRM_PAYMENT,
}),
createTxTransition({
createdAt: new Date(Date.UTC(2017, 5, 1)),
@@ -393,7 +399,7 @@ describe('TransactionPanel - Order', () => {
const end = new Date(Date.UTC(2017, 5, 13));
const tx = createTransaction({
id: 'order-tx',
- lastTransition: TRANSITION_REQUEST,
+ lastTransition: TRANSITION_REQUEST_PAYMENT,
total: new Money(16500, 'USD'),
booking: createBooking('booking1', {
start,
diff --git a/src/components/TransactionPanel/__snapshots__/TransactionPanel.test.js.snap b/src/components/TransactionPanel/__snapshots__/TransactionPanel.test.js.snap
index bc55429202..24d528229d 100644
--- a/src/components/TransactionPanel/__snapshots__/TransactionPanel.test.js.snap
+++ b/src/components/TransactionPanel/__snapshots__/TransactionPanel.test.js.snap
@@ -180,7 +180,12 @@ exports[`TransactionPanel - Order accepted matches snapshot 1`] = `
Object {
"by": "customer",
"createdAt": 2017-05-01T00:00:00.000Z,
- "transition": "transition/request",
+ "transition": "transition/request-payment",
+ },
+ Object {
+ "by": "customer",
+ "createdAt": 2017-05-01T00:00:01.000Z,
+ "transition": "transition/confirm-payment",
},
Object {
"by": "provider",
@@ -325,7 +330,12 @@ exports[`TransactionPanel - Order accepted matches snapshot 1`] = `
Object {
"by": "customer",
"createdAt": 2017-05-01T00:00:00.000Z,
- "transition": "transition/request",
+ "transition": "transition/request-payment",
+ },
+ Object {
+ "by": "customer",
+ "createdAt": 2017-05-01T00:00:01.000Z,
+ "transition": "transition/confirm-payment",
},
Object {
"by": "provider",
@@ -605,7 +615,12 @@ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor i
Object {
"by": "customer",
"createdAt": 2017-05-01T00:00:00.000Z,
- "transition": "transition/request",
+ "transition": "transition/request-payment",
+ },
+ Object {
+ "by": "customer",
+ "createdAt": 2017-05-01T00:00:01.000Z,
+ "transition": "transition/confirm-payment",
},
Object {
"by": "provider",
@@ -922,7 +937,12 @@ exports[`TransactionPanel - Order autodeclined matches snapshot 1`] = `
Object {
"by": "customer",
"createdAt": 2017-05-01T00:00:00.000Z,
- "transition": "transition/request",
+ "transition": "transition/request-payment",
+ },
+ Object {
+ "by": "customer",
+ "createdAt": 2017-05-01T00:00:01.000Z,
+ "transition": "transition/confirm-payment",
},
Object {
"by": "provider",
@@ -1067,7 +1087,12 @@ exports[`TransactionPanel - Order autodeclined matches snapshot 1`] = `
Object {
"by": "customer",
"createdAt": 2017-05-01T00:00:00.000Z,
- "transition": "transition/request",
+ "transition": "transition/request-payment",
+ },
+ Object {
+ "by": "customer",
+ "createdAt": 2017-05-01T00:00:01.000Z,
+ "transition": "transition/confirm-payment",
},
Object {
"by": "provider",
@@ -1346,7 +1371,12 @@ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor i
Object {
"by": "customer",
"createdAt": 2017-05-01T00:00:00.000Z,
- "transition": "transition/request",
+ "transition": "transition/request-payment",
+ },
+ Object {
+ "by": "customer",
+ "createdAt": 2017-05-01T00:00:01.000Z,
+ "transition": "transition/confirm-payment",
},
Object {
"by": "provider",
@@ -1664,7 +1694,12 @@ exports[`TransactionPanel - Order canceled matches snapshot 1`] = `
Object {
"by": "customer",
"createdAt": 2017-05-01T00:00:00.000Z,
- "transition": "transition/request",
+ "transition": "transition/request-payment",
+ },
+ Object {
+ "by": "customer",
+ "createdAt": 2017-05-01T00:00:01.000Z,
+ "transition": "transition/confirm-payment",
},
Object {
"by": "provider",
@@ -1809,7 +1844,12 @@ exports[`TransactionPanel - Order canceled matches snapshot 1`] = `
Object {
"by": "customer",
"createdAt": 2017-05-01T00:00:00.000Z,
- "transition": "transition/request",
+ "transition": "transition/request-payment",
+ },
+ Object {
+ "by": "customer",
+ "createdAt": 2017-05-01T00:00:01.000Z,
+ "transition": "transition/confirm-payment",
},
Object {
"by": "provider",
@@ -2089,7 +2129,12 @@ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor i
Object {
"by": "customer",
"createdAt": 2017-05-01T00:00:00.000Z,
- "transition": "transition/request",
+ "transition": "transition/request-payment",
+ },
+ Object {
+ "by": "customer",
+ "createdAt": 2017-05-01T00:00:01.000Z,
+ "transition": "transition/confirm-payment",
},
Object {
"by": "provider",
@@ -2406,7 +2451,12 @@ exports[`TransactionPanel - Order declined matches snapshot 1`] = `
Object {
"by": "customer",
"createdAt": 2017-05-01T00:00:00.000Z,
- "transition": "transition/request",
+ "transition": "transition/request-payment",
+ },
+ Object {
+ "by": "customer",
+ "createdAt": 2017-05-01T00:00:01.000Z,
+ "transition": "transition/confirm-payment",
},
Object {
"by": "provider",
@@ -2551,7 +2601,12 @@ exports[`TransactionPanel - Order declined matches snapshot 1`] = `
Object {
"by": "customer",
"createdAt": 2017-05-01T00:00:00.000Z,
- "transition": "transition/request",
+ "transition": "transition/request-payment",
+ },
+ Object {
+ "by": "customer",
+ "createdAt": 2017-05-01T00:00:01.000Z,
+ "transition": "transition/confirm-payment",
},
Object {
"by": "provider",
@@ -2830,7 +2885,12 @@ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor i
Object {
"by": "customer",
"createdAt": 2017-05-01T00:00:00.000Z,
- "transition": "transition/request",
+ "transition": "transition/request-payment",
+ },
+ Object {
+ "by": "customer",
+ "createdAt": 2017-05-01T00:00:01.000Z,
+ "transition": "transition/confirm-payment",
},
Object {
"by": "provider",
@@ -3148,7 +3208,12 @@ exports[`TransactionPanel - Order delivered matches snapshot 1`] = `
Object {
"by": "customer",
"createdAt": 2017-05-01T00:00:00.000Z,
- "transition": "transition/request",
+ "transition": "transition/request-payment",
+ },
+ Object {
+ "by": "customer",
+ "createdAt": 2017-05-01T00:00:01.000Z,
+ "transition": "transition/confirm-payment",
},
Object {
"by": "provider",
@@ -3298,7 +3363,12 @@ exports[`TransactionPanel - Order delivered matches snapshot 1`] = `
Object {
"by": "customer",
"createdAt": 2017-05-01T00:00:00.000Z,
- "transition": "transition/request",
+ "transition": "transition/request-payment",
+ },
+ Object {
+ "by": "customer",
+ "createdAt": 2017-05-01T00:00:01.000Z,
+ "transition": "transition/confirm-payment",
},
Object {
"by": "provider",
@@ -3583,7 +3653,12 @@ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor i
Object {
"by": "customer",
"createdAt": 2017-05-01T00:00:00.000Z,
- "transition": "transition/request",
+ "transition": "transition/request-payment",
+ },
+ Object {
+ "by": "customer",
+ "createdAt": 2017-05-01T00:00:01.000Z,
+ "transition": "transition/confirm-payment",
},
Object {
"by": "provider",
@@ -3905,7 +3980,12 @@ exports[`TransactionPanel - Order enquired matches snapshot 1`] = `
Object {
"by": "customer",
"createdAt": 2017-05-01T00:00:00.000Z,
- "transition": "transition/request",
+ "transition": "transition/request-payment",
+ },
+ Object {
+ "by": "customer",
+ "createdAt": 2017-05-01T00:00:01.000Z,
+ "transition": "transition/confirm-payment",
},
Object {
"by": "provider",
@@ -4050,7 +4130,12 @@ exports[`TransactionPanel - Order enquired matches snapshot 1`] = `
Object {
"by": "customer",
"createdAt": 2017-05-01T00:00:00.000Z,
- "transition": "transition/request",
+ "transition": "transition/request-payment",
+ },
+ Object {
+ "by": "customer",
+ "createdAt": 2017-05-01T00:00:01.000Z,
+ "transition": "transition/confirm-payment",
},
Object {
"by": "provider",
@@ -4328,7 +4413,12 @@ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor i
Object {
"by": "customer",
"createdAt": 2017-05-01T00:00:00.000Z,
- "transition": "transition/request",
+ "transition": "transition/request-payment",
+ },
+ Object {
+ "by": "customer",
+ "createdAt": 2017-05-01T00:00:01.000Z,
+ "transition": "transition/confirm-payment",
},
Object {
"by": "provider",
@@ -4591,7 +4681,7 @@ exports[`TransactionPanel - Order preauthorized matches snapshot 1`] = `
Object {
"attributes": Object {
"createdAt": 2017-05-01T00:00:00.000Z,
- "lastTransition": "transition/request",
+ "lastTransition": "transition/confirm-payment",
"lastTransitionedAt": 2017-06-01T00:00:00.000Z,
"lineItems": Array [
Object {
@@ -4645,7 +4735,12 @@ exports[`TransactionPanel - Order preauthorized matches snapshot 1`] = `
Object {
"by": "customer",
"createdAt": 2017-05-01T00:00:00.000Z,
- "transition": "transition/request",
+ "transition": "transition/request-payment",
+ },
+ Object {
+ "by": "customer",
+ "createdAt": 2017-05-01T00:00:01.000Z,
+ "transition": "transition/confirm-payment",
},
Object {
"by": "provider",
@@ -4736,7 +4831,7 @@ exports[`TransactionPanel - Order preauthorized matches snapshot 1`] = `
Object {
"attributes": Object {
"createdAt": 2017-05-01T00:00:00.000Z,
- "lastTransition": "transition/request",
+ "lastTransition": "transition/confirm-payment",
"lastTransitionedAt": 2017-06-01T00:00:00.000Z,
"lineItems": Array [
Object {
@@ -4790,7 +4885,12 @@ exports[`TransactionPanel - Order preauthorized matches snapshot 1`] = `
Object {
"by": "customer",
"createdAt": 2017-05-01T00:00:00.000Z,
- "transition": "transition/request",
+ "transition": "transition/request-payment",
+ },
+ Object {
+ "by": "customer",
+ "createdAt": 2017-05-01T00:00:01.000Z,
+ "transition": "transition/confirm-payment",
},
Object {
"by": "provider",
@@ -5015,7 +5115,7 @@ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor i
Object {
"attributes": Object {
"createdAt": 2017-05-01T00:00:00.000Z,
- "lastTransition": "transition/request",
+ "lastTransition": "transition/confirm-payment",
"lastTransitionedAt": 2017-06-01T00:00:00.000Z,
"lineItems": Array [
Object {
@@ -5069,7 +5169,12 @@ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor i
Object {
"by": "customer",
"createdAt": 2017-05-01T00:00:00.000Z,
- "transition": "transition/request",
+ "transition": "transition/request-payment",
+ },
+ Object {
+ "by": "customer",
+ "createdAt": 2017-05-01T00:00:01.000Z,
+ "transition": "transition/confirm-payment",
},
Object {
"by": "provider",
@@ -5387,7 +5492,12 @@ exports[`TransactionPanel - Sale accepted matches snapshot 1`] = `
Object {
"by": "customer",
"createdAt": 2017-05-01T00:00:00.000Z,
- "transition": "transition/request",
+ "transition": "transition/request-payment",
+ },
+ Object {
+ "by": "customer",
+ "createdAt": 2017-05-01T00:00:01.000Z,
+ "transition": "transition/confirm-payment",
},
Object {
"by": "provider",
@@ -5532,7 +5642,12 @@ exports[`TransactionPanel - Sale accepted matches snapshot 1`] = `
Object {
"by": "customer",
"createdAt": 2017-05-01T00:00:00.000Z,
- "transition": "transition/request",
+ "transition": "transition/request-payment",
+ },
+ Object {
+ "by": "customer",
+ "createdAt": 2017-05-01T00:00:01.000Z,
+ "transition": "transition/confirm-payment",
},
Object {
"by": "provider",
@@ -5812,7 +5927,12 @@ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor i
Object {
"by": "customer",
"createdAt": 2017-05-01T00:00:00.000Z,
- "transition": "transition/request",
+ "transition": "transition/request-payment",
+ },
+ Object {
+ "by": "customer",
+ "createdAt": 2017-05-01T00:00:01.000Z,
+ "transition": "transition/confirm-payment",
},
Object {
"by": "provider",
@@ -6129,7 +6249,12 @@ exports[`TransactionPanel - Sale autodeclined matches snapshot 1`] = `
Object {
"by": "customer",
"createdAt": 2017-05-01T00:00:00.000Z,
- "transition": "transition/request",
+ "transition": "transition/request-payment",
+ },
+ Object {
+ "by": "customer",
+ "createdAt": 2017-05-01T00:00:01.000Z,
+ "transition": "transition/confirm-payment",
},
Object {
"by": "provider",
@@ -6274,7 +6399,12 @@ exports[`TransactionPanel - Sale autodeclined matches snapshot 1`] = `
Object {
"by": "customer",
"createdAt": 2017-05-01T00:00:00.000Z,
- "transition": "transition/request",
+ "transition": "transition/request-payment",
+ },
+ Object {
+ "by": "customer",
+ "createdAt": 2017-05-01T00:00:01.000Z,
+ "transition": "transition/confirm-payment",
},
Object {
"by": "provider",
@@ -6553,7 +6683,12 @@ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor i
Object {
"by": "customer",
"createdAt": 2017-05-01T00:00:00.000Z,
- "transition": "transition/request",
+ "transition": "transition/request-payment",
+ },
+ Object {
+ "by": "customer",
+ "createdAt": 2017-05-01T00:00:01.000Z,
+ "transition": "transition/confirm-payment",
},
Object {
"by": "provider",
@@ -6871,7 +7006,12 @@ exports[`TransactionPanel - Sale canceled matches snapshot 1`] = `
Object {
"by": "customer",
"createdAt": 2017-05-01T00:00:00.000Z,
- "transition": "transition/request",
+ "transition": "transition/request-payment",
+ },
+ Object {
+ "by": "customer",
+ "createdAt": 2017-05-01T00:00:01.000Z,
+ "transition": "transition/confirm-payment",
},
Object {
"by": "provider",
@@ -7016,7 +7156,12 @@ exports[`TransactionPanel - Sale canceled matches snapshot 1`] = `
Object {
"by": "customer",
"createdAt": 2017-05-01T00:00:00.000Z,
- "transition": "transition/request",
+ "transition": "transition/request-payment",
+ },
+ Object {
+ "by": "customer",
+ "createdAt": 2017-05-01T00:00:01.000Z,
+ "transition": "transition/confirm-payment",
},
Object {
"by": "provider",
@@ -7296,7 +7441,12 @@ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor i
Object {
"by": "customer",
"createdAt": 2017-05-01T00:00:00.000Z,
- "transition": "transition/request",
+ "transition": "transition/request-payment",
+ },
+ Object {
+ "by": "customer",
+ "createdAt": 2017-05-01T00:00:01.000Z,
+ "transition": "transition/confirm-payment",
},
Object {
"by": "provider",
@@ -7613,7 +7763,12 @@ exports[`TransactionPanel - Sale declined matches snapshot 1`] = `
Object {
"by": "customer",
"createdAt": 2017-05-01T00:00:00.000Z,
- "transition": "transition/request",
+ "transition": "transition/request-payment",
+ },
+ Object {
+ "by": "customer",
+ "createdAt": 2017-05-01T00:00:01.000Z,
+ "transition": "transition/confirm-payment",
},
Object {
"by": "provider",
@@ -7758,7 +7913,12 @@ exports[`TransactionPanel - Sale declined matches snapshot 1`] = `
Object {
"by": "customer",
"createdAt": 2017-05-01T00:00:00.000Z,
- "transition": "transition/request",
+ "transition": "transition/request-payment",
+ },
+ Object {
+ "by": "customer",
+ "createdAt": 2017-05-01T00:00:01.000Z,
+ "transition": "transition/confirm-payment",
},
Object {
"by": "provider",
@@ -8037,7 +8197,12 @@ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor i
Object {
"by": "customer",
"createdAt": 2017-05-01T00:00:00.000Z,
- "transition": "transition/request",
+ "transition": "transition/request-payment",
+ },
+ Object {
+ "by": "customer",
+ "createdAt": 2017-05-01T00:00:01.000Z,
+ "transition": "transition/confirm-payment",
},
Object {
"by": "provider",
@@ -8355,7 +8520,7 @@ exports[`TransactionPanel - Sale delivered matches snapshot 1`] = `
Object {
"by": "customer",
"createdAt": 2017-05-01T00:00:00.000Z,
- "transition": "transition/request",
+ "transition": "transition/request-payment",
},
Object {
"by": "provider",
@@ -8505,7 +8670,7 @@ exports[`TransactionPanel - Sale delivered matches snapshot 1`] = `
Object {
"by": "customer",
"createdAt": 2017-05-01T00:00:00.000Z,
- "transition": "transition/request",
+ "transition": "transition/request-payment",
},
Object {
"by": "provider",
@@ -8790,7 +8955,7 @@ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor i
Object {
"by": "customer",
"createdAt": 2017-05-01T00:00:00.000Z,
- "transition": "transition/request",
+ "transition": "transition/request-payment",
},
Object {
"by": "provider",
@@ -9112,7 +9277,12 @@ exports[`TransactionPanel - Sale enquired matches snapshot 1`] = `
Object {
"by": "customer",
"createdAt": 2017-05-01T00:00:00.000Z,
- "transition": "transition/request",
+ "transition": "transition/request-payment",
+ },
+ Object {
+ "by": "customer",
+ "createdAt": 2017-05-01T00:00:01.000Z,
+ "transition": "transition/confirm-payment",
},
Object {
"by": "provider",
@@ -9257,7 +9427,12 @@ exports[`TransactionPanel - Sale enquired matches snapshot 1`] = `
Object {
"by": "customer",
"createdAt": 2017-05-01T00:00:00.000Z,
- "transition": "transition/request",
+ "transition": "transition/request-payment",
+ },
+ Object {
+ "by": "customer",
+ "createdAt": 2017-05-01T00:00:01.000Z,
+ "transition": "transition/confirm-payment",
},
Object {
"by": "provider",
@@ -9535,7 +9710,12 @@ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor i
Object {
"by": "customer",
"createdAt": 2017-05-01T00:00:00.000Z,
- "transition": "transition/request",
+ "transition": "transition/request-payment",
+ },
+ Object {
+ "by": "customer",
+ "createdAt": 2017-05-01T00:00:01.000Z,
+ "transition": "transition/confirm-payment",
},
Object {
"by": "provider",
@@ -9743,7 +9923,7 @@ exports[`TransactionPanel - Sale preauthorized matches snapshot 1`] = `
listingDeleted={false}
listingId="listing1"
listingTitle="listing1 title"
- panelHeadingState="requested"
+ panelHeadingState="pending-payment"
providerName={
({
const initiateOrderRequest = () => ({ type: INITIATE_ORDER_REQUEST });
-const initiateOrderSuccess = orderId => ({
+const initiateOrderSuccess = order => ({
type: INITIATE_ORDER_SUCCESS,
- payload: orderId,
+ payload: order,
});
const initiateOrderError = e => ({
@@ -92,6 +110,19 @@ const initiateOrderError = e => ({
payload: e,
});
+const confirmPaymentRequest = () => ({ type: CONFIRM_PAYMENT_REQUEST });
+
+const confirmPaymentSuccess = orderId => ({
+ type: CONFIRM_PAYMENT_SUCCESS,
+ payload: orderId,
+});
+
+const confirmPaymentError = e => ({
+ type: CONFIRM_PAYMENT_ERROR,
+ error: true,
+ payload: e,
+});
+
export const speculateTransactionRequest = () => ({ type: SPECULATE_TRANSACTION_REQUEST });
export const speculateTransactionSuccess = transaction => ({
@@ -107,37 +138,39 @@ export const speculateTransactionError = e => ({
/* ================ Thunks ================ */
-export const initiateOrder = (orderParams, initialMessage) => (dispatch, getState, sdk) => {
+export const initiateOrder = (orderParams, transactionId) => (dispatch, getState, sdk) => {
dispatch(initiateOrderRequest());
- const bodyParams = {
- transition: TRANSITION_REQUEST,
- processAlias: config.bookingProcessAlias,
- params: orderParams,
+ const bodyParams = transactionId
+ ? {
+ id: transactionId,
+ transition: TRANSITION_REQUEST_PAYMENT_AFTER_ENQUIRY,
+ params: orderParams,
+ }
+ : {
+ processAlias: config.bookingProcessAlias,
+ transition: TRANSITION_REQUEST_PAYMENT,
+ params: orderParams,
+ };
+ const queryParams = {
+ include: ['booking', 'provider'],
+ expand: true,
};
- return sdk.transactions
- .initiate(bodyParams)
+
+ const createOrder = transactionId ? sdk.transactions.transition : sdk.transactions.initiate;
+
+ return createOrder(bodyParams, queryParams)
.then(response => {
- const orderId = response.data.data.id;
- dispatch(initiateOrderSuccess(orderId));
+ const entities = denormalisedResponseEntities(response);
+ const order = entities[0];
+ dispatch(initiateOrderSuccess(order));
dispatch(fetchCurrentUserHasOrdersSuccess(true));
-
- if (initialMessage) {
- return sdk.messages
- .send({ transactionId: orderId, content: initialMessage })
- .then(() => {
- return { orderId, initialMessageSuccess: true };
- })
- .catch(e => {
- log.error(e, 'initial-message-send-failed', { txId: orderId });
- return { orderId, initialMessageSuccess: false };
- });
- } else {
- return Promise.resolve({ orderId, initialMessageSuccess: true });
- }
+ return order;
})
.catch(e => {
dispatch(initiateOrderError(storableError(e)));
+ const transactionIdMaybe = transactionId ? { transactionId: transactionId.uuid } : {};
log.error(e, 'initiate-order-failed', {
+ ...transactionIdMaybe,
listingId: orderParams.listingId.uuid,
bookingStart: orderParams.bookingStart,
bookingEnd: orderParams.bookingEnd,
@@ -146,43 +179,53 @@ export const initiateOrder = (orderParams, initialMessage) => (dispatch, getStat
});
};
-/**
- * Initiate an order after an enquiry. Transitions previously created transaction.
- */
-export const initiateOrderAfterEnquiry = (transactionId, orderParams) => (
- dispatch,
- getState,
- sdk
-) => {
- dispatch(initiateOrderRequest());
+export const confirmPayment = orderParams => (dispatch, getState, sdk) => {
+ dispatch(confirmPaymentRequest());
const bodyParams = {
- id: transactionId,
- transition: TRANSITION_REQUEST_AFTER_ENQUIRY,
- params: orderParams,
+ id: orderParams.transactionId,
+ transition: TRANSITION_CONFIRM_PAYMENT,
+ params: {},
};
return sdk.transactions
.transition(bodyParams)
.then(response => {
- const orderId = response.data.data.id;
- dispatch(initiateOrderSuccess(orderId));
- dispatch(fetchCurrentUserHasOrdersSuccess(true));
- // set initialMessageSuccess to true to unify promise handling with initiateOrder
- return Promise.resolve({ orderId, initialMessageSuccess: true });
+ const order = response.data.data;
+ dispatch(confirmPaymentSuccess(order.id));
+ return order;
})
.catch(e => {
- dispatch(initiateOrderError(storableError(e)));
+ dispatch(confirmPaymentError(storableError(e)));
+ const transactionIdMaybe = orderParams.transactionId
+ ? { transactionId: orderParams.transactionId.uuid }
+ : {};
log.error(e, 'initiate-order-failed', {
- transactionId: transactionId.uuid,
- listingId: orderParams.listingId.uuid,
- bookingStart: orderParams.bookingStart,
- bookingEnd: orderParams.bookingEnd,
+ ...transactionIdMaybe,
});
throw e;
});
};
+export const sendMessage = params => (dispatch, getState, sdk) => {
+ const message = params.message;
+ const orderId = params.id;
+
+ if (message) {
+ return sdk.messages
+ .send({ transactionId: orderId, content: message })
+ .then(() => {
+ return { orderId, messageSuccess: true };
+ })
+ .catch(e => {
+ log.error(e, 'initial-message-send-failed', { txId: orderId });
+ return { orderId, messageSuccess: false };
+ });
+ } else {
+ return Promise.resolve({ orderId, messageSuccess: true });
+ }
+};
+
/**
* Initiate the speculative transaction with the given booking details
*
@@ -198,7 +241,7 @@ export const initiateOrderAfterEnquiry = (transactionId, orderParams) => (
export const speculateTransaction = params => (dispatch, getState, sdk) => {
dispatch(speculateTransactionRequest());
const bodyParams = {
- transition: TRANSITION_REQUEST,
+ transition: TRANSITION_REQUEST_PAYMENT,
processAlias: config.bookingProcessAlias,
params: {
...params,
diff --git a/src/containers/CheckoutPage/CheckoutPage.js b/src/containers/CheckoutPage/CheckoutPage.js
index 680f9faf9e..14dd651e3b 100644
--- a/src/containers/CheckoutPage/CheckoutPage.js
+++ b/src/containers/CheckoutPage/CheckoutPage.js
@@ -1,15 +1,22 @@
import React, { Component } from 'react';
-import { bool, func, instanceOf, object, shape, string } from 'prop-types';
+import { bool, func, instanceOf, object, oneOfType, shape, string } from 'prop-types';
import { compose } from 'redux';
import { connect } from 'react-redux';
import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
import { withRouter } from 'react-router-dom';
import classNames from 'classnames';
+import config from '../../config';
import routeConfiguration from '../../routeConfiguration';
import { pathByRouteName, findRouteByRouteName } from '../../util/routes';
import { propTypes, LINE_ITEM_NIGHT, LINE_ITEM_DAY } from '../../util/types';
-import { ensureListing, ensureUser, ensureTransaction, ensureBooking } from '../../util/data';
-import { dateFromLocalToAPI } from '../../util/dates';
+import {
+ ensureListing,
+ ensureCurrentUser,
+ ensureUser,
+ ensureTransaction,
+ ensureBooking,
+} from '../../util/data';
+import { dateFromLocalToAPI, minutesBetween } from '../../util/dates';
import { createSlug } from '../../util/urlHelpers';
import {
isTransactionInitiateAmountTooLowError,
@@ -21,6 +28,7 @@ import {
transactionInitiateOrderStripeErrors,
} from '../../util/errors';
import { formatMoney } from '../../util/currency';
+import { TRANSITION_ENQUIRE, txIsPaymentPending, txIsPaymentExpired } from '../../util/transaction';
import {
AvatarMedium,
BookingBreakdown,
@@ -32,20 +40,40 @@ import {
} from '../../components';
import { StripePaymentForm } from '../../forms';
import { isScrollingDisabled } from '../../ducks/UI.duck';
+import { handleCardPayment, retrievePaymentIntent } from '../../ducks/stripe.duck.js';
+
import {
initiateOrder,
- initiateOrderAfterEnquiry,
setInitialValues,
speculateTransaction,
+ confirmPayment,
+ sendMessage,
} from './CheckoutPage.duck';
-import { createStripePaymentToken, clearStripePaymentToken } from '../../ducks/stripe.duck.js';
-import config from '../../config';
-
import { storeData, storedData, clearData } from './CheckoutPageSessionHelpers';
import css from './CheckoutPage.css';
const STORAGE_KEY = 'CheckoutPage';
+// Stripe PaymentIntent statuses, where user actions are already completed
+// https://stripe.com/docs/payments/payment-intents/status
+const STRIPE_PI_USER_ACTIONS_DONE_STATUSES = ['processing', 'requires_capture', 'succeeded'];
+
+const initializeOrderPage = (initialValues, routes, dispatch) => {
+ const OrderPage = findRouteByRouteName('OrderDetailsPage', routes);
+
+ // Transaction is already created, but if the initial message
+ // sending failed, we tell it to the OrderDetailsPage.
+ dispatch(OrderPage.setInitialValues(initialValues));
+};
+
+const checkIsPaymentExpired = existingTransaction => {
+ return txIsPaymentExpired(existingTransaction)
+ ? true
+ : txIsPaymentPending(existingTransaction)
+ ? minutesBetween(existingTransaction.attributes.lastTransitionedAt, new Date()) >= 15
+ : false;
+};
+
export class CheckoutPageComponent extends Component {
constructor(props) {
super(props);
@@ -55,8 +83,11 @@ export class CheckoutPageComponent extends Component {
dataLoaded: false,
submitting: false,
};
+ this.stripe = null;
+ this.onStripeInitialized = this.onStripeInitialized.bind(this);
this.loadInitialData = this.loadInitialData.bind(this);
+ this.handlePaymentIntent = this.handlePaymentIntent.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
@@ -87,7 +118,7 @@ export class CheckoutPageComponent extends Component {
bookingData,
bookingDates,
listing,
- enquiredTransaction,
+ transaction,
fetchSpeculatedTransaction,
history,
} = this.props;
@@ -100,24 +131,29 @@ export class CheckoutPageComponent extends Component {
const hasDataInProps = !!(bookingData && bookingDates && listing) && hasNavigatedThroughLink;
if (hasDataInProps) {
// Store data only if data is passed through props and user has navigated through a link.
- storeData(bookingData, bookingDates, listing, enquiredTransaction, STORAGE_KEY);
+ storeData(bookingData, bookingDates, listing, transaction, STORAGE_KEY);
}
// NOTE: stored data can be empty if user has already successfully completed transaction.
const pageData = hasDataInProps
- ? { bookingData, bookingDates, listing, enquiredTransaction }
+ ? { bookingData, bookingDates, listing, transaction }
: storedData(STORAGE_KEY);
- const hasData =
+ // Check if a booking is already created according to stored data.
+ const tx = pageData ? pageData.transaction : null;
+ const isBookingCreated = tx && tx.booking && tx.booking.id;
+
+ const shouldFetchSpeculatedTransaction =
pageData &&
pageData.listing &&
pageData.listing.id &&
pageData.bookingData &&
pageData.bookingDates &&
pageData.bookingDates.bookingStart &&
- pageData.bookingDates.bookingEnd;
+ pageData.bookingDates.bookingEnd &&
+ !isBookingCreated;
- if (hasData) {
+ if (shouldFetchSpeculatedTransaction) {
const listingId = pageData.listing.id;
const { bookingStart, bookingEnd } = pageData.bookingDates;
@@ -139,81 +175,209 @@ export class CheckoutPageComponent extends Component {
this.setState({ pageData: pageData || {}, dataLoaded: true });
}
- handleSubmit(values) {
- if (this.state.submitting) {
- return;
- }
- this.setState({ submitting: true });
+ handlePaymentIntent(handlePaymentParams) {
+ const { onInitiateOrder, onHandleCardPayment, onConfirmPayment, onSendMessage } = this.props;
+ const { pageData, speculatedTransaction, message } = handlePaymentParams;
+ const storedTx = ensureTransaction(pageData.transaction);
- const cardToken = values.token;
- const initialMessage = values.message;
- const {
- history,
- sendOrderRequest,
- sendOrderRequestAfterEnquiry,
- speculatedTransaction,
- onClearStripePaymentToken,
- dispatch,
- } = this.props;
+ // Step 1: initiate order by requesting payment from Marketplace API
+ const fnRequestPayment = fnParams => {
+ // fnParams should be { listingId, bookingStart, bookingEnd }
+ const hasPaymentIntents =
+ storedTx.attributes.protectedData && storedTx.attributes.protectedData.stripePaymentIntents;
+
+ // If paymentIntent exists, order has been initiated previously.
+ return hasPaymentIntents ? Promise.resolve(storedTx) : onInitiateOrder(fnParams, storedTx.id);
+ };
+
+ // Step 2: pay using Stripe SDK
+ const fnHandleCardPayment = fnParams => {
+ // fnParams should be returned transaction entity
+
+ const order = ensureTransaction(fnParams);
+ if (order.id) {
+ // Store order.
+ const { bookingData, bookingDates, listing } = pageData;
+ storeData(bookingData, bookingDates, listing, order, STORAGE_KEY);
+ this.setState({ pageData: { ...pageData, transaction: order } });
+ }
+
+ const hasPaymentIntents =
+ order.attributes.protectedData && order.attributes.protectedData.stripePaymentIntents;
+
+ if (!hasPaymentIntents) {
+ throw new Error(
+ `Missing StripePaymentIntents key in transaction's protectedData. Check that your transaction process is configured to use payment intents.`
+ );
+ }
+
+ const { stripePaymentIntentClientSecret } = hasPaymentIntents
+ ? order.attributes.protectedData.stripePaymentIntents.default
+ : null;
+
+ const { stripe, card, billingDetails, paymentIntent } = handlePaymentParams;
+
+ const params = {
+ stripePaymentIntentClientSecret,
+ orderId: order.id,
+ stripe,
+ card,
+ paymentParams: {
+ payment_method_data: {
+ billing_details: billingDetails,
+ },
+ },
+ };
+
+ // If paymentIntent status is not waiting user action,
+ // handleCardPayment has been called previously.
+ const hasPaymentIntentUserActionsDone =
+ paymentIntent && STRIPE_PI_USER_ACTIONS_DONE_STATUSES.includes(paymentIntent.status);
+ return hasPaymentIntentUserActionsDone
+ ? Promise.resolve({ transactionId: order.id, paymentIntent })
+ : onHandleCardPayment(params);
+ };
+
+ // Step 3: complete order by confirming payment to Marketplace API
+ // Parameter should contain { paymentIntent, transactionId } returned in step 2
+ const fnConfirmPayment = onConfirmPayment;
+
+ // Step 4: send initial message
+ const fnSendMessage = fnParams => {
+ return onSendMessage({ ...fnParams, message });
+ };
+
+ // Here we create promise calls in sequence
+ // This is pretty much the same as:
+ // fnRequestPayment({...initialParams})
+ // .then(result => fnHandleCardPayment({...result}))
+ // .then(result => fnConfirmPayment({...result}))
+ const applyAsync = (acc, val) => acc.then(val);
+ const composeAsync = (...funcs) => x => funcs.reduce(applyAsync, Promise.resolve(x));
+ const handlePaymentIntentCreation = composeAsync(
+ fnRequestPayment,
+ fnHandleCardPayment,
+ fnConfirmPayment,
+ fnSendMessage
+ );
// Create order aka transaction
// NOTE: if unit type is line-item/units, quantity needs to be added.
// The way to pass it to checkout page is through pageData.bookingData
- const requestParams = {
- listingId: this.state.pageData.listing.id,
- cardToken,
- bookingStart: speculatedTransaction.booking.attributes.start,
- bookingEnd: speculatedTransaction.booking.attributes.end,
+ const tx = speculatedTransaction ? speculatedTransaction : storedTx;
+ const orderParams = {
+ listingId: pageData.listing.id,
+ bookingStart: tx.booking.attributes.start,
+ bookingEnd: tx.booking.attributes.end,
};
- const enquiredTransaction = this.state.pageData.enquiredTransaction;
+ return handlePaymentIntentCreation(orderParams);
+ }
- // if an enquired transaction is available, use that as basis
- // otherwise initiate a new transaction
- const initiateRequest = enquiredTransaction
- ? sendOrderRequestAfterEnquiry(enquiredTransaction.id, requestParams)
- : sendOrderRequest(requestParams, initialMessage);
+ handleSubmit(values) {
+ if (this.state.submitting) {
+ return;
+ }
+ this.setState({ submitting: true });
+
+ const { history, speculatedTransaction, currentUser, paymentIntent, dispatch } = this.props;
+ const { card, message, formValues } = values;
+ const { name, addressLine1, addressLine2, postal, city, state, country } = formValues;
+
+ // Billing address is recommended.
+ // However, let's not assume that data is among formValues.
+ // Read more about this from Stripe's docs
+ // https://stripe.com/docs/stripe-js/reference#stripe-handle-card-payment-no-element
+ const addressMaybe =
+ addressLine1 && postal
+ ? {
+ address: {
+ city: city,
+ country: country,
+ line1: addressLine1,
+ line2: addressLine2,
+ postal_code: postal,
+ state: state,
+ },
+ }
+ : {};
+ const billingDetails = {
+ name,
+ email: ensureCurrentUser(currentUser).attributes.email,
+ ...addressMaybe,
+ };
- initiateRequest
- .then(values => {
- const { orderId, initialMessageSuccess } = values;
+ const requestPaymentParams = {
+ pageData: this.state.pageData,
+ speculatedTransaction,
+ stripe: this.stripe,
+ card,
+ billingDetails,
+ message,
+ paymentIntent,
+ };
+
+ this.handlePaymentIntent(requestPaymentParams)
+ .then(res => {
+ const { orderId, messageSuccess } = res;
this.setState({ submitting: false });
+
const routes = routeConfiguration();
- const OrderPage = findRouteByRouteName('OrderDetailsPage', routes);
-
- // Transaction is already created, but if the initial message
- // sending failed, we tell it to the OrderDetailsPage.
- dispatch(
- OrderPage.setInitialValues({
- initialMessageFailedToTransaction: initialMessageSuccess ? null : orderId,
- })
- );
- const orderDetailsPath = pathByRouteName('OrderDetailsPage', routes, {
- id: orderId.uuid,
- });
- onClearStripePaymentToken();
+ const initialMessageFailedToTransaction = messageSuccess ? null : orderId;
+ const orderDetailsPath = pathByRouteName('OrderDetailsPage', routes, { id: orderId.uuid });
+
+ initializeOrderPage({ initialMessageFailedToTransaction }, routes, dispatch);
clearData(STORAGE_KEY);
history.push(orderDetailsPath);
})
- .catch(() => {
+ .catch(err => {
+ console.error(err);
this.setState({ submitting: false });
});
}
+ onStripeInitialized(stripe) {
+ this.stripe = stripe;
+
+ const { paymentIntent, onRetrievePaymentIntent } = this.props;
+ const tx = this.state.pageData ? this.state.pageData.transaction : null;
+
+ // We need to get up to date PI, if booking is created but payment is not expired.
+ const shouldFetchPaymentIntent =
+ this.stripe &&
+ !paymentIntent &&
+ tx &&
+ tx.id &&
+ tx.booking &&
+ tx.booking.id &&
+ txIsPaymentPending(tx) &&
+ !checkIsPaymentExpired(tx);
+
+ if (shouldFetchPaymentIntent) {
+ const { stripePaymentIntentClientSecret } =
+ tx.attributes.protectedData && tx.attributes.protectedData.stripePaymentIntents
+ ? tx.attributes.protectedData.stripePaymentIntents.default
+ : {};
+
+ // Fetch up to date PaymentIntent from Stripe
+ onRetrievePaymentIntent({ stripe, stripePaymentIntentClientSecret });
+ }
+ }
+
render() {
const {
scrollingDisabled,
speculateTransactionInProgress,
speculateTransactionError,
- speculatedTransaction,
+ speculatedTransaction: speculatedTransactionMaybe,
initiateOrderError,
+ confirmPaymentError,
intl,
params,
currentUser,
- onCreateStripePaymentToken,
- stripePaymentTokenInProgress,
- stripePaymentTokenError,
- stripePaymentToken,
+ handleCardPaymentError,
+ paymentIntent,
+ retrievePaymentIntentError,
} = this.props;
// Since the listing data is already given from the ListingPage
@@ -228,9 +392,9 @@ export class CheckoutPageComponent extends Component {
const isLoading = !this.state.dataLoaded || speculateTransactionInProgress;
- const { listing, bookingDates, enquiredTransaction } = this.state.pageData;
- const currentTransaction = ensureTransaction(speculatedTransaction, {}, null);
- const currentBooking = ensureBooking(currentTransaction.booking);
+ const { listing, bookingDates, transaction } = this.state.pageData;
+ const existingTransaction = ensureTransaction(transaction);
+ const speculatedTransaction = ensureTransaction(speculatedTransactionMaybe, {}, null);
const currentListing = ensureListing(listing);
const currentAuthor = ensureUser(currentListing.author);
@@ -256,26 +420,30 @@ export class CheckoutPageComponent extends Component {
if (shouldRedirect) {
// eslint-disable-next-line no-console
console.error('Missing or invalid data for checkout, redirecting back to listing page.', {
- transaction: currentTransaction,
+ transaction: speculatedTransaction,
bookingDates,
listing,
});
return ;
}
- // Show breakdown only when transaction and booking are loaded
+ // Show breakdown only when speculated transaction and booking are loaded
// (i.e. have an id)
+ const tx = existingTransaction.booking ? existingTransaction : speculatedTransaction;
+ const txBooking = ensureBooking(tx.booking);
const breakdown =
- currentTransaction.id && currentBooking.id ? (
+ tx.id && txBooking.id ? (
) : null;
+ const isPaymentExpired = checkIsPaymentExpired(existingTransaction);
+
// Allow showing page when currentUser is still being downloaded,
// but show payment form only when user info is loaded.
const showPaymentForm = !!(
@@ -283,7 +451,9 @@ export class CheckoutPageComponent extends Component {
hasRequiredData &&
!listingNotFound &&
!initiateOrderError &&
- !speculateTransactionError
+ !speculateTransactionError &&
+ !retrievePaymentIntentError &&
+ !isPaymentExpired
);
const listingTitle = currentListing.attributes.title;
@@ -292,11 +462,6 @@ export class CheckoutPageComponent extends Component {
const firstImage =
currentListing.images && currentListing.images.length > 0 ? currentListing.images[0] : null;
- const listingNotFoundErrorMessage = listingNotFound ? (
-
-
-
- ) : null;
const listingLink = (
+
+
+ );
+ } else if (isAmountTooLowError) {
initiateOrderErrorMessage = (
);
- } else if (!listingNotFound && isBookingTimeNotAvailableError) {
+ } else if (isBookingTimeNotAvailableError) {
initiateOrderErrorMessage = (
);
- } else if (!listingNotFound && isChargeDisabledError) {
+ } else if (isChargeDisabledError) {
initiateOrderErrorMessage = (
);
- } else if (!listingNotFound && stripeErrors && stripeErrors.length > 0) {
+ } else if (stripeErrors && stripeErrors.length > 0) {
// NOTE: Error messages from Stripes are not part of translations.
// By default they are in English.
const stripeErrorsAsString = stripeErrors.join(', ');
@@ -345,7 +517,8 @@ export class CheckoutPageComponent extends Component {
/>
);
- } else if (!listingNotFound && initiateOrderError) {
+ } else if (initiateOrderError) {
+ // Generic initiate order error
initiateOrderErrorMessage = (
@@ -417,7 +590,9 @@ export class CheckoutPageComponent extends Component {
const formattedPrice = formatMoney(intl, price);
const detailsSubTitle = `${formattedPrice} ${intl.formatMessage({ id: unitTranslationKey })}`;
- const showInitialMessageInput = !enquiredTransaction;
+ const showInitialMessageInput = !(
+ existingTransaction && existingTransaction.attributes.lastTransition === TRANSITION_ENQUIRE
+ );
const pageProps = { title, scrollingDisabled };
@@ -432,6 +607,22 @@ export class CheckoutPageComponent extends Component {
);
}
+ // Get first and last name of the current user and use it in the StripePaymentForm to autofill the name field
+ const userName =
+ currentUser && currentUser.attributes
+ ? `${currentUser.attributes.profile.firstName} ${currentUser.attributes.profile.lastName}`
+ : null;
+
+ // If paymentIntent status is not waiting user action,
+ // handleCardPayment has been called previously.
+ const hasPaymentIntentUserActionsDone =
+ paymentIntent && STRIPE_PI_USER_ACTIONS_DONE_STATUSES.includes(paymentIntent.status);
+
+ // If your marketplace works mostly in one country you can use initial values to select country automatically
+ // e.g. {country: 'FI'}
+
+ const initalValuesForStripePayment = { name: userName };
+
return (
{topbar}
@@ -470,6 +661,14 @@ export class CheckoutPageComponent extends Component {
{initiateOrderErrorMessage}
{listingNotFoundErrorMessage}
{speculateErrorMessage}
+ {retrievePaymentIntentError ? (
+
+
+
+ ) : null}
{showPaymentForm ? (
) : null}
+ {isPaymentExpired ? (
+
+
+
+ ) : null}
@@ -518,16 +728,15 @@ export class CheckoutPageComponent extends Component {
CheckoutPageComponent.defaultProps = {
initiateOrderError: null,
+ confirmPaymentError: null,
listing: null,
bookingData: {},
bookingDates: null,
speculateTransactionError: null,
speculatedTransaction: null,
- enquiredTransaction: null,
+ transaction: null,
currentUser: null,
- stripePaymentToken: null,
- stripePaymentTokenInProgress: false,
- stripePaymentTokenError: null,
+ paymentIntent: null,
};
CheckoutPageComponent.propTypes = {
@@ -542,18 +751,20 @@ CheckoutPageComponent.propTypes = {
speculateTransactionInProgress: bool.isRequired,
speculateTransactionError: propTypes.error,
speculatedTransaction: propTypes.transaction,
- enquiredTransaction: propTypes.transaction,
- initiateOrderError: propTypes.error,
+ transaction: propTypes.transaction,
currentUser: propTypes.currentUser,
params: shape({
id: string,
slug: string,
}).isRequired,
- sendOrderRequest: func.isRequired,
- onCreateStripePaymentToken: func.isRequired,
- stripePaymentTokenInProgress: bool,
- stripePaymentTokenError: propTypes.error,
- stripePaymentToken: object,
+ onInitiateOrder: func.isRequired,
+ onHandleCardPayment: func.isRequired,
+ onRetrievePaymentIntent: func.isRequired,
+ initiateOrderError: propTypes.error,
+ confirmPaymentError: propTypes.error,
+ // handleCardPaymentError comes from Stripe so that's why we can't expect it to be in a specific form
+ handleCardPaymentError: oneOfType([propTypes.error, object]),
+ paymentIntent: object,
// from connect
dispatch: func.isRequired,
@@ -575,15 +786,12 @@ const mapStateToProps = state => {
speculateTransactionInProgress,
speculateTransactionError,
speculatedTransaction,
- enquiredTransaction,
+ transaction,
initiateOrderError,
+ confirmPaymentError,
} = state.CheckoutPage;
const { currentUser } = state.user;
- const {
- stripePaymentTokenInProgress,
- stripePaymentTokenError,
- stripePaymentToken,
- } = state.stripe;
+ const { handleCardPaymentError, paymentIntent, retrievePaymentIntentError } = state.stripe;
return {
scrollingDisabled: isScrollingDisabled(state),
currentUser,
@@ -592,23 +800,24 @@ const mapStateToProps = state => {
speculateTransactionInProgress,
speculateTransactionError,
speculatedTransaction,
- enquiredTransaction,
+ transaction,
listing,
initiateOrderError,
- stripePaymentTokenInProgress,
- stripePaymentTokenError,
- stripePaymentToken,
+ handleCardPaymentError,
+ confirmPaymentError,
+ paymentIntent,
+ retrievePaymentIntentError,
};
};
const mapDispatchToProps = dispatch => ({
dispatch,
- sendOrderRequest: (params, initialMessage) => dispatch(initiateOrder(params, initialMessage)),
- sendOrderRequestAfterEnquiry: (transactionId, params) =>
- dispatch(initiateOrderAfterEnquiry(transactionId, params)),
+ onInitiateOrder: (params, transactionId) => dispatch(initiateOrder(params, transactionId)),
fetchSpeculatedTransaction: params => dispatch(speculateTransaction(params)),
- onCreateStripePaymentToken: params => dispatch(createStripePaymentToken(params)),
- onClearStripePaymentToken: () => dispatch(clearStripePaymentToken()),
+ onRetrievePaymentIntent: params => dispatch(retrievePaymentIntent(params)),
+ onHandleCardPayment: params => dispatch(handleCardPayment(params)),
+ onConfirmPayment: params => dispatch(confirmPayment(params)),
+ onSendMessage: params => dispatch(sendMessage(params)),
});
const CheckoutPage = compose(
diff --git a/src/containers/CheckoutPage/CheckoutPage.test.js b/src/containers/CheckoutPage/CheckoutPage.test.js
index 4171266038..caddeb3c10 100644
--- a/src/containers/CheckoutPage/CheckoutPage.test.js
+++ b/src/containers/CheckoutPage/CheckoutPage.test.js
@@ -24,9 +24,10 @@ describe('CheckoutPage', () => {
fetchSpeculatedTransaction: noop,
speculateTransactionInProgress: false,
scrollingDisabled: false,
- onCreateStripePaymentToken: noop,
- stripePaymentTokenInProgress: false,
- stripePaymentTokenError: null,
+ onHandleCardPayment: noop,
+ onInitiateOrder: noop,
+ onRetrievePaymentIntent: noop,
+ handleCardPaymentInProgress: false,
};
const tree = renderShallow(
);
expect(tree).toMatchSnapshot();
@@ -62,7 +63,8 @@ describe('CheckoutPage', () => {
speculateTransactionError: null,
speculateTransactionInProgress: false,
speculatedTransaction: null,
- enquiredTransaction: null,
+ transaction: null,
+ confirmPaymentError: null,
};
it('should return the initial state', () => {
diff --git a/src/containers/CheckoutPage/CheckoutPageSessionHelpers.js b/src/containers/CheckoutPage/CheckoutPageSessionHelpers.js
index 1dd9a3fe7c..37222aec5c 100644
--- a/src/containers/CheckoutPage/CheckoutPageSessionHelpers.js
+++ b/src/containers/CheckoutPage/CheckoutPageSessionHelpers.js
@@ -6,8 +6,9 @@
*/
import moment from 'moment';
import reduce from 'lodash/reduce';
+import Decimal from 'decimal.js';
import { types as sdkTypes } from '../../util/sdkLoader';
-import { TRANSITION_ENQUIRE } from '../../util/transaction';
+import { TRANSITIONS } from '../../util/transaction';
const { UUID, Money } = sdkTypes;
@@ -47,37 +48,41 @@ export const isValidListing = listing => {
return validateProperties(listing, props);
};
-// Validate content of an enquired transaction received from SessionStore.
-// An id is required and the last transition needs to be the enquire transition.
-export const isValidEnquiredTransaction = transaction => {
+// Validate content of an transaction received from SessionStore.
+// An id is required and the last transition needs to be one of the known transitions
+export const isValidTransaction = transaction => {
const props = {
id: id => id instanceof UUID,
+ type: type => type === 'transaction',
attributes: v => {
- return typeof v === 'object' && v.lastTransition === TRANSITION_ENQUIRE;
+ return typeof v === 'object' && TRANSITIONS.includes(v.lastTransition);
},
};
return validateProperties(transaction, props);
};
// Stores given bookingDates and listing to sessionStorage
-export const storeData = (bookingData, bookingDates, listing, enquiredTransaction, storageKey) => {
+export const storeData = (bookingData, bookingDates, listing, transaction, storageKey) => {
if (window && window.sessionStorage && listing && bookingDates && bookingData) {
- // TODO: How should we deal with Dates when data is serialized?
- // Hard coded serializable date objects atm.
- /* eslint-disable no-underscore-dangle */
const data = {
bookingData,
- bookingDates: {
- bookingStart: { date: bookingDates.bookingStart, _serializedType: 'SerializableDate' },
- bookingEnd: { date: bookingDates.bookingEnd, _serializedType: 'SerializableDate' },
- },
+ bookingDates,
listing,
- enquiredTransaction,
- storedAt: { date: new Date(), _serializedType: 'SerializableDate' },
+ transaction,
+ storedAt: new Date(),
};
- /* eslint-enable no-underscore-dangle */
- const storableData = JSON.stringify(data, sdkTypes.replacer);
+ const replacer = function(k, v) {
+ if (this[k] instanceof Date) {
+ return { date: v, _serializedType: 'SerializableDate' };
+ }
+ if (this[k] instanceof Decimal) {
+ return { decimal: v, _serializedType: 'SerializableDecimal' };
+ }
+ return sdkTypes.replacer(k, v);
+ };
+
+ const storableData = JSON.stringify(data, replacer);
window.sessionStorage.setItem(storageKey, storableData);
}
};
@@ -87,17 +92,20 @@ export const storedData = storageKey => {
if (window && window.sessionStorage) {
const checkoutPageData = window.sessionStorage.getItem(storageKey);
- // TODO How should we deal with Dates when data is serialized?
- // Dates are expected to be in format: { date: new Date(), _serializedType: 'SerializableDate' }
const reviver = (k, v) => {
- // eslint-disable-next-line no-underscore-dangle
if (v && typeof v === 'object' && v._serializedType === 'SerializableDate') {
+ // Dates are expected to be stored as:
+ // { date: new Date(), _serializedType: 'SerializableDate' }
return new Date(v.date);
+ } else if (v && typeof v === 'object' && v._serializedType === 'SerializableDecimal') {
+ // Decimals are expected to be stored as:
+ // { decimal: v, _serializedType: 'SerializableDecimal' }
+ return new Decimal(v.decimal);
}
return sdkTypes.reviver(k, v);
};
- const { bookingData, bookingDates, listing, enquiredTransaction, storedAt } = checkoutPageData
+ const { bookingData, bookingDates, listing, transaction, storedAt } = checkoutPageData
? JSON.parse(checkoutPageData, reviver)
: {};
@@ -106,19 +114,17 @@ export const storedData = storageKey => {
? moment(storedAt).isAfter(moment().subtract(1, 'days'))
: false;
- // resolve enquired transaction as valid if it is missing
- const isEnquiredTransactionValid = !!enquiredTransaction
- ? isValidEnquiredTransaction(enquiredTransaction)
- : true;
+ // resolve transaction as valid if it is missing
+ const isTransactionValid = !!transaction ? isValidTransaction(transaction) : true;
const isStoredDataValid =
isFreshlySaved &&
isValidBookingDates(bookingDates) &&
isValidListing(listing) &&
- isEnquiredTransactionValid;
+ isTransactionValid;
if (isStoredDataValid) {
- return { bookingData, bookingDates, listing, enquiredTransaction };
+ return { bookingData, bookingDates, listing, transaction };
}
}
return {};
diff --git a/src/containers/CheckoutPage/__snapshots__/CheckoutPage.test.js.snap b/src/containers/CheckoutPage/__snapshots__/CheckoutPage.test.js.snap
index 56452338d8..ff50630ef3 100644
--- a/src/containers/CheckoutPage/__snapshots__/CheckoutPage.test.js.snap
+++ b/src/containers/CheckoutPage/__snapshots__/CheckoutPage.test.js.snap
@@ -88,15 +88,21 @@ exports[`CheckoutPage matches snapshot 1`] = `
diff --git a/src/containers/InboxPage/InboxPage.js b/src/containers/InboxPage/InboxPage.js
index d2aaac3d04..3034d8db5c 100644
--- a/src/containers/InboxPage/InboxPage.js
+++ b/src/containers/InboxPage/InboxPage.js
@@ -12,6 +12,8 @@ import {
txIsEnquired,
txIsRequested,
txHasBeenDelivered,
+ txIsPaymentExpired,
+ txIsPaymentPending,
} from '../../util/transaction';
import { LINE_ITEM_DAY, LINE_ITEM_UNITS, propTypes } from '../../util/types';
import { formatMoney } from '../../util/currency';
@@ -89,6 +91,26 @@ export const txState = (intl, tx, type) => {
};
return requested;
+ } else if (txIsPaymentPending(tx)) {
+ return {
+ nameClassName: isOrder ? css.nameNotEmphasized : css.nameEmphasized,
+ bookingClassName: css.bookingNoActionNeeded,
+ lastTransitionedAtClassName: css.lastTransitionedAtNotEmphasized,
+ stateClassName: isOrder ? css.stateActionNeeded : css.stateNoActionNeeded,
+ state: intl.formatMessage({
+ id: 'InboxPage.statePendingPayment',
+ }),
+ };
+ } else if (txIsPaymentExpired(tx)) {
+ return {
+ nameClassName: css.nameNotEmphasized,
+ bookingClassName: css.bookingNoActionNeeded,
+ lastTransitionedAtClassName: css.lastTransitionedAtNotEmphasized,
+ stateClassName: css.stateNoActionNeeded,
+ state: intl.formatMessage({
+ id: 'InboxPage.stateExpired',
+ }),
+ };
} else if (txIsDeclined(tx)) {
return {
nameClassName: css.nameNotEmphasized,
diff --git a/src/containers/InboxPage/InboxPage.test.js b/src/containers/InboxPage/InboxPage.test.js
index 8207469849..478dd6828b 100644
--- a/src/containers/InboxPage/InboxPage.test.js
+++ b/src/containers/InboxPage/InboxPage.test.js
@@ -10,7 +10,7 @@ import {
} from '../../util/test-data';
import { InboxPageComponent, InboxItem, txState } from './InboxPage';
import routeConfiguration from '../../routeConfiguration';
-import { TRANSITION_REQUEST } from '../../util/transaction';
+import { TRANSITION_CONFIRM_PAYMENT } from '../../util/transaction';
import { LINE_ITEM_NIGHT } from '../../util/types';
const noop = () => null;
@@ -61,7 +61,7 @@ describe('InboxPage', () => {
transactions: [
createTransaction({
id: 'order-1',
- lastTransition: TRANSITION_REQUEST,
+ lastTransition: TRANSITION_CONFIRM_PAYMENT,
customer,
provider,
lastTransitionedAt: new Date(Date.UTC(2017, 0, 15)),
@@ -69,7 +69,7 @@ describe('InboxPage', () => {
}),
createTransaction({
id: 'order-2',
- lastTransition: TRANSITION_REQUEST,
+ lastTransition: TRANSITION_CONFIRM_PAYMENT,
customer,
provider,
lastTransitionedAt: new Date(Date.UTC(2016, 0, 15)),
@@ -118,7 +118,7 @@ describe('InboxPage', () => {
transactions: [
createTransaction({
id: 'sale-1',
- lastTransition: TRANSITION_REQUEST,
+ lastTransition: TRANSITION_CONFIRM_PAYMENT,
customer,
provider,
lastTransitionedAt: new Date(Date.UTC(2017, 0, 15)),
@@ -126,7 +126,7 @@ describe('InboxPage', () => {
}),
createTransaction({
id: 'sale-2',
- lastTransition: TRANSITION_REQUEST,
+ lastTransition: TRANSITION_CONFIRM_PAYMENT,
customer,
provider,
lastTransitionedAt: new Date(Date.UTC(2016, 0, 15)),
diff --git a/src/containers/InboxPage/__snapshots__/InboxPage.test.js.snap b/src/containers/InboxPage/__snapshots__/InboxPage.test.js.snap
index c1f85580fe..4502ad6eb4 100644
--- a/src/containers/InboxPage/__snapshots__/InboxPage.test.js.snap
+++ b/src/containers/InboxPage/__snapshots__/InboxPage.test.js.snap
@@ -104,7 +104,7 @@ exports[`InboxPage matches snapshot 1`] = `
Object {
"attributes": Object {
"createdAt": 2017-05-01T00:00:00.000Z,
- "lastTransition": "transition/request",
+ "lastTransition": "transition/confirm-payment",
"lastTransitionedAt": 2017-01-15T00:00:00.000Z,
"lineItems": Array [
Object {
@@ -158,7 +158,12 @@ exports[`InboxPage matches snapshot 1`] = `
Object {
"by": "customer",
"createdAt": 2017-05-01T00:00:00.000Z,
- "transition": "transition/request",
+ "transition": "transition/request-payment",
+ },
+ Object {
+ "by": "customer",
+ "createdAt": 2017-05-01T00:00:01.000Z,
+ "transition": "transition/confirm-payment",
},
Object {
"by": "provider",
@@ -252,7 +257,7 @@ exports[`InboxPage matches snapshot 1`] = `
Object {
"attributes": Object {
"createdAt": 2017-05-01T00:00:00.000Z,
- "lastTransition": "transition/request",
+ "lastTransition": "transition/confirm-payment",
"lastTransitionedAt": 2016-01-15T00:00:00.000Z,
"lineItems": Array [
Object {
@@ -306,7 +311,12 @@ exports[`InboxPage matches snapshot 1`] = `
Object {
"by": "customer",
"createdAt": 2017-05-01T00:00:00.000Z,
- "transition": "transition/request",
+ "transition": "transition/request-payment",
+ },
+ Object {
+ "by": "customer",
+ "createdAt": 2017-05-01T00:00:01.000Z,
+ "transition": "transition/confirm-payment",
},
Object {
"by": "provider",
@@ -546,7 +556,7 @@ exports[`InboxPage matches snapshot 3`] = `
Object {
"attributes": Object {
"createdAt": 2017-05-01T00:00:00.000Z,
- "lastTransition": "transition/request",
+ "lastTransition": "transition/confirm-payment",
"lastTransitionedAt": 2017-01-15T00:00:00.000Z,
"lineItems": Array [
Object {
@@ -600,7 +610,12 @@ exports[`InboxPage matches snapshot 3`] = `
Object {
"by": "customer",
"createdAt": 2017-05-01T00:00:00.000Z,
- "transition": "transition/request",
+ "transition": "transition/request-payment",
+ },
+ Object {
+ "by": "customer",
+ "createdAt": 2017-05-01T00:00:01.000Z,
+ "transition": "transition/confirm-payment",
},
Object {
"by": "provider",
@@ -694,7 +709,7 @@ exports[`InboxPage matches snapshot 3`] = `
Object {
"attributes": Object {
"createdAt": 2017-05-01T00:00:00.000Z,
- "lastTransition": "transition/request",
+ "lastTransition": "transition/confirm-payment",
"lastTransitionedAt": 2016-01-15T00:00:00.000Z,
"lineItems": Array [
Object {
@@ -748,7 +763,12 @@ exports[`InboxPage matches snapshot 3`] = `
Object {
"by": "customer",
"createdAt": 2017-05-01T00:00:00.000Z,
- "transition": "transition/request",
+ "transition": "transition/request-payment",
+ },
+ Object {
+ "by": "customer",
+ "createdAt": 2017-05-01T00:00:01.000Z,
+ "transition": "transition/confirm-payment",
},
Object {
"by": "provider",
diff --git a/src/containers/ListingPage/ListingPage.js b/src/containers/ListingPage/ListingPage.js
index 065156be7d..b956fe284a 100644
--- a/src/containers/ListingPage/ListingPage.js
+++ b/src/containers/ListingPage/ListingPage.js
@@ -27,6 +27,7 @@ import {
import { richText } from '../../util/richText';
import { getMarketplaceEntities } from '../../ducks/marketplaceData.duck';
import { manageDisableScrolling, isScrollingDisabled } from '../../ducks/UI.duck';
+import { initializeCardPaymentData } from '../../ducks/stripe.duck.js';
import {
Page,
NamedLink,
@@ -90,7 +91,13 @@ export class ListingPageComponent extends Component {
}
handleSubmit(values) {
- const { history, getListing, params, callSetInitialValues } = this.props;
+ const {
+ history,
+ getListing,
+ params,
+ callSetInitialValues,
+ onInitializeCardPaymentData,
+ } = this.props;
const listingId = new UUID(params.id);
const listing = getListing(listingId);
@@ -103,6 +110,7 @@ export class ListingPageComponent extends Component {
bookingStart: bookingDates.startDate,
bookingEnd: bookingDates.endDate,
},
+ confirmPaymentError: null,
};
const routes = routeConfiguration();
@@ -110,6 +118,9 @@ export class ListingPageComponent extends Component {
const { setInitialValues } = findRouteByRouteName('CheckoutPage', routes);
callSetInitialValues(setInitialValues, initialValues);
+ // Clear previous Stripe errors from store if there is any
+ onInitializeCardPaymentData();
+
// Redirect to CheckoutPage
history.push(
createResourceLocatorString(
@@ -512,6 +523,7 @@ ListingPageComponent.propTypes = {
sendEnquiryInProgress: bool.isRequired,
sendEnquiryError: propTypes.error,
onSendEnquiry: func.isRequired,
+ onInitializeCardPaymentData: func.isRequired,
categoriesConfig: array,
amenitiesConfig: array,
@@ -565,6 +577,7 @@ const mapDispatchToProps = dispatch => ({
dispatch(manageDisableScrolling(componentId, disableScrolling)),
callSetInitialValues: (setInitialValues, values) => dispatch(setInitialValues(values)),
onSendEnquiry: (listingId, message) => dispatch(sendEnquiry(listingId, message)),
+ onInitializeCardPaymentData: () => dispatch(initializeCardPaymentData()),
});
// Note: it is important that the withRouter HOC is **outside** the
diff --git a/src/containers/ListingPage/ListingPage.test.js b/src/containers/ListingPage/ListingPage.test.js
index e1fe74ee95..b4c3d1fe65 100644
--- a/src/containers/ListingPage/ListingPage.test.js
+++ b/src/containers/ListingPage/ListingPage.test.js
@@ -73,6 +73,7 @@ describe('ListingPage', () => {
callSetInitialValues: noop,
sendVerificationEmailInProgress: false,
onResendVerificationEmail: noop,
+ onInitializeCardPaymentData: noop,
sendEnquiryInProgress: false,
onSendEnquiry: noop,
categoriesConfig,
diff --git a/src/containers/TransactionPage/TransactionPage.js b/src/containers/TransactionPage/TransactionPage.js
index da66cce877..15df684c16 100644
--- a/src/containers/TransactionPage/TransactionPage.js
+++ b/src/containers/TransactionPage/TransactionPage.js
@@ -9,9 +9,12 @@ import { createResourceLocatorString, findRouteByRouteName } from '../../util/ro
import routeConfiguration from '../../routeConfiguration';
import { propTypes } from '../../util/types';
import { ensureListing, ensureTransaction } from '../../util/data';
+import { dateFromAPIToLocalNoon } from '../../util/dates';
import { createSlug } from '../../util/urlHelpers';
+import { txIsPaymentPending } from '../../util/transaction';
import { getMarketplaceEntities } from '../../ducks/marketplaceData.duck';
import { isScrollingDisabled, manageDisableScrolling } from '../../ducks/UI.duck';
+import { initializeCardPaymentData } from '../../ducks/stripe.duck.js';
import {
NamedRedirect,
TransactionPanel,
@@ -72,29 +75,23 @@ export const TransactionPageComponent = props => {
timeSlots,
fetchTimeSlotsError,
callSetInitialValues,
+ onInitializeCardPaymentData,
} = props;
const currentTransaction = ensureTransaction(transaction);
const currentListing = ensureListing(currentTransaction.listing);
+ const isProviderRole = transactionRole === PROVIDER;
+ const isCustomerRole = transactionRole === CUSTOMER;
- const handleSubmitBookingRequest = values => {
- const { bookingDates, ...bookingData } = values;
-
- const initialValues = {
- listing: currentListing,
- enquiredTransaction: currentTransaction,
- bookingData,
- bookingDates: {
- bookingStart: bookingDates.startDate,
- bookingEnd: bookingDates.endDate,
- },
- };
-
+ const redirectToCheckoutPageWithInitialValues = (initialValues, listing) => {
const routes = routeConfiguration();
// Customize checkout page state with current listing and selected bookingDates
const { setInitialValues } = findRouteByRouteName('CheckoutPage', routes);
callSetInitialValues(setInitialValues, initialValues);
+ // Clear previous Stripe errors from store if there is any
+ onInitializeCardPaymentData();
+
// Redirect to CheckoutPage
history.push(
createResourceLocatorString(
@@ -106,6 +103,46 @@ export const TransactionPageComponent = props => {
);
};
+ // If payment is pending, redirect to CheckoutPage
+ if (txIsPaymentPending(currentTransaction) && isCustomerRole) {
+ const currentBooking = ensureListing(currentTransaction.booking);
+
+ const initialValues = {
+ listing: currentListing,
+ // Transaction with payment pending should be passed to CheckoutPage
+ transaction: currentTransaction,
+ // Original bookingData content is not available,
+ // but it is already used since booking is created.
+ // (E.g. quantity is used when booking is created.)
+ bookingData: {},
+ bookingDates: {
+ bookingStart: dateFromAPIToLocalNoon(currentBooking.attributes.start),
+ bookingEnd: dateFromAPIToLocalNoon(currentBooking.attributes.end),
+ },
+ };
+
+ redirectToCheckoutPageWithInitialValues(initialValues, currentListing);
+ }
+
+ // Customer can create a booking, if the tx is in "enquiry" state.
+ const handleSubmitBookingRequest = values => {
+ const { bookingDates, ...bookingData } = values;
+
+ const initialValues = {
+ listing: currentListing,
+ // enquired transaction should be passed to CheckoutPage
+ transaction: currentTransaction,
+ bookingData,
+ bookingDates: {
+ bookingStart: bookingDates.startDate,
+ bookingEnd: bookingDates.endDate,
+ },
+ confirmPaymentError: null,
+ };
+
+ redirectToCheckoutPageWithInitialValues(initialValues, currentListing);
+ };
+
const deletedListingTitle = intl.formatMessage({
id: 'TransactionPage.deletedListing',
});
@@ -123,8 +160,6 @@ export const TransactionPageComponent = props => {
currentTransaction.provider &&
!fetchTransactionError;
- const isProviderRole = transactionRole === PROVIDER;
- const isCustomerRole = transactionRole === CUSTOMER;
const isOwnSale =
isDataAvailable &&
isProviderRole &&
@@ -265,6 +300,7 @@ TransactionPageComponent.propTypes = {
timeSlots: arrayOf(propTypes.timeSlot),
fetchTimeSlotsError: propTypes.error,
callSetInitialValues: func.isRequired,
+ onInitializeCardPaymentData: func.isRequired,
// from withRouter
history: shape({
@@ -339,6 +375,7 @@ const mapDispatchToProps = dispatch => {
onSendReview: (role, tx, reviewRating, reviewContent) =>
dispatch(sendReview(role, tx, reviewRating, reviewContent)),
callSetInitialValues: (setInitialValues, values) => dispatch(setInitialValues(values)),
+ onInitializeCardPaymentData: () => dispatch(initializeCardPaymentData()),
};
};
diff --git a/src/containers/TransactionPage/TransactionPage.test.js b/src/containers/TransactionPage/TransactionPage.test.js
index 550f5ab63a..358e48d96f 100644
--- a/src/containers/TransactionPage/TransactionPage.test.js
+++ b/src/containers/TransactionPage/TransactionPage.test.js
@@ -8,7 +8,7 @@ import {
fakeIntl,
} from '../../util/test-data';
import { renderShallow } from '../../util/test-helpers';
-import { TRANSITION_REQUEST } from '../../util/transaction';
+import { TRANSITION_CONFIRM_PAYMENT } from '../../util/transaction';
import { TransactionPageComponent } from './TransactionPage';
const noop = () => null;
@@ -20,7 +20,7 @@ describe('TransactionPage - Sale', () => {
const end = new Date(Date.UTC(2017, 5, 13));
const transaction = createTransaction({
id: txId,
- lastTransition: TRANSITION_REQUEST,
+ lastTransition: TRANSITION_CONFIRM_PAYMENT,
booking: createBooking('booking1', {
start,
end,
@@ -49,6 +49,7 @@ describe('TransactionPage - Sale', () => {
oldestMessagePageFetched: 0,
messages: [],
sendMessageInProgress: false,
+ onInitializeCardPaymentData: noop,
onShowMoreMessages: noop,
onSendMessage: noop,
onResetForm: noop,
@@ -77,7 +78,7 @@ describe('TransactionPage - Order', () => {
const transaction = createTransaction({
id: txId,
- lastTransition: TRANSITION_REQUEST,
+ lastTransition: TRANSITION_CONFIRM_PAYMENT,
booking: createBooking('booking1', {
start,
end,
@@ -103,6 +104,7 @@ describe('TransactionPage - Order', () => {
scrollingDisabled: false,
callSetInitialValues: noop,
transaction,
+ onInitializeCardPaymentData: noop,
onShowMoreMessages: noop,
onSendMessage: noop,
onResetForm: noop,
diff --git a/src/containers/TransactionPage/__snapshots__/TransactionPage.test.js.snap b/src/containers/TransactionPage/__snapshots__/TransactionPage.test.js.snap
index 318752a274..4183e204f4 100644
--- a/src/containers/TransactionPage/__snapshots__/TransactionPage.test.js.snap
+++ b/src/containers/TransactionPage/__snapshots__/TransactionPage.test.js.snap
@@ -66,7 +66,7 @@ exports[`TransactionPage - Order matches snapshot 1`] = `
Object {
"attributes": Object {
"createdAt": 2017-05-01T00:00:00.000Z,
- "lastTransition": "transition/request",
+ "lastTransition": "transition/confirm-payment",
"lastTransitionedAt": 2017-06-01T00:00:00.000Z,
"lineItems": Array [
Object {
@@ -120,7 +120,12 @@ exports[`TransactionPage - Order matches snapshot 1`] = `
Object {
"by": "customer",
"createdAt": 2017-05-01T00:00:00.000Z,
- "transition": "transition/request",
+ "transition": "transition/request-payment",
+ },
+ Object {
+ "by": "customer",
+ "createdAt": 2017-05-01T00:00:01.000Z,
+ "transition": "transition/confirm-payment",
},
Object {
"by": "provider",
@@ -283,7 +288,7 @@ exports[`TransactionPage - Sale matches snapshot 1`] = `
Object {
"attributes": Object {
"createdAt": 2017-05-01T00:00:00.000Z,
- "lastTransition": "transition/request",
+ "lastTransition": "transition/confirm-payment",
"lastTransitionedAt": 2017-06-01T00:00:00.000Z,
"lineItems": Array [
Object {
@@ -337,7 +342,12 @@ exports[`TransactionPage - Sale matches snapshot 1`] = `
Object {
"by": "customer",
"createdAt": 2017-05-01T00:00:00.000Z,
- "transition": "transition/request",
+ "transition": "transition/request-payment",
+ },
+ Object {
+ "by": "customer",
+ "createdAt": 2017-05-01T00:00:01.000Z,
+ "transition": "transition/confirm-payment",
},
Object {
"by": "provider",
diff --git a/src/ducks/stripe.duck.js b/src/ducks/stripe.duck.js
index b158807c71..ba6833ed31 100644
--- a/src/ducks/stripe.duck.js
+++ b/src/ducks/stripe.duck.js
@@ -18,12 +18,18 @@ export const PERSON_CREATE_REQUEST = 'app/stripe/PERSON_CREATE_REQUEST';
export const PERSON_CREATE_SUCCESS = 'app/stripe/PERSON_CREATE_SUCCESS';
export const PERSON_CREATE_ERROR = 'app/stripe/PERSON_CREATE_ERROR';
-export const CREATE_PAYMENT_TOKEN_REQUEST = 'app/stripe/CREATE_PAYMENT_TOKEN_REQUEST';
-export const CREATE_PAYMENT_TOKEN_SUCCESS = 'app/stripe/CREATE_PAYMENT_TOKEN_SUCCESS';
-export const CREATE_PAYMENT_TOKEN_ERROR = 'app/stripe/CREATE_PAYMENT_TOKEN_ERROR';
-
export const CLEAR_PAYMENT_TOKEN = 'app/stripe/CLEAR_PAYMENT_TOKEN';
+export const HANDLE_CARD_PAYMENT_REQUEST = 'app/stripe/HANDLE_CARD_PAYMENT_REQUEST';
+export const HANDLE_CARD_PAYMENT_SUCCESS = 'app/stripe/HANDLE_CARD_PAYMENT_SUCCESS';
+export const HANDLE_CARD_PAYMENT_ERROR = 'app/stripe/HANDLE_CARD_PAYMENT_ERROR';
+
+export const CLEAR_HANDLE_CARD_PAYMENT = 'app/stripe/CLEAR_HANDLE_CARD_PAYMENT';
+
+export const RETRIEVE_PAYMENT_INTENT_REQUEST = 'app/stripe/RETRIEVE_PAYMENT_INTENT_REQUEST';
+export const RETRIEVE_PAYMENT_INTENT_SUCCESS = 'app/stripe/RETRIEVE_PAYMENT_INTENT_SUCCESS';
+export const RETRIEVE_PAYMENT_INTENT_ERROR = 'app/stripe/RETRIEVE_PAYMENT_INTENT_ERROR';
+
// ================ Reducer ================ //
const initialState = {
@@ -35,9 +41,11 @@ const initialState = {
persons: [],
stripeAccount: null,
stripeAccountFetched: false,
- stripePaymentTokenInProgress: false,
- stripePaymentTokenError: null,
- stripePaymentToken: null,
+ handleCardPaymentInProgress: false,
+ handleCardPaymentError: null,
+ paymentIntent: null,
+ retrievePaymentIntentInProgress: false,
+ retrievePaymentIntentError: null,
};
export default function reducer(state = initialState, action = {}) {
@@ -103,19 +111,41 @@ export default function reducer(state = initialState, action = {}) {
}),
};
- case CREATE_PAYMENT_TOKEN_REQUEST:
+ case HANDLE_CARD_PAYMENT_REQUEST:
return {
...state,
- stripePaymentTokenError: null,
- stripePaymentTokenInProgress: true,
+ handleCardPaymentError: null,
+ handleCardPaymentInProgress: true,
};
- case CREATE_PAYMENT_TOKEN_SUCCESS:
- return { ...state, stripePaymentTokenInProgress: false, stripePaymentToken: payload };
- case CREATE_PAYMENT_TOKEN_ERROR:
+ case HANDLE_CARD_PAYMENT_SUCCESS:
+ return { ...state, paymentIntent: payload, handleCardPaymentInProgress: false };
+ case HANDLE_CARD_PAYMENT_ERROR:
console.error(payload);
- return { ...state, stripePaymentTokenError: payload, stripePaymentTokenInProgress: false };
- case CLEAR_PAYMENT_TOKEN:
- return { ...state, stripePaymentToken: null };
+ return { ...state, handleCardPaymentError: payload, handleCardPaymentInProgress: false };
+
+ case CLEAR_HANDLE_CARD_PAYMENT:
+ return {
+ ...state,
+ handleCardPaymentInProgress: false,
+ handleCardPaymentError: null,
+ paymentIntent: null,
+ };
+
+ case RETRIEVE_PAYMENT_INTENT_REQUEST:
+ return {
+ ...state,
+ retrievePaymentIntentError: null,
+ retrievePaymentIntentInProgress: true,
+ };
+ case RETRIEVE_PAYMENT_INTENT_SUCCESS:
+ return { ...state, paymentIntent: payload, retrievePaymentIntentInProgress: false };
+ case RETRIEVE_PAYMENT_INTENT_ERROR:
+ console.error(payload);
+ return {
+ ...state,
+ retrievePaymentIntentError: payload,
+ retrievePaymentIntentInProgress: false,
+ };
default:
return state;
@@ -173,23 +203,38 @@ export const personCreateError = payload => ({
error: true,
});
-export const createPaymentTokenRequest = () => ({
- type: CREATE_PAYMENT_TOKEN_REQUEST,
+export const handleCardPaymentRequest = () => ({
+ type: HANDLE_CARD_PAYMENT_REQUEST,
});
-export const createPaymentTokenSuccess = payload => ({
- type: CREATE_PAYMENT_TOKEN_SUCCESS,
+export const handleCardPaymentSuccess = payload => ({
+ type: HANDLE_CARD_PAYMENT_SUCCESS,
payload,
});
-export const createPaymentTokenError = payload => ({
- type: CREATE_PAYMENT_TOKEN_ERROR,
+export const handleCardPaymentError = payload => ({
+ type: HANDLE_CARD_PAYMENT_ERROR,
payload,
error: true,
});
-export const clearPaymentToken = () => ({
- type: CLEAR_PAYMENT_TOKEN,
+export const initializeCardPaymentData = () => ({
+ type: CLEAR_HANDLE_CARD_PAYMENT,
+});
+
+export const retrievePaymentIntentRequest = () => ({
+ type: RETRIEVE_PAYMENT_INTENT_REQUEST,
+});
+
+export const retrievePaymentIntentSuccess = payload => ({
+ type: RETRIEVE_PAYMENT_INTENT_SUCCESS,
+ payload,
+});
+
+export const retrievePaymentIntentError = payload => ({
+ type: RETRIEVE_PAYMENT_INTENT_ERROR,
+ payload,
+ error: true,
});
// ================ Thunks ================ //
@@ -502,28 +547,81 @@ export const createStripeAccount = payoutDetails => (dispatch, getState, sdk) =>
}
};
-export const createStripePaymentToken = params => dispatch => {
+export const retrievePaymentIntent = params => dispatch => {
+ const { stripe, stripePaymentIntentClientSecret } = params;
+ dispatch(retrievePaymentIntentRequest());
+
+ return stripe
+ .retrievePaymentIntent(stripePaymentIntentClientSecret)
+ .then(response => {
+ if (response.error) {
+ return Promise.reject(response);
+ } else {
+ dispatch(retrievePaymentIntentSuccess(response.paymentIntent));
+ return response;
+ }
+ })
+ .catch(err => {
+ // Unwrap Stripe error.
+ const e = err.error || storableError(err);
+ dispatch(retrievePaymentIntentError(e));
+
+ // Log error
+ const { code, doc_url, message, payment_intent } = err.error || {};
+ const loggableError = err.error
+ ? {
+ code,
+ message,
+ doc_url,
+ paymentIntentStatus: payment_intent
+ ? payment_intent.status
+ : 'no payment_intent included',
+ }
+ : e;
+ log.error(loggableError, 'stripe-retrieve-payment-intent-failed', {
+ stripeMessage: loggableError.message,
+ });
+ throw err;
+ });
+};
+
+export const handleCardPayment = params => dispatch => {
// It's required to use the same instance of Stripe as where the card has been created
// so that's why Stripe needs to be passed here and we can't create a new instance.
- const { stripe, card } = params;
+ const { stripe, card, paymentParams, stripePaymentIntentClientSecret } = params;
+ const transactionId = params.orderId;
- dispatch(createPaymentTokenRequest());
+ dispatch(handleCardPaymentRequest());
return stripe
- .createToken(card)
+ .handleCardPayment(stripePaymentIntentClientSecret, card, paymentParams)
.then(response => {
- dispatch(createPaymentTokenSuccess(response.token));
- return response;
+ if (response.error) {
+ return Promise.reject(response);
+ } else {
+ dispatch(handleCardPaymentSuccess(response));
+ return { ...response, transactionId };
+ }
})
.catch(err => {
- const e = storableError(err);
- dispatch(createPaymentTokenError(e));
- const stripeMessage = e.message;
- log.error(err, 'create-stripe-payment-token-failed', { stripeMessage });
+ // Unwrap Stripe error.
+ const e = err.error || storableError(err);
+ dispatch(handleCardPaymentError(e));
+
+ // Log error
+ const containsPaymentIntent = err.error && err.error.payment_intent;
+ const { code, doc_url, message, payment_intent } = containsPaymentIntent ? err.error : {};
+ const loggableError = containsPaymentIntent
+ ? {
+ code,
+ message,
+ doc_url,
+ paymentIntentStatus: payment_intent.status,
+ }
+ : e;
+ log.error(loggableError, 'stripe-handle-card-payment-failed', {
+ stripeMessage: loggableError.message,
+ });
throw e;
});
};
-
-export const clearStripePaymentToken = () => dispatch => {
- dispatch(clearPaymentToken());
-};
diff --git a/src/forms/BookingDatesForm/EstimatedBreakdownMaybe.js b/src/forms/BookingDatesForm/EstimatedBreakdownMaybe.js
index 8289d4235d..6fa01417de 100644
--- a/src/forms/BookingDatesForm/EstimatedBreakdownMaybe.js
+++ b/src/forms/BookingDatesForm/EstimatedBreakdownMaybe.js
@@ -30,7 +30,7 @@ import moment from 'moment';
import Decimal from 'decimal.js';
import { types as sdkTypes } from '../../util/sdkLoader';
import { dateFromLocalToAPI, nightsBetween, daysBetween } from '../../util/dates';
-import { TRANSITION_REQUEST, TX_TRANSITION_ACTOR_CUSTOMER } from '../../util/transaction';
+import { TRANSITION_REQUEST_PAYMENT, TX_TRANSITION_ACTOR_CUSTOMER } from '../../util/transaction';
import { LINE_ITEM_DAY, LINE_ITEM_NIGHT, LINE_ITEM_UNITS } from '../../util/types';
import { unitDivisor, convertMoneyToNumber, convertUnitToSubUnit } from '../../util/currency';
import { BookingBreakdown } from '../../components';
@@ -86,7 +86,7 @@ const estimatedTransaction = (unitType, bookingStart, bookingEnd, unitPrice, qua
attributes: {
createdAt: now,
lastTransitionedAt: now,
- lastTransition: TRANSITION_REQUEST,
+ lastTransition: TRANSITION_REQUEST_PAYMENT,
payinTotal: totalPrice,
payoutTotal: totalPrice,
lineItems: [
@@ -103,7 +103,7 @@ const estimatedTransaction = (unitType, bookingStart, bookingEnd, unitPrice, qua
{
createdAt: now,
by: TX_TRANSITION_ACTOR_CUSTOMER,
- transition: TRANSITION_REQUEST,
+ transition: TRANSITION_REQUEST_PAYMENT,
},
],
},
diff --git a/src/forms/StripePaymentForm/StripePaymentAddress.js b/src/forms/StripePaymentForm/StripePaymentAddress.js
new file mode 100644
index 0000000000..14dbe1f207
--- /dev/null
+++ b/src/forms/StripePaymentForm/StripePaymentAddress.js
@@ -0,0 +1,186 @@
+import React from 'react';
+import { intlShape } from 'react-intl';
+import { bool, object, string } from 'prop-types';
+import config from '../../config';
+import * as validators from '../../util/validators';
+import getCountryCodes from '../../translations/countryCodes';
+import { FieldTextInput, FieldSelect } from '../../components';
+
+import css from './StripePaymentForm.css';
+
+const StripePaymentAddress = props => {
+ const { className, intl, disabled, form, fieldId, card } = props;
+
+ const optionalText = intl.formatMessage({
+ id: 'StripePaymentAddress.optionalText',
+ });
+
+ const addressLine1Label = intl.formatMessage({
+ id: 'StripePaymentAddress.addressLine1Label',
+ });
+ const addressLine1Placeholder = intl.formatMessage({
+ id: 'StripePaymentAddress.addressLine1Placeholder',
+ });
+ const addressLine1Required = validators.required(
+ intl.formatMessage({
+ id: 'StripePaymentAddress.addressLine1Required',
+ })
+ );
+
+ const addressLine2Label = intl.formatMessage(
+ { id: 'StripePaymentAddress.addressLine2Label' },
+ { optionalText: optionalText }
+ );
+
+ const addressLine2Placeholder = intl.formatMessage({
+ id: 'StripePaymentAddress.addressLine2Placeholder',
+ });
+
+ const postalCodeLabel = intl.formatMessage({ id: 'StripePaymentAddress.postalCodeLabel' });
+ const postalCodePlaceholder = intl.formatMessage({
+ id: 'StripePaymentAddress.postalCodePlaceholder',
+ });
+ const postalCodeRequired = validators.required(
+ intl.formatMessage({
+ id: 'StripePaymentAddress.postalCodeRequired',
+ })
+ );
+
+ const cityLabel = intl.formatMessage({ id: 'StripePaymentAddress.cityLabel' });
+ const cityPlaceholder = intl.formatMessage({ id: 'StripePaymentAddress.cityPlaceholder' });
+ const cityRequired = validators.required(
+ intl.formatMessage({
+ id: 'StripePaymentAddress.cityRequired',
+ })
+ );
+
+ const stateLabel = intl.formatMessage(
+ { id: 'StripePaymentAddress.stateLabel' },
+ { optionalText: optionalText }
+ );
+ const statePlaceholder = intl.formatMessage({ id: 'StripePaymentAddress.statePlaceholder' });
+
+ const countryLabel = intl.formatMessage({ id: 'StripePaymentAddress.countryLabel' });
+ const countryPlaceholder = intl.formatMessage({ id: 'StripePaymentAddress.countryPlaceholder' });
+ const countryRequired = validators.required(
+ intl.formatMessage({
+ id: 'StripePaymentAddress.countryRequired',
+ })
+ );
+
+ const handleOnChange = event => {
+ const value = event.target.value;
+ form.change('postal', value);
+ card.update({ value: { postalCode: value } });
+ };
+
+ // Use tha language set in config.locale to get the correct translations of the country names
+ const countryCodes = getCountryCodes(config.locale);
+
+ return (
+