Adding Storybook as dev playground (#1934)

* feat: draft

* feat: cleaned up

* feat: converted some files to ts

* fix: redirect result + typescript

* feat: cleaned up server

* feat: adding some stories

* feat: more changes

* feat: small adjustments

* feat: removed docs. cleaned preview

* feat: cleanup

* feat: clean up types

* feat: adding to window object

* feat: global loaders

* feat: added type to loaded checkout

* feat: storybook 7 + vite

* feat: cleanup deps not used by vite or storybook

* feat: removing unused import

* Update main.ts

* feat: webpack5

* feat: attempt to update packages

* refactor: move storybook to lib

* refactor: use rollup.dev.config.js

* refactor: redirect story fix and add a11y check

* refactor: rename story

* refactor: split rollup config

* refactor(storybook-config): use dev rollup config

* remove playground-storybook folder

* some fix

* run storybook https

* rebase main resolve conflicts

* add mirrored rollup dev plugins to vite

* correct postcss.config.js path

* fix returnUrl

* refactor: remove unused code and add types

* feat: cleaning up

---------

Co-authored-by: Yu Long <longyu901009@gmail.com>
This commit is contained in:
Guilherme Ribeiro
2023-07-11 16:16:36 +02:00
committed by GitHub
parent f5af3e1f5f
commit f189f4eede
42 changed files with 4544 additions and 96 deletions

1
.gitignore vendored
View File

@@ -1,6 +1,7 @@
node_modules
dist
coverage
storybook-static
*.log*
.env

View File

@@ -14,6 +14,7 @@
],
"scripts": {
"start": "concurrently --kill-others-on-fail \"yarn workspace @adyen/adyen-web start\" \"yarn workspace @adyen/adyen-web-playground start\" --names \"lib,playground\"",
"start:storybook": "yarn workspace @adyen/adyen-web start:storybook",
"build": "yarn workspace @adyen/adyen-web build",
"format": "yarn workspace @adyen/adyen-web format",
"lint": "yarn workspace @adyen/adyen-web lint",

View File

@@ -1,4 +1,5 @@
dist/
server/
config/
src/polyfills.ts
src/polyfills.ts
!.storybook

View File

@@ -5,7 +5,8 @@ module.exports = {
'eslint:recommended',
'plugin:react/recommended',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended' /*'prettier/@typescript-eslint'*/
'plugin:@typescript-eslint/recommended',
'plugin:storybook/recommended'
],
parserOptions: {
ecmaVersion: 2018,
@@ -41,8 +42,10 @@ module.exports = {
}
],
'no-console': 0,
'class-methods-use-this': 'off', // TODO
'no-underscore-dangle': 'off', // TODO
'class-methods-use-this': 'off',
// TODO
'no-underscore-dangle': 'off',
// TODO
'import/prefer-default-export': 'off',
'no-debugger': 'warn',
indent: 'off',
@@ -56,13 +59,19 @@ module.exports = {
tsx: 'never'
}
],
'import/no-extraneous-dependencies': ['error', { devDependencies: true }],
'import/no-extraneous-dependencies': [
'error',
{
devDependencies: true
}
],
'max-len': [
'error',
{
code: 150,
tabWidth: 2,
ignoreComments: true, // Allow long comments in the code
ignoreComments: true,
// Allow long comments in the code
ignoreUrls: true,
ignoreStrings: true,
ignoreTemplateLiterals: true
@@ -86,20 +95,36 @@ module.exports = {
radix: 'off',
// This serves no practical purpose
'eol-last': 'off',
// the base rule can report incorrect errors
'no-useless-constructor': 'off',
// Typescript Rules
'@typescript-eslint/no-unused-vars': ['error', { ignoreRestSiblings: true, vars: 'local' }],
'@typescript-eslint/no-unused-vars': [
'error',
{
ignoreRestSiblings: true,
vars: 'local'
}
],
'@typescript-eslint/explicit-member-accessibility': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/indent': 'off',
'@typescript-eslint/no-empty-function': ['error', { allow: ['arrowFunctions'] }],
'@typescript-eslint/ban-types': 'off', // TODO
'@typescript-eslint/ban-ts-comment': ['error', { 'ts-ignore': 'allow-with-description' }],
'@typescript-eslint/explicit-module-boundary-types': 'off', // TODO
'@typescript-eslint/no-empty-function': [
'error',
{
allow: ['arrowFunctions']
}
],
'@typescript-eslint/ban-types': 'off',
// TODO
'@typescript-eslint/ban-ts-comment': [
'error',
{
'ts-ignore': 'allow-with-description'
}
],
'@typescript-eslint/explicit-module-boundary-types': 'off',
// TODO
// React Rules
'react/prop-types': 'off',
@@ -107,7 +132,6 @@ module.exports = {
// TSDoc
'tsdoc/syntax': 'warn',
// a11y
'jsx-a11y/alt-text': 'error',
'jsx-a11y/aria-role': 'error',
@@ -127,11 +151,25 @@ module.exports = {
'jsx-a11y/mouse-events-have-key-events': 'error'
},
overrides: [
{
files: ['storybook/**/*.tsx'],
rules: {
'react/react-in-jsx-scope': 'off'
}
},
{
// enable the rule specifically for TypeScript files
files: ['*.ts', '*.tsx'],
rules: {
'@typescript-eslint/explicit-member-accessibility': ['error', { accessibility: 'off', overrides: { properties: 'explicit' } }]
'@typescript-eslint/explicit-member-accessibility': [
'error',
{
accessibility: 'off',
overrides: {
properties: 'explicit'
}
}
]
}
},
{

View File

@@ -1,2 +1,3 @@
dist
/*.tgz
/*.tgz
.stylelintcache

View File

@@ -0,0 +1,14 @@
html,
body {
font: 16px/1.21 -apple-system, BlinkMacSystemFont, sans-serif;
font-weight: 400;
background-color: #fbfbfb;
}
.component-wrapper {
background: #fff;
border: 1px solid #edf0f3;
max-width: 600px;
padding: 24px;
margin: auto;
}

View File

@@ -0,0 +1,59 @@
import type { StorybookConfig } from '@storybook/preact-vite';
import { mergeConfig, loadEnv } from 'vite';
import * as path from 'path';
import version = require('../config/version');
import eslint from '@rollup/plugin-eslint';
import stylelint from 'vite-plugin-stylelint';
const currentVersion = version();
const config: StorybookConfig = {
stories: ['../storybook/**/*.stories.mdx', '../storybook/**/*.stories.@(js|jsx|ts|tsx)'],
addons: [
{
name: '@storybook/addon-essentials',
options: {
docs: false
}
},
{
name: '@storybook/addon-a11y'
}
],
framework: {
name: '@storybook/preact-vite',
options: {}
},
async viteFinal(config, options) {
const env = loadEnv(options.configType, path.resolve('../../', '.env'), '');
return mergeConfig(config, {
define: {
'process.env': env,
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV),
'process.env.VERSION': JSON.stringify(currentVersion.ADYEN_WEB_VERSION),
'process.env.COMMIT_HASH': JSON.stringify(currentVersion.COMMIT_HASH),
'process.env.COMMIT_BRANCH': JSON.stringify(currentVersion.COMMIT_BRANCH),
'process.env.ADYEN_BUILD_ID': JSON.stringify(currentVersion.ADYEN_BUILD_ID),
'process.env.__SF_ENV__': JSON.stringify(env.SF_ENV || 'build')
},
server: {
watch: {
usePolling: true
}
},
plugins: [
stylelint(),
{
...eslint({
include: ['./src/**'],
exclude: ['./src/**/*.json', './src/**/*.scss']
}),
enforce: 'pre',
apply: 'serve'
}
]
});
}
};
export default config;

View File

@@ -0,0 +1,9 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import { addons } from '@storybook/manager-api';
/**
* https://storybook.js.org/docs/html/configure/features-and-behavior
*/
addons.setConfig({
panelPosition: 'right'
});

View File

@@ -0,0 +1,7 @@
const checkoutDevServer = require('@adyen/adyen-web-server');
const middleware = router => {
checkoutDevServer(router);
};
module.exports = middleware;

View File

@@ -0,0 +1,43 @@
import './main.css';
import '../src/style/index.scss';
import { Preview } from '@storybook/preact';
import { DEFAULT_COUNTRY_CODE, DEFAULT_SHOPPER_LOCALE, DEFAULT_AMOUNT_VALUE } from '../storybook/config/commonConfig';
import { createCheckout } from '../storybook/helpers/create-checkout';
const preview: Preview = {
argTypes: {
useSessions: {
control: 'boolean'
},
countryCode: {
control: 'text'
},
shopperLocale: {
control: 'text'
},
amount: {
control: 'number'
},
showPayButton: {
control: 'boolean'
}
},
args: {
useSessions: true,
countryCode: DEFAULT_COUNTRY_CODE,
shopperLocale: DEFAULT_SHOPPER_LOCALE,
amount: DEFAULT_AMOUNT_VALUE,
showPayButton: true
},
loaders: [
async context => {
if (context.componentId.includes('redirectresult')) {
return {};
}
const checkout = await createCheckout(context);
return { checkout };
}
]
};
export default preview;

View File

@@ -0,0 +1,13 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const { execSync } = require('child_process');
const path = require('path');
require('dotenv').config({ path: path.resolve('../../', '.env') });
const isHttps = process.env.IS_HTTPS === 'true';
const certPath = process.env.CERT_PATH ?? path.resolve(__dirname, 'localhost.pem');
const certKeyPath = process.env.CERT_KEY_PATH ?? path.resolve(__dirname, 'localhost-key.pem');
const runStorybook = 'storybook dev --port 3020 --disable-telemetry';
const runStorybookHttps = `storybook dev --port 3020 --https --ssl-cert ${certPath} --ssl-key ${certKeyPath} --disable-telemetry`;
execSync(isHttps ? runStorybookHttps : runStorybook, { stdio: 'inherit' });

View File

@@ -71,7 +71,7 @@ export async function getPlugins(analyze = isBundleAnalyzer) {
postcss({
use: ['sass'],
config: {
path: 'config/postcss.config.js'
path: 'postcss.config.js'
},
sourceMap: true,
inject: false,

View File

@@ -34,9 +34,11 @@
"scripts": {
"start": "npm run dev-server",
"dev-server": "cross-env NODE_ENV=development rollup --watch --config config/rollup.dev.config.js",
"start:storybook": "node .storybook/run.js",
"docs:generate": "typedoc --out docs src --exclude \"**/*+(index|.test).ts\"",
"build": "rm -rf dist/ && npm run type-check-generate && cross-env NODE_ENV=production rollup --config config/rollup.config.js",
"build:analyze": "rm -rf dist/ && cross-env NODE_ENV=analyze rollup --config config/rollup.config.js",
"build:storybook": "storybook build --disable-telemetry",
"test": "jest --config config/jest.config.js",
"test:watch": "npm run test -- --watchAll",
"test:coverage": "npm run test -- --coverage",
@@ -54,12 +56,13 @@
"prepare": "cd ../.. && husky install packages/lib/.husky"
},
"devDependencies": {
"@adyen/adyen-web-server": "1.0.0",
"@babel/cli": "^7.18.10",
"@babel/core": "^7.16.0",
"@babel/plugin-transform-runtime": "^7.19.1",
"@babel/preset-env": "^7.20.2",
"@babel/preset-env": "^7.21.5",
"@babel/preset-react": "^7.18.6",
"@babel/preset-typescript": "^7.15.0",
"@babel/preset-typescript": "^7.21.5",
"@babel/runtime-corejs3": "^7.20.1",
"@rollup/plugin-babel": "^6.0.2",
"@rollup/plugin-commonjs": "^24.0.0",
@@ -67,6 +70,11 @@
"@rollup/plugin-json": "^6.0.0",
"@rollup/plugin-node-resolve": "^15.0.0",
"@rollup/plugin-replace": "^5.0.1",
"@storybook/addon-a11y": "^7.0.20",
"@storybook/addon-essentials": "^7.0.20",
"@storybook/manager-api": "^7.0.20",
"@storybook/preact": "^7.0.20",
"@storybook/preact-vite": "^7.0.20",
"@testing-library/jest-dom": "5.16.5",
"@testing-library/preact": "3.2.3",
"@testing-library/preact-hooks": "1.1.0",
@@ -85,6 +93,7 @@
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsx-a11y": "^6.6.1",
"eslint-plugin-react": "^7.31.8",
"eslint-plugin-storybook": "^0.6.12",
"eslint-plugin-testing-library": "^5.9.1",
"eslint-plugin-tsdoc": "^0.2.17",
"filesize": "^10.0.0",
@@ -95,16 +104,21 @@
"jest-mock-extended": "^3.0.1",
"lint-staged": "^13.0.3",
"postcss": "8.4.23",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"rollup": "^2.79.1",
"rollup-plugin-postcss": "4.0.2",
"rollup-plugin-stylelint": "1.0.0",
"rollup-plugin-terser": "^7.0.2",
"rollup-plugin-visualizer": "^5.8.3",
"sass": "1.62.1",
"storybook": "^7.0.20",
"stylelint": "15.10.1",
"stylelint-config-standard-scss": "7.0.1",
"tslib": "^2.4.1",
"typescript": "4.9.5",
"vite": "4.3.9",
"vite-plugin-stylelint": "^4.3.0",
"whatwg-fetch": "^3.6.2"
},
"dependencies": {

View File

@@ -0,0 +1,5 @@
console.log('Using postcss plugins...');
module.exports = {
// eslint-disable-next-line @typescript-eslint/no-var-requires
plugins: [require('autoprefixer'), require('cssnano')({ preset: ['default', { colormin: false }] })]
};

View File

@@ -0,0 +1,6 @@
const protocol = process.env.IS_HTTPS === 'true' ? 'https' : 'http';
export const DEFAULT_SHOPPER_LOCALE = 'en-US';
export const DEFAULT_COUNTRY_CODE = 'US';
export const DEFAULT_AMOUNT_VALUE = 25900;
export const SHOPPER_REFERENCE = 'newshoppert';
export const RETURN_URL = `${protocol}://localhost:3020/?path=/story/helpers-redirectresult--redirect-result`;

View File

@@ -0,0 +1,30 @@
import { SHOPPER_REFERENCE } from './commonConfig';
const paymentMethodsConfig = {
channel: 'Web',
shopperReference: SHOPPER_REFERENCE,
shopperName: {
firstName: 'Jan',
lastName: 'Jansen',
gender: 'MALE'
},
telephoneNumber: '0612345678',
shopperEmail: 'test@adyen.com',
dateOfBirth: '1970-07-10'
// billingAddress: {
// city: 'Gravenhage',
// country: commonConfiguration.countryCode,
// houseNumberOrName: '1',
// postalCode: '2521VA',
// street: 'Neherkade'
// },
// deliveryAddress: {
// city: 'Gravenhage',
// country: commonConfiguration.countryCode,
// houseNumberOrName: '2',
// postalCode: '2521VA',
// street: 'Neherkade'
// }
};
export default paymentMethodsConfig;

View File

@@ -0,0 +1,49 @@
const identifier = new Date().getMilliseconds();
const protocol = process.env.IS_HTTPS === 'true' ? 'https' : 'http';
const { origin = `${protocol}://localhost:3020`, search } = window.location;
const returnUrl = origin + search;
const paymentsConfig = {
origin,
returnUrl,
reference: `${identifier}-checkout-components-ref`,
additionalData: {
// Force response code. See https://docs.adyen.com/development-resources/test-cards/result-code-testing/adyen-response-codes
// RequestedTestAcquirerResponseCode: 2,
allow3DS2: true
},
shopperEmail: 'test-shopper@storytel.com',
shopperIP: '172.30.0.1',
// threeDS2RequestData: {
// authenticationOnly: false
// },
channel: 'Web',
browserInfo: {
acceptHeader: 'http'
},
lineItems: [
{
taxPercentage: 0,
id: 'item1',
taxAmount: 0,
description: 'Test Item 1',
amountIncludingTax: 75900,
quantity: 1,
taxCategory: 'None',
amountExcludingTax: 75900
},
{
taxPercentage: 0,
id: 'item2',
taxAmount: 0,
description: 'Test Item 2',
amountIncludingTax: 10000,
quantity: 1,
taxCategory: 'None',
amountExcludingTax: 10000
}
]
};
export default paymentsConfig;

View File

@@ -0,0 +1,19 @@
interface IResult {
resultCode?: string;
resultMessage?: string;
}
export const Result = ({ resultCode, resultMessage }: IResult) => {
const isAuthorized = resultCode === 'Authorised' || resultCode === 'Received';
const imgSrc = isAuthorized
? 'https://checkoutshopper-test.adyen.com/checkoutshopper/images/components/success.gif'
: 'https://checkoutshopper-test.adyen.com/checkoutshopper/images/components/error.gif';
const imgAlt = isAuthorized ? 'success' : 'error';
return (
<div className="redirect-result">
<img src={imgSrc} alt={imgAlt} className="result-img"></img>
{resultCode} {resultMessage}
</div>
);
};

View File

@@ -0,0 +1,39 @@
import paymentMethodsConfig from '../config/paymentMethodsConfig';
import paymentsConfig from '../config/paymentsConfig';
import { httpPost } from '../utils/http-post';
import { PaymentMethodsResponse } from '../../src/core/ProcessResponse/PaymentMethodsResponse/types';
import { RawPaymentResponse } from '../../src/components/types';
import { CheckoutSessionSetupResponse, Order, OrderStatus, PaymentAction, PaymentAmount } from '../../src/types';
export const getPaymentMethods = async (configuration?: any): Promise<PaymentMethodsResponse> =>
await httpPost('paymentMethods', { ...paymentMethodsConfig, ...configuration });
export const makePayment = async (stateData: any, paymentData: any): Promise<RawPaymentResponse> => {
const paymentRequest = { ...paymentsConfig, ...stateData, ...paymentData };
if (paymentRequest.order) delete paymentRequest.amount; // why?
return await httpPost('payments', paymentRequest);
};
export const makeDetailsCall = async (detailsData: {
details: { redirectResult: string };
}): Promise<{ resultCode: string; action?: PaymentAction }> => await httpPost('details', detailsData);
export const createSession = async (data: any): Promise<CheckoutSessionSetupResponse> => {
return await httpPost('sessions', { ...data, lineItems: paymentsConfig.lineItems });
};
export const checkBalance = async (
giftcardStateData: any
): Promise<{
pspReference: string;
resultCode: string;
balance: {
currency: string;
value: number;
};
}> => await httpPost('paymentMethods/balance', giftcardStateData);
export const createOrder = async (amount: PaymentAmount): Promise<Order & OrderStatus> =>
await httpPost('orders', { reference: `order-reference-${Date.now()}`, amount });
export const cancelOrder = async (order: Order): Promise<{ resultCode: string; pspReference: string }> => await httpPost('orders/cancel', order);

View File

@@ -0,0 +1,105 @@
import { makePayment, makeDetailsCall, getPaymentMethods } from './checkout-api-calls';
import UIElement from '../../src/components/UIElement';
import Core from '../../src/core';
function displayResultMessage(isAuthorized: boolean, resultCode: string): void {
const image = document.createElement('img');
image.setAttribute(
'src',
isAuthorized
? 'https://checkoutshopper-test.adyen.com/checkoutshopper/images/components/success.gif'
: 'https://checkoutshopper-test.adyen.com/checkoutshopper/images/components/error.gif'
);
image.setAttribute('height', '100');
image.style.display = 'flex';
image.style.margin = 'auto auto 30px';
const resultText = document.createElement('div');
resultText.setAttribute('data-testid', 'result-message');
resultText.style.textAlign = 'center';
resultText.textContent = resultCode;
const container = document.getElementById('component-root');
container.appendChild(image);
container.appendChild(resultText);
}
export function handleFinalState(result: any, component: UIElement): void {
const isDropin = component?.props?.isDropin;
const isAuthorized = result.resultCode === 'Authorised' || result.resultCode === 'Received';
if (isDropin) {
if (isAuthorized) {
component.setStatus('success');
} else {
component.setStatus('error');
}
return;
}
if (component?.unmount) {
component.unmount();
}
displayResultMessage(isAuthorized, result.resultCode);
}
export async function handleResponse(response, component, checkout, paymentData?) {
if (response.action) {
component.handleAction(response.action);
return;
}
if (response.order && response.order?.remainingAmount?.value > 0) {
const order = {
orderData: response.order.orderData,
pspReference: response.order.pspReference
};
const orderPaymentMethods = await getPaymentMethods({
order,
amount: paymentData.amount,
shopperLocale: paymentData.shopperLocale
});
checkout.update({
paymentMethodsResponse: orderPaymentMethods,
order,
amount: response.order.remainingAmount
});
return;
}
handleFinalState(response, component);
}
export function handleChange(state: any, component: UIElement) {
console.groupCollapsed(`onChange - ${state.data.paymentMethod.type}`);
console.log('isValid', state.isValid);
console.log('data', state.data);
console.log('node', component._node);
console.log('state', state);
console.groupEnd();
}
export function handleError(error, component) {
// SecuredField related errors should not go straight to console.error
if (error.type === 'card') {
console.log('### Card::onError:: obj=', error);
} else {
console.error(error.name, error.message, error.stack, component);
}
}
export async function handleSubmit(state: any, component: UIElement, checkout: Core, paymentData: any) {
component.setStatus('loading');
const response = await makePayment(state.data, paymentData);
component.setStatus('ready');
await handleResponse(response, component, checkout, paymentData);
}
export async function handleAdditionalDetails(details, component: UIElement, checkout: Core) {
component.setStatus('loading');
const response = await makeDetailsCall(details.data);
component.setStatus('ready');
await handleResponse(response, component, checkout);
}

View File

@@ -0,0 +1,85 @@
import AdyenCheckout from '../../src/index';
import { cancelOrder, checkBalance, createOrder, getPaymentMethods } from './checkout-api-calls';
import { handleAdditionalDetails, handleChange, handleError, handleSubmit } from './checkout-handlers';
import getCurrency from '../utils/get-currency';
import { AdyenCheckoutProps } from '../stories/types';
import { PaymentMethodsResponse } from '../../src/core/ProcessResponse/PaymentMethodsResponse/types';
import Checkout from '../../src/core/core';
async function createAdvancedFlowCheckout({
showPayButton,
paymentMethodsConfiguration,
countryCode,
shopperLocale,
amount
}: AdyenCheckoutProps): Promise<Checkout> {
const paymentAmount = {
currency: getCurrency(countryCode),
value: Number(amount)
};
const paymentMethodsResponse: PaymentMethodsResponse = await getPaymentMethods({ amount: paymentAmount, shopperLocale, countryCode });
const checkout = await AdyenCheckout({
clientKey: process.env.CLIENT_KEY,
environment: process.env.CLIENT_ENV,
amount: paymentAmount,
countryCode,
paymentMethodsResponse,
locale: shopperLocale,
showPayButton,
paymentMethodsConfiguration,
onSubmit: (state, component) => {
const paymentData = {
amount: paymentAmount,
countryCode,
shopperLocale
};
handleSubmit(state, component, checkout, paymentData);
},
onChange: (state, component) => {
handleChange(state, component);
},
onAdditionalDetails: async (state, component) => {
await handleAdditionalDetails(state, component, checkout);
},
onBalanceCheck: async (resolve, reject, data) => {
try {
const res = await checkBalance(data);
resolve(res);
} catch (e) {
reject(e);
}
},
onOrderRequest: async (resolve, reject) => {
try {
const order = await createOrder(paymentAmount);
resolve(order);
} catch (e) {
reject(e);
}
},
onOrderCancel: async order => {
await cancelOrder(order);
await checkout.update({
paymentMethodsResponse: await getPaymentMethods({ amount: paymentAmount, shopperLocale, countryCode }),
order: null,
amount: paymentAmount
});
},
onError: (error, component) => {
handleError(error, component);
}
});
return checkout;
}
export { createAdvancedFlowCheckout };

View File

@@ -0,0 +1,18 @@
import { createSessionsCheckout } from './create-sessions-checkout';
import { createAdvancedFlowCheckout } from './create-advanced-checkout';
async function createCheckout(context: any): Promise<any> {
const { useSessions, paymentMethodsConfiguration, showPayButton, countryCode, shopperLocale, amount } = context.args;
return useSessions
? await createSessionsCheckout({ showPayButton, paymentMethodsConfiguration, countryCode, shopperLocale, amount })
: await createAdvancedFlowCheckout({
paymentMethodsConfiguration,
showPayButton,
countryCode,
shopperLocale,
amount
});
}
export { createCheckout };

View File

@@ -0,0 +1,56 @@
import AdyenCheckout from '../../src/index';
import { createSession } from './checkout-api-calls';
import { RETURN_URL, SHOPPER_REFERENCE } from '../config/commonConfig';
import { handleChange, handleError, handleFinalState } from './checkout-handlers';
import getCurrency from '../utils/get-currency';
import { AdyenCheckoutProps } from '../stories/types';
import Checkout from '../../src/core/core';
async function createSessionsCheckout({
showPayButton,
paymentMethodsConfiguration,
countryCode,
shopperLocale,
amount
}: AdyenCheckoutProps): Promise<Checkout> {
const session = await createSession({
amount: {
currency: getCurrency(countryCode),
value: Number(amount)
},
shopperLocale,
countryCode,
reference: 'ABC123',
returnUrl: RETURN_URL,
shopperReference: SHOPPER_REFERENCE,
shopperEmail: 'shopper.ctp1@adyen.com'
});
const checkout = await AdyenCheckout({
clientKey: process.env.CLIENT_KEY,
environment: process.env.CLIENT_ENV,
session,
showPayButton,
paymentMethodsConfiguration,
// @ts-ignore TODO: Fix beforeSubmit type
beforeSubmit: (data, component, actions) => {
actions.resolve(data);
},
onPaymentCompleted: (result, component) => {
handleFinalState(result, component);
},
onError: (error, component) => {
handleError(error, component);
},
onChange: (state, component) => {
handleChange(state, component);
}
});
return checkout;
}
export { createSessionsCheckout };

View File

@@ -0,0 +1,22 @@
import { useEffect, useRef } from 'preact/hooks';
import Core from '../../src/core';
import { PaymentMethodOptions, PaymentMethods } from '../../src/types';
interface IContainer<T extends keyof PaymentMethods> {
type: T;
componentConfiguration: PaymentMethodOptions<T>;
checkout: Core;
}
export const Container = <T extends keyof PaymentMethods>({ type, componentConfiguration, checkout }: IContainer<T>) => {
const container = useRef(null);
useEffect(() => {
if (!checkout) {
return;
}
checkout.create(type, { ...componentConfiguration }).mount(container.current);
}, []);
return <div ref={container} id="component-root" className="component-wrapper" />;
};

View File

@@ -0,0 +1,37 @@
import { Meta, StoryObj } from '@storybook/preact';
import { DropinStoryProps } from './types';
import { getStoryContextCheckout } from '../utils/get-story-context-checkout';
import { Container } from './Container';
type DropinStory = StoryObj<DropinStoryProps>;
const meta: Meta<DropinStoryProps> = {
title: 'Dropin/Default',
argTypes: {
componentConfiguration: {
control: 'object'
},
paymentMethodsConfiguration: {
control: 'object'
}
},
args: {
componentConfiguration: {
instantPaymentTypes: ['googlepay']
},
paymentMethodsConfiguration: {
googlepay: {
buttonType: 'plain'
}
}
}
};
export default meta;
export const Default: DropinStory = {
render: (args, context) => {
const checkout = getStoryContextCheckout(context);
return <Container type={'dropin'} componentConfiguration={args.componentConfiguration} checkout={checkout} />;
}
};

View File

@@ -0,0 +1,116 @@
import { Meta, StoryObj } from '@storybook/preact';
import { PaymentMethodStoryProps } from '../types';
import { getStoryContextCheckout } from '../../utils/get-story-context-checkout';
import { CardElementProps } from '../../../src/components/Card/types';
import { Container } from '../Container';
type CardStory = StoryObj<PaymentMethodStoryProps<CardElementProps> & { txVariant: string }>;
const meta: Meta<PaymentMethodStoryProps<CardElementProps>> = {
title: 'Cards/Card'
};
export default meta;
const createComponent = (args, context) => {
const { txVariant = 'card', componentConfiguration } = args;
const checkout = getStoryContextCheckout(context);
return <Container type={txVariant} componentConfiguration={componentConfiguration} checkout={checkout} />;
};
export const Default: CardStory = {
render: createComponent,
args: {
componentConfiguration: {
_disableClickToPay: true
}
}
};
export const WithAVS: CardStory = {
render: createComponent,
args: {
componentConfiguration: {
_disableClickToPay: true,
billingAddressRequired: true,
billingAddressAllowedCountries: ['US', 'CA', 'GB'],
data: {
billingAddress: {
street: 'Virginia Street',
postalCode: '95014',
city: 'Cupertino',
houseNumberOrName: '1',
country: 'US',
stateOrProvince: 'CA'
}
}
}
}
};
export const WithPartialAVS: CardStory = {
render: createComponent,
args: {
componentConfiguration: {
_disableClickToPay: true,
billingAddressRequired: true,
billingAddressMode: 'partial'
}
}
};
export const WithInstallments: CardStory = {
render: createComponent,
args: {
componentConfiguration: {
_disableClickToPay: true,
showBrandsUnderCardNumber: true,
showInstallmentAmounts: true,
installmentOptions: {
mc: {
values: [1, 2, 3]
},
visa: {
values: [1, 2, 3, 4],
plans: ['regular', 'revolving']
}
}
}
}
};
export const BCMC: CardStory = {
render: createComponent,
args: {
txVariant: 'bcmc',
componentConfiguration: {
_disableClickToPay: true
}
}
};
export const KCP: CardStory = {
render: createComponent,
args: {
componentConfiguration: {
_disableClickToPay: true,
// Set koreanAuthenticationRequired AND countryCode so KCP fields show at start
// Just set koreanAuthenticationRequired if KCP fields should only show if korean_local_card entered
configuration: {
koreanAuthenticationRequired: true
},
countryCode: 'KR'
}
}
};
export const WithClickToPay: CardStory = {
render: createComponent,
args: {
componentConfiguration: {
clickToPayConfiguration: {
shopperEmail: 'gui.ctp@adyen.com',
merchantDisplayName: 'Adyen Merchant Name'
}
}
}
};

View File

@@ -0,0 +1,35 @@
import { Meta, StoryObj } from '@storybook/preact';
import { PaymentMethodStoryProps } from '../types';
import { getStoryContextCheckout } from '../../utils/get-story-context-checkout';
import { PixProps } from '../../../src/components/Pix/types';
import { Container } from '../Container';
type PixStory = StoryObj<PaymentMethodStoryProps<PixProps>>;
const meta: Meta<PaymentMethodStoryProps<PixProps>> = {
title: 'Components/Pix'
};
export default meta;
const createComponent = (args, context) => {
const checkout = getStoryContextCheckout(context);
return <Container type={'pix'} componentConfiguration={args.componentConfiguration} checkout={checkout} />;
};
export const Default: PixStory = {
render: createComponent,
args: {
countryCode: 'BR'
}
};
export const WithPersonalDetails: PixStory = {
render: createComponent,
args: {
...Default.args,
// @ts-ignore TODO: Make Pix 'introduction' prop optional
componentConfiguration: {
personalDetailsRequired: true
}
}
};

View File

@@ -0,0 +1,26 @@
import { Meta, StoryObj } from '@storybook/preact';
import { PaymentMethodStoryProps } from '../types';
import { getStoryContextCheckout } from '../../utils/get-story-context-checkout';
import { UPIElementProps } from '../../../src/components/UPI/types';
import { Container } from '../Container';
type UpiStory = StoryObj<PaymentMethodStoryProps<UPIElementProps>>;
const meta: Meta<PaymentMethodStoryProps<UPIElementProps>> = {
title: 'Components/UPI'
};
export default meta;
export const UPI: UpiStory = {
render: (args, context) => {
const checkout = getStoryContextCheckout(context);
return <Container type={'upi'} componentConfiguration={args.componentConfiguration} checkout={checkout} />;
},
args: {
countryCode: 'IN',
componentConfiguration: {
// @ts-ignore Seems like enum isnt the best way to export fixed strings
defaultMode: 'vpa'
}
}
};

View File

@@ -0,0 +1,24 @@
import { Meta, StoryObj } from '@storybook/preact';
import { getSearchParameter } from '../../utils/get-query-parameters';
import { RedirectResultContainer } from './RedirectResultContainer';
type RedirectResultProps = {
redirectResult: string;
sessionId: string;
};
type RedirectStory = StoryObj<RedirectResultProps>;
const meta: Meta<RedirectResultProps> = {
title: 'Helpers/RedirectResult'
};
export default meta;
export const RedirectResult: RedirectStory = {
render: args => <RedirectResultContainer {...args} />,
args: {
redirectResult: getSearchParameter('redirectResult'),
sessionId: getSearchParameter('sessionId')
}
};

View File

@@ -0,0 +1,38 @@
import { useEffect, useState } from 'preact/hooks';
import AdyenCheckout from '../../../src';
import { handleError, handleFinalState } from '../../helpers/checkout-handlers';
export const RedirectResultContainer = ({ redirectResult, sessionId }) => {
const [isRedirecting, setIsRedirecting] = useState<boolean>(true);
let message = isRedirecting ? 'Submitting details...' : '';
useEffect(() => {
if (!redirectResult || !sessionId) {
message = 'There is no redirectResult / sessionId provided';
return;
}
AdyenCheckout({
clientKey: process.env.CLIENT_KEY,
environment: process.env.CLIENT_ENV,
session: { id: sessionId },
onPaymentCompleted: (result, component) => {
setIsRedirecting(false);
handleFinalState(result, component);
},
onError: (error, component) => {
setIsRedirecting(false);
handleError(error, component);
}
}).then(checkout => {
setIsRedirecting(true);
checkout.submitDetails({ details: { redirectResult } });
});
}, [sessionId]);
return (
<div id="component-root" className="component-wrapper">
{message}
</div>
);
};

View File

@@ -0,0 +1,22 @@
import { Meta, StoryObj } from '@storybook/preact';
import { PaymentMethodStoryProps } from '../types';
import { getStoryContextCheckout } from '../../utils/get-story-context-checkout';
import { UIElementProps } from '../../../src/components/types';
import { Container } from '../Container';
type DotpayStory = StoryObj<PaymentMethodStoryProps<UIElementProps>>;
const meta: Meta<PaymentMethodStoryProps<UIElementProps>> = {
title: 'IssuerList/Dotpay'
};
export default meta;
export const Dotpay: DotpayStory = {
render: (args, context) => {
const checkout = getStoryContextCheckout(context);
return <Container type={'dotpay'} componentConfiguration={args.componentConfiguration} checkout={checkout} />;
},
args: {
countryCode: 'PL'
}
};

View File

@@ -0,0 +1,22 @@
import { Meta, StoryObj } from '@storybook/preact';
import { PaymentMethodStoryProps } from '../types';
import { getStoryContextCheckout } from '../../utils/get-story-context-checkout';
import { UIElementProps } from '../../../src/components/types';
import { Container } from '../Container';
type EntercashStory = StoryObj<PaymentMethodStoryProps<UIElementProps>>;
const meta: Meta<PaymentMethodStoryProps<UIElementProps>> = {
title: 'IssuerList/Entercash'
};
export default meta;
export const Entercash: EntercashStory = {
render: (args, context) => {
const checkout = getStoryContextCheckout(context);
return <Container type={'entercash'} componentConfiguration={args.componentConfiguration} checkout={checkout} />;
},
args: {
countryCode: 'FI'
}
};

View File

@@ -0,0 +1,35 @@
import { Meta, StoryObj } from '@storybook/preact';
import { PaymentMethodStoryProps } from '../types';
import { getStoryContextCheckout } from '../../utils/get-story-context-checkout';
import { Container } from '../Container';
import { UIElementProps } from '../../../src/components/types';
type IdealStory = StoryObj<PaymentMethodStoryProps<UIElementProps>>;
const meta: Meta<PaymentMethodStoryProps<UIElementProps>> = {
title: 'IssuerList/IDEAL'
};
export default meta;
const createComponent = (args, context) => {
const checkout = getStoryContextCheckout(context);
return <Container type={'ideal'} componentConfiguration={args.componentConfiguration} checkout={checkout} />;
};
export const Default: IdealStory = {
render: createComponent,
args: {
countryCode: 'NL'
}
};
export const WithHighlightedIssuers: IdealStory = {
render: createComponent,
args: {
...Default.args,
componentConfiguration: {
// @ts-ignore TODO: 'highlightedIssuers' is not documented
highlightedIssuers: ['1121', '1154', '1153']
}
}
};

View File

@@ -0,0 +1,25 @@
import { DropinElementProps } from '../../src/components/Dropin/types';
type GlobalStoryProps = {
useSessions: boolean;
countryCode: string;
shopperLocale: string;
amount: number;
showPayButton: boolean;
};
export interface PaymentMethodStoryProps<T> extends GlobalStoryProps {
componentConfiguration: T;
}
export interface DropinStoryProps extends PaymentMethodStoryProps<DropinElementProps> {
paymentMethodsConfiguration: any;
}
export type AdyenCheckoutProps = {
showPayButton: boolean;
paymentMethodsConfiguration?: Record<string, object>;
countryCode: string;
shopperLocale: string;
amount: number;
};

View File

@@ -0,0 +1,22 @@
import { Meta, StoryObj } from '@storybook/preact';
import { PaymentMethodStoryProps } from '../types';
import { getStoryContextCheckout } from '../../utils/get-story-context-checkout';
import { UIElementProps } from '../../../src/components/types';
import { Container } from '../Container';
type OxxoStory = StoryObj<PaymentMethodStoryProps<UIElementProps>>;
const meta: Meta<PaymentMethodStoryProps<UIElementProps>> = {
title: 'Vouchers/Oxxo'
};
export default meta;
export const Oxxo: OxxoStory = {
render: (args, context) => {
const checkout = getStoryContextCheckout(context);
return <Container type={'oxxo'} componentConfiguration={args.componentConfiguration} checkout={checkout} />;
},
args: {
countryCode: 'MX'
}
};

View File

@@ -0,0 +1,19 @@
import { Meta, StoryObj } from '@storybook/preact';
import { PaymentMethodStoryProps } from '../types';
import { getStoryContextCheckout } from '../../utils/get-story-context-checkout';
import { PayPalElementProps } from '../../../src/components/PayPal/types';
import { Container } from '../Container';
type Story = StoryObj<PaymentMethodStoryProps<PayPalElementProps>>;
const meta: Meta = {
title: 'Wallets/Paypal'
};
export default meta;
export const Paypal: Story = {
render: (args, context) => {
const checkout = getStoryContextCheckout(context);
return <Container type={'paypal'} componentConfiguration={args.componentConfiguration} checkout={checkout} />;
}
};

View File

@@ -0,0 +1,8 @@
import { IUIElement } from '../../src/components/types';
const addToWindow = (component: IUIElement) => {
// @ts-ignore ignore
window.component = component;
};
export { addToWindow };

View File

@@ -0,0 +1,39 @@
const currencies: Record<string, string> = {
AR: 'ARS',
AU: 'AUD',
BR: 'BRL',
CA: 'CAD',
CH: 'CHF',
CN: 'CNY',
CZ: 'CZK',
DK: 'DKK',
GB: 'GBP',
HK: 'HKD',
HR: 'HRK',
HU: 'HUN',
ID: 'IDR',
IN: 'INR',
JP: 'JPY',
KR: 'KRW',
MG: 'MGA',
MX: 'MXN',
MY: 'MYR',
NO: 'NOK',
NZ: 'NZD',
PH: 'PHP',
PL: 'PLN',
RO: 'RON',
RU: 'RUB',
SE: 'SEK',
SG: 'SGD',
SK: 'SKK',
TH: 'THB',
TW: 'TWD',
US: 'USD',
VN: 'VND',
default: 'EUR'
};
const getCurrency = (countryCode: string): string => currencies[countryCode] || currencies.default;
export default getCurrency;

View File

@@ -0,0 +1,14 @@
export const getSearchParameter = (parameter: string, queryString = window.location.search): string => {
const urlParams = new URLSearchParams(queryString);
return urlParams.get(parameter);
};
export const getSearchParameters = (queryString = window.location.search): Record<string, string> => {
const urlParams = new URLSearchParams(queryString);
const parameters: Record<string, string> = {};
// @ts-ignore
for (const entry of urlParams.entries()) {
parameters[entry[0]] = entry[1];
}
return parameters;
};

View File

@@ -0,0 +1,8 @@
import Core from '../../src/core';
const getStoryContextCheckout = (context): Core | undefined => {
const { checkout } = context.loaded;
return checkout;
};
export { getStoryContextCheckout };

View File

@@ -0,0 +1,13 @@
const { host, protocol } = window.location;
export async function httpPost<T>(endpoint: string, data: any): Promise<T> {
const response = await fetch(`${protocol}//${host}/${endpoint}`, {
method: 'POST',
headers: {
Accept: 'application/json, text/plain, */*',
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
return await response.json();
}

3464
yarn.lock

File diff suppressed because it is too large Load Diff