Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Payment method - CashAppPay #2105

Merged
merged 34 commits into from
Jun 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
f168377
feat: initial draft
ribeiroguilherme Feb 8, 2023
159c040
feat: creating customer request
ribeiroguilherme Feb 9, 2023
4887870
feat: moved imports
ribeiroguilherme Feb 9, 2023
285f7a0
feat: fixed when payment is declined and retry happens
ribeiroguilherme Feb 10, 2023
a743058
feat: added missing types
ribeiroguilherme Feb 10, 2023
1155b5c
feat: added few tests
ribeiroguilherme Feb 15, 2023
7c2355f
feat: added more tests
ribeiroguilherme Feb 15, 2023
d9068a6
feat: added test for component
ribeiroguilherme Feb 15, 2023
d4593f8
feat: unsubscribing
ribeiroguilherme Feb 20, 2023
2411d23
feat: lock file
ribeiroguilherme Feb 20, 2023
41edcab
fix: tests
ribeiroguilherme Feb 22, 2023
6c0decd
feat: added test for ui element
ribeiroguilherme Feb 22, 2023
89aebcd
feat: more tests
ribeiroguilherme Feb 22, 2023
c7ea4a9
feat: cashapp custom pay button
ribeiroguilherme Apr 4, 2023
2c74807
Merge branch 'master' into feature/cashapp
ribeiroguilherme Apr 5, 2023
8862190
feat: storing payment method
ribeiroguilherme Apr 6, 2023
150023d
feat: showing as stored payment method
ribeiroguilherme Apr 6, 2023
215f674
feat: fixed tests. clean up
ribeiroguilherme Apr 6, 2023
29c2906
feat: added test for submit
ribeiroguilherme Apr 7, 2023
66d948b
feat: more tests
ribeiroguilherme Apr 7, 2023
91f2b2f
fix: clicking on checkbox doesnt trigger cashapp + stored pm button c…
ribeiroguilherme May 1, 2023
307b81a
Merge branch 'master' into feature/cashapp
ribeiroguilherme May 15, 2023
925fe05
fix: issue with super.prop within async arrow function
ribeiroguilherme May 15, 2023
de4b790
fix: unit tests
ribeiroguilherme May 15, 2023
962b89d
feat: displaying proper labels as stored payment method
ribeiroguilherme May 15, 2023
6fa0fd3
Merge branch 'master' into feature/cashapp
ribeiroguilherme May 16, 2023
3174047
Merge branch 'master' into feature/cashapp
ribeiroguilherme May 23, 2023
a75acb4
fix: label for stored pm
ribeiroguilherme May 23, 2023
d4910a1
feat: redirect button label change + minor fixes
ribeiroguilherme May 25, 2023
dab9454
feat: clean up code
ribeiroguilherme May 25, 2023
15c4fdb
feat: forEach instead of map
ribeiroguilherme May 30, 2023
f0b601e
Merge branch 'master' into feature/cashapp
ribeiroguilherme May 31, 2023
1bb875d
Merge branch 'main' into feature/cashapp
ribeiroguilherme Jun 1, 2023
7b9ff5e
fix: changeset
ribeiroguilherme Jun 1, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/witty-suns-beg.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@adyen/adyen-web': minor
---

Adding support for the payment method Cash App Pay
62 changes: 62 additions & 0 deletions packages/e2e-playwright/playwright-report/index.html

Large diffs are not rendered by default.

76 changes: 76 additions & 0 deletions packages/lib/src/components/CashAppPay/CashAppPay.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import CashAppPay from './CashAppPay';
import { render, screen } from '@testing-library/preact';
import CashAppService from './services/CashAppService';
import { CashAppPayEventData } from './types';

jest.mock('./services/CashAppService');
jest.spyOn(CashAppService.prototype, 'subscribeToEvent').mockImplementation(() => () => {});
const mockCreateCustomerRequest = jest.spyOn(CashAppService.prototype, 'createCustomerRequest').mockResolvedValue();
const mockBegin = jest.spyOn(CashAppService.prototype, 'begin').mockImplementation();

beforeEach(() => {
// @ts-ignore 'mockClear' is provided by jest.mock
CashAppService.mockClear();
mockCreateCustomerRequest.mockClear();
mockBegin.mockClear();
});

test('should return on-file data if available', () => {
const onFileGrantId = 'xxxx-yyyy';
const customerId = 'abcdef';
const cashTag = '$john-doe';

const cashAppPayElement = new CashAppPay({ storePaymentMethod: true });

const data: CashAppPayEventData = {
onFileGrantId,
cashTag,
customerId
};

cashAppPayElement.setState({ data });

expect(cashAppPayElement.formatData()).toEqual({
paymentMethod: { type: 'cashapp', onFileGrantId, customerId, cashtag: cashTag },
storePaymentMethod: true
});
});

test('should return grantId, customerId and correct txVariant', () => {
const grantId = 'xxxx-yyyy';
const customerId = 'abcdef';

const cashAppPayElement = new CashAppPay({});

const data: CashAppPayEventData = {
grantId,
customerId
};

cashAppPayElement.setState({ data });

expect(cashAppPayElement.formatData()).toEqual({ paymentMethod: { type: 'cashapp', grantId, customerId } });
});

test('should initially display the loading spinner while SDK is being loaded', async () => {
const cashAppPayElement = new CashAppPay({ i18n: global.i18n, loadingContext: 'test', modules: { resources: {} } });
render(cashAppPayElement.render());

expect(CashAppService).toHaveBeenCalledTimes(1);
expect(await screen.findByTestId('spinner')).toBeTruthy();
});

test('should create customer request and then begin CashApp flow when submit is triggered', async () => {
const onClick = jest.fn().mockImplementation(actions => actions.resolve());
const cashAppPayElement = new CashAppPay({ onClick, i18n: global.i18n, loadingContext: 'test', modules: { resources: {} } });
render(cashAppPayElement.render());

cashAppPayElement.submit();

expect(onClick).toHaveBeenCalledTimes(1);

await new Promise(process.nextTick);

expect(mockCreateCustomerRequest).toHaveBeenCalledTimes(1);
expect(mockBegin).toHaveBeenCalledTimes(1);
});
173 changes: 173 additions & 0 deletions packages/lib/src/components/CashAppPay/CashAppPay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import { h } from 'preact';
import UIElement from '../UIElement';
import CoreProvider from '../../core/Context/CoreProvider';
import { CashAppComponent } from './components/CashAppComponent';
import CashAppService from './services/CashAppService';
import { CashAppSdkLoader } from './services/CashAppSdkLoader';
import { CashAppPayElementData, CashAppPayElementProps, CashAppPayEventData } from './types';
import { ICashAppService } from './services/types';
import defaultProps from './defaultProps';
import RedirectButton from '../internal/RedirectButton';
import { payAmountLabel } from '../internal/PayButton';

export class CashAppPay extends UIElement<CashAppPayElementProps> {
public static type = 'cashapp';

private readonly cashAppService: ICashAppService | undefined;

protected static defaultProps = defaultProps;

constructor(props) {
super(props);

if (this.props.enableStoreDetails && this.props.storePaymentMethod) {
console.warn(
'CashAppPay: enableStoreDetails AND storePaymentMethod configuration properties should not be used together. That can lead to undesired behavior.'
);
}

if (this.props.storedPaymentMethodId) {
return;
}

this.cashAppService = new CashAppService(new CashAppSdkLoader(), {
storePaymentMethod: this.props.storePaymentMethod,
useCashAppButtonUi: this.props.showPayButton,
environment: this.props.environment,
amount: this.props.amount,
redirectURL: this.props.redirectURL,
clientId: this.props.configuration?.clientId,
scopeId: this.props.configuration?.scopeId,
button: this.props.button,
referenceId: this.props.referenceId
});
}

public formatProps(props: CashAppPayElementProps) {
return {
...props,
enableStoreDetails: props.session?.configuration?.enableStoreDetails || props.enableStoreDetails
};
}

public formatData(): CashAppPayElementData {
const { shopperWantsToStore, grantId, onFileGrantId, cashTag, customerId } = this.state.data || {};
const { storePaymentMethod: storePaymentMethodSetByMerchant, storedPaymentMethodId } = this.props;

/**
* We include 'storePaymentMethod' flag if we either Display the Checkbox OR if it is non-sessions flow AND the merchant wants to store the payment method
*/
const includeStorePaymentMethod = this.props.enableStoreDetails || (!this.props.session && storePaymentMethodSetByMerchant);

if (storedPaymentMethodId) {
return {
paymentMethod: {
type: CashAppPay.type,
storedPaymentMethodId
}
};
}

const shouldAddOnFileProperties = onFileGrantId && cashTag;

return {
paymentMethod: {
type: CashAppPay.type,
...(grantId && { grantId }),
...(customerId && { customerId }),
...(shouldAddOnFileProperties && { onFileGrantId, cashtag: cashTag })
},
...(includeStorePaymentMethod && { storePaymentMethod: storePaymentMethodSetByMerchant || shopperWantsToStore })
};
}

get displayName() {
if (this.props.storedPaymentMethodId && this.props.cashtag) {
return this.props.cashtag;
}
return this.props.name;
}

get additionalInfo() {
return this.props.storedPaymentMethodId ? 'Cash App Pay' : '';
}

public submit = () => {
const { onClick, storedPaymentMethodId } = this.props;

if (storedPaymentMethodId) {
super.submit();
return;
}

let onClickPromiseRejected = false;

new Promise<void>((resolve, reject) => onClick({ resolve, reject }))
.catch(() => {
onClickPromiseRejected = true;
throw Error('onClick rejected');
})
.then(() => {
return this.cashAppService.createCustomerRequest();
})
.then(() => {
this.cashAppService.begin();
})
.catch(error => {
if (onClickPromiseRejected) {
// Swallow exception triggered by onClick reject
return;
}
this.handleError(error);
});
};

public get isValid(): boolean {
return true;
}

private handleOnChangeStoreDetails = (storePayment: boolean) => {
const data = { ...this.state.data, shopperWantsToStore: storePayment };
this.setState({ data });
};

private handleAuthorize = (cashAppPaymentData: CashAppPayEventData): void => {
const data = { ...this.state.data, ...cashAppPaymentData };
this.setState({ data, valid: {}, errors: {}, isValid: true });
super.submit();
};

render() {
return (
<CoreProvider i18n={this.props.i18n} resources={this.resources} loadingContext={this.props.loadingContext}>
{this.props.storedPaymentMethodId ? (
<RedirectButton
label={payAmountLabel(this.props.i18n, this.props.amount)}
icon={this.resources?.getImage({ imageFolder: 'components/' })('lock')}
name={this.displayName}
amount={this.props.amount}
payButton={this.payButton}
onSubmit={this.submit}
ref={ref => {
this.componentRef = ref;
}}
/>
) : (
<CashAppComponent
ref={ref => {
this.componentRef = ref;
}}
enableStoreDetails={this.props.enableStoreDetails}
cashAppService={this.cashAppService}
onChangeStoreDetails={this.handleOnChangeStoreDetails}
onError={this.handleError}
onClick={this.submit}
onAuthorize={this.handleAuthorize}
/>
)}
</CoreProvider>
);
}
}

export default CashAppPay;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.adyen-checkout__cashapp > .adyen-checkout__store-details {
margin-top: 0;
margin-bottom: 16px;
}
Loading