Skip to content

Commit

Permalink
Refactor payment flow to be synchronous
Browse files Browse the repository at this point in the history
You now have a 'start payment' button that creates a payment intent, and
the client_secret is sent with Websockets to the client for further
action. Also renamed `charge` to `payment`.
  • Loading branch information
LudvigHz committed Sep 26, 2019
1 parent 11b6698 commit aec2c43
Show file tree
Hide file tree
Showing 12 changed files with 162 additions and 84 deletions.
1 change: 1 addition & 0 deletions app/actions/ActionTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const Event = {
SOCKET_REGISTRATION: generateStatuses('Event.SOCKET_REGISTRATION'),
SOCKET_UNREGISTRATION: generateStatuses('Event.SOCKET_UNREGISTRATION'),
SOCKET_PAYMENT: generateStatuses('Event.SOCKET_PAYMENT'),
SOCKET_INITIATE_PAYMENT: generateStatuses('Event.SOCKET_INITIATE_PAYMENT'),
SOCKET_EVENT_UPDATED: 'SOCKET_EVENT_UPDATED',
FOLLOW: generateStatuses('Event.FOLLOW'),
UNFOLLOW: generateStatuses('Event.UNFOLLOW'),
Expand Down
4 changes: 2 additions & 2 deletions app/actions/EventActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ export function updatePresence(
export function updatePayment(
eventId: number,
registrationId: number,
chargeStatus: string
paymentStatus: string
): Thunk<Promise<?Action>> {
return dispatch =>
dispatch(
Expand All @@ -356,7 +356,7 @@ export function updatePayment(
endpoint: `/events/${eventId}/registrations/${registrationId}/`,
method: 'PATCH',
body: {
chargeStatus
paymentStatus
},
meta: {
errorMessage: 'Oppdatering av betaling feilet'
Expand Down
7 changes: 4 additions & 3 deletions app/models.js
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export type EventRegistrationPhotoConsent =
| 'PHOTO_CONSENT'
| 'UNKNOWN';

export type EventRegistrationChargeStatus =
export type EventRegistrationPaymentStatus =
| 'pending'
| 'manual'
| 'succeeded'
Expand All @@ -137,10 +137,11 @@ export type EventRegistration = {
unregistrationDate: Dateish,
pool: number,
presence: EventRegistrationPresence,
chargeStatus: EventRegistrationChargeStatus,
paymentStatus: EventRegistrationPaymentStatus,
feedback: string,
sharedMemberships?: number,
consent: EventRegistrationPhotoConsent
consent: EventRegistrationPhotoConsent,
clientSecret?: string
};

type EventPoolBase = {
Expand Down
4 changes: 3 additions & 1 deletion app/reducers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import readme from './readme';
import surveySubmissions from './surveySubmissions';
import tags from './tags';
import fetchHistory from './fetchHistory';
import payments from './payments';
import { User } from '../actions/ActionTypes';
import joinReducers from 'app/utils/joinReducers';
import type { State, Action } from 'app/types';
Expand Down Expand Up @@ -96,7 +97,8 @@ const reducers = {
surveys,
tags,
toasts,
users
users,
payments
};

export type Reducers = typeof reducers;
Expand Down
42 changes: 41 additions & 1 deletion app/reducers/registrations.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ export default createEntityReducer({
case Event.ADMIN_REGISTER.SUCCESS:
case Event.SOCKET_REGISTRATION.SUCCESS:
case Event.PAYMENT_QUEUE.SUCCESS:
case Event.SOCKET_PAYMENT.SUCCESS:
case Event.SOCKET_PAYMENT.FAILURE: {
const registration = normalize(action.payload, registrationSchema)
.entities.registrations[action.payload.id];
Expand All @@ -46,6 +45,47 @@ export default createEntityReducer({
items: union(state.items, [registration.id])
};
}
case Event.SOCKET_PAYMENT.SUCCESS: {
const registration = normalize(action.payload, registrationSchema)
.entities.registrations[action.payload.id];
if (!registration) {
return state;
}
return {
...state,
byId: {
...state.byId,
[registration.id]: {
...omit(state.byId[registration.id], [
'unregistrationDate',
'clientSecret'
]),
...registration
}
},
items: union(state.items, [registration.id])
};
}
case Event.SOCKET_INITIATE_PAYMENT.SUCCESS: {
const registration = normalize(action.payload, registrationSchema)
.entities.registrations[action.payload.id];
const { clientSecret } = action.meta;
if (!registration) {
return state;
}
return {
...state,
byId: {
...state.byId,
[registration.id]: {
...omit(state.byId[registration.id], 'unregistrationDate'),
...registration,
clientSecret
}
},
items: union(state.items, [registration.id])
};
}
case Event.REQUEST_UNREGISTER.BEGIN: {
return {
...state,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import cx from 'classnames';
import LoadingIndicator from 'app/components/LoadingIndicator';
import Button from 'app/components/Button';
import type {
EventRegistrationChargeStatus,
EventRegistrationPaymentStatus,
EventRegistrationPresence,
ID
} from 'app/models';
Expand Down Expand Up @@ -36,9 +36,9 @@ type StripeStatusProps = {
id: ID,
handlePayment: (
registrationId: ID,
chargeStatus: EventRegistrationChargeStatus
paymentStatus: EventRegistrationPaymentStatus
) => Promise<*>,
chargeStatus: EventRegistrationChargeStatus
paymentStatus: EventRegistrationPaymentStatus
};

export const TooltipIcon = ({
Expand Down Expand Up @@ -92,23 +92,23 @@ export const PresenceIcons = ({
export const StripeStatus = ({
id,
handlePayment,
chargeStatus
paymentStatus
}: StripeStatusProps) => (
<Flex className={styles.presenceIcons}>
<TooltipIcon
content="Betalt stripe"
iconClass={cx('fa fa-cc-stripe', styles.greenIcon)}
transparent={chargeStatus !== 'succeeded'}
transparent={paymentStatus !== 'succeeded'}
/>
<TooltipIcon
content="Betalt manuelt"
transparent={chargeStatus !== 'manual'}
transparent={paymentStatus !== 'manual'}
iconClass={cx('fa fa-money', styles.greenIcon)}
onClick={() => handlePayment(id, 'manual')}
/>
<TooltipIcon
content="Ikke betalt"
transparent={['manual', 'succeeded'].includes(chargeStatus)}
transparent={['manual', 'succeeded'].includes(paymentStatus)}
iconClass={cx('fa fa-times', styles.crossIcon)}
onClick={() => handlePayment(id, 'failed')}
/>
Expand Down
8 changes: 4 additions & 4 deletions app/routes/events/components/EventAdministrate/Attendees.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import type {
User,
ID,
EventRegistration,
EventRegistrationChargeStatus,
EventRegistrationPaymentStatus,
EventRegistrationPresence
} from 'app/models';

Expand All @@ -34,7 +34,7 @@ export type Props = {
*
>,
updatePresence: (number, number, string) => Promise<*>,
updatePayment: (ID, ID, EventRegistrationChargeStatus) => Promise<*>,
updatePayment: (ID, ID, EventRegistrationPaymentStatus) => Promise<*>,
usersResult: Array<User>,
actionGrant: ActionGrant,
onQueryChanged: (value: string) => any,
Expand Down Expand Up @@ -73,9 +73,9 @@ export default class Attendees extends Component<Props, State> {

handlePayment = (
registrationId: number,
chargeStatus: EventRegistrationChargeStatus
paymentStatus: EventRegistrationPaymentStatus
) =>
this.props.updatePayment(this.props.eventId, registrationId, chargeStatus);
this.props.updatePayment(this.props.eventId, registrationId, paymentStatus);

render() {
const {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { WEBKOM_GROUP_ID } from 'app/utils/constants';
import type {
EventRegistration,
EventRegistrationPresence,
EventRegistrationChargeStatus,
EventRegistrationPaymentStatus,
ID,
Event
} from 'app/models';
Expand All @@ -31,7 +31,7 @@ type Props = {
) => Promise<*>,
handlePayment: (
registrationId: ID,
chargeStatus: EventRegistrationChargeStatus
paymentStatus: EventRegistrationPaymentStatus
) => Promise<*>,
handleUnregister: (registrationId: ID) => void,
clickedUnregister: ID,
Expand Down Expand Up @@ -164,13 +164,13 @@ export class RegisteredTable extends Component<Props> {
},
{
title: 'Betaling',
dataIndex: 'chargeStatus',
dataIndex: 'paymentStatus',
visible: event.isPriced,
center: true,
render: (chargeStatus, registration) => (
render: (paymentStatus, registration) => (
<StripeStatus
id={registration.id}
chargeStatus={chargeStatus}
paymentStatus={paymentStatus}
handlePayment={handlePayment}
/>
)
Expand Down
56 changes: 39 additions & 17 deletions app/routes/events/components/JoinEventForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,15 @@ import formStyles from 'app/components/Form/Field.css';
import moment from 'moment-timezone';
import { paymentSuccess, paymentManual } from '../utils';
import { registrationIsClosed } from '../utils';
import type { User, EventRegistration } from 'app/models';

type Event = Object;

export type Props = {
title?: string,
event: Event,
registration: ?Object,
currentUser: Object,
registration: ?EventRegistration,
currentUser: User,
onSubmit: Object => void,
createPaymentIntent: () => Promise<*>,

Expand Down Expand Up @@ -92,6 +93,32 @@ const SubmitButton = ({
);
};

const PaymentForm = ({
createPaymentIntent,
event,
currentUser,
registration
}: {
createPaymentIntent: () => Promise<*>,
event: Event,
currentUser: User,
registration: EventRegistration
}) => (
<div style={{ width: '100%' }}>
<div className={styles.joinHeader}>Betaling</div>
<div className={styles.eventPrice}>
Du skal betale {event.price / 100},-
</div>
<PaymentRequestForm
createPaymentIntent={createPaymentIntent}
event={event}
currentUser={currentUser}
paymentStatus={registration.paymentStatus}
clientSecret={registration.clientSecret}
/>
</div>
);

const SpotsLeft = ({ activeCapacity, spotsLeft }: SpotsLeftProps) => {
// If the pool has infinite capacity or spotsLeft isn't calculated don't show the message
if (!activeCapacity || spotsLeft === null) return null;
Expand Down Expand Up @@ -169,21 +196,22 @@ class JoinEventForm extends Component<Props> {
const showCaptcha =
!submitting && !registration && captchaOpen && event.useCaptcha;
const showStripe =
event.useStripe &&
event.isPriced &&
event.price > 0 &&
registration &&
registration.pool &&
![paymentManual, paymentSuccess].includes(registration.chargeStatus);
![paymentManual, paymentSuccess].includes(registration.paymentStatus);

if (registrationIsClosed(event)) {
return (
<>
{!formOpen && registration && showStripe && (
<PaymentRequestForm
<PaymentForm
createPaymentIntent={createPaymentIntent}
event={event}
currentUser={currentUser}
chargeStatus={registration.chargeStatus}
registration={registration}
/>
)}
</>
Expand Down Expand Up @@ -308,18 +336,12 @@ class JoinEventForm extends Component<Props> {
)}
</Form>
{registration && showStripe && (
<div style={{ width: '100%' }}>
<div className={styles.joinHeader}>Betaling</div>
<div className={styles.eventPrice}>
Du skal betale {event.price / 100},-
</div>
<PaymentRequestForm
createPaymentIntent={createPaymentIntent}
event={event}
currentUser={currentUser}
chargeStatus={registration.chargeStatus}
/>
</div>
<PaymentForm
event={event}
createPaymentIntent={createPaymentIntent}
currentUser={currentUser}
registration={registration}
/>
)}
</Flex>
)}
Expand Down
10 changes: 5 additions & 5 deletions app/routes/events/components/RegistrationMeta.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import React from 'react';
import type {
EventRegistrationPresence,
EventRegistrationChargeStatus,
EventRegistrationPaymentStatus,
EventRegistrationPhotoConsent
} from 'app/models';
import {
Expand Down Expand Up @@ -95,14 +95,14 @@ const PresenceStatus = ({
};

const PaymentStatus = ({
chargeStatus,
paymentStatus,
isPriced
}: {
chargeStatus: EventRegistrationChargeStatus,
paymentStatus: EventRegistrationPaymentStatus,
isPriced: boolean
}) => {
if (!isPriced) return null;
switch (chargeStatus) {
switch (paymentStatus) {
case paymentPending:
return (
<div>
Expand Down Expand Up @@ -179,7 +179,7 @@ const RegistrationMeta = ({
</Tooltip>
<PaymentStatus
isPriced={isPriced}
chargeStatus={registration.chargeStatus}
paymentStatus={registration.paymentStatus}
/>
</div>
)}
Expand Down
Loading

0 comments on commit aec2c43

Please sign in to comment.