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

POC - Centralized payment method definitions #10217

Draft
wants to merge 47 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
3849078
Sorry for the big commit - first pass at getting the php side working…
brettshumaker Jan 15, 2025
888fcc6
We need to access this method outside of the definition.
brettshumaker Jan 15, 2025
66ab9a1
Update afterpay class to use new definitions.
brettshumaker Jan 15, 2025
ab7c4fb
Fix dark icon file checking.
brettshumaker Jan 15, 2025
845ecd6
Push generated types to client mapping
brettshumaker Jan 15, 2025
ae13734
Fix tests.
brettshumaker Jan 16, 2025
9d0cc13
Fixing the tests revealed that the stripe_id here should just be the …
brettshumaker Jan 16, 2025
c3c093c
Add note to Afterpay
brettshumaker Jan 16, 2025
b0aece2
Make get_stripe_id a consistent trait for the definitions.
brettshumaker Jan 16, 2025
b0d8884
Use existing constant for definition ID.
brettshumaker Jan 16, 2025
70ec9bd
Address notes from Marcin
brettshumaker Feb 3, 2025
2baec4b
Revert "Address notes from Marcin"
brettshumaker Feb 11, 2025
87e8f88
Rename classes to psr-4 and update references
brettshumaker Feb 11, 2025
ffe2a5d
Remove BNPL trait and definition.
brettshumaker Feb 11, 2025
136a012
Remove Payment_Method_Icons trait and add direct icon URL methods to …
brettshumaker Feb 11, 2025
45bc090
Remove Base_Payment_Method trait and move some of its functionality t…
brettshumaker Feb 11, 2025
9ae210b
Moved to static methods and updated all references to reflect that.
brettshumaker Feb 12, 2025
df36af2
white space clean up and use the "correct" id for the Affirm payment …
brettshumaker Feb 13, 2025
5258803
Update to definition registry, the definition registration process, a…
brettshumaker Feb 13, 2025
8fd22ab
Make a new CLI Commands class for implementing our CLI command and an…
brettshumaker Feb 13, 2025
d436df5
Fix PSR-4 naming inconsistencies in includes/payment-methods/Configs
brettshumaker Feb 13, 2025
22e3e4f
init payment definitions at run time
brettshumaker Feb 13, 2025
58e1feb
Make PaymentMethodDefinitionRegistry consistently have instance methods
brettshumaker Feb 14, 2025
bf2c18c
Rearrange method order
brettshumaker Feb 14, 2025
db37a11
Make is_reusable/bnpl/accept_only_domestic_payment part of the defini…
brettshumaker Feb 14, 2025
be7c41f
Merge remote-tracking branch 'origin/develop' into poc/centralized-pa…
brettshumaker Feb 14, 2025
76a18d2
Remove unused `use` statements
brettshumaker Feb 14, 2025
f3d88c0
Rename payment method definition method
brettshumaker Feb 14, 2025
7795dff
Add support for manual capture to definition
brettshumaker Feb 14, 2025
ff219f6
Add manual capture to the definition interface - missed this in the l…
brettshumaker Feb 14, 2025
a035b82
Remove unneeded npm command and associated file.
brettshumaker Feb 14, 2025
521b941
Simplify mapping class
brettshumaker Feb 17, 2025
fad6bde
Auto-generate icon components when generating types for payment methods.
brettshumaker Feb 17, 2025
174689d
Fix psalm linting
brettshumaker Feb 18, 2025
b4e30c1
Merge branch 'develop' into poc/centralized-payment-method-definitions
brettshumaker Feb 18, 2025
c7b9b8e
Convert Klarna to new payment method definition
brettshumaker Feb 18, 2025
81f31e0
Add utility method for checking if a given currency is "domestic" for…
brettshumaker Feb 18, 2025
ceb5657
Remove unused method in definition.
brettshumaker Feb 18, 2025
dccdc2d
Fix js tests
brettshumaker Feb 18, 2025
8af7634
Merge branch 'develop' into poc/centralized-payment-method-definitions
brettshumaker Feb 18, 2025
3069f05
Fix Klarna tests
brettshumaker Feb 18, 2025
ad0bc2e
Create poc-centralized-payment-method-definitions
brettshumaker Feb 18, 2025
ee07ebc
Merge branch 'develop' into poc/centralized-payment-method-definitions
frosso Feb 19, 2025
5e06321
Revert "Fix js tests"
brettshumaker Feb 19, 2025
b3cd1dc
Actually fix js tests
brettshumaker Feb 19, 2025
a16c8f3
Resolve (compiling) circular dependency.
brettshumaker Feb 19, 2025
7be446b
Merge branch 'develop' into poc/centralized-payment-method-definitions
brettshumaker Feb 19, 2025
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
228 changes: 228 additions & 0 deletions build/payment-methods-plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
const { spawn } = require( 'child_process' );
const path = require( 'path' );
const fs = require( 'fs' );

// Helper function to recursively get all files in a directory
function getAllFiles( dirPath, arrayOfFiles = [] ) {
const files = fs.readdirSync( dirPath );

files.forEach( ( file ) => {
const fullPath = path.join( dirPath, file );
if ( fs.statSync( fullPath ).isDirectory() ) {
arrayOfFiles = getAllFiles( fullPath, arrayOfFiles );
} else {
arrayOfFiles.push( fullPath );
}
} );

return arrayOfFiles;
}

// Helper function to recursively remove a directory
function removeDirectory( dirPath ) {
if ( fs.existsSync( dirPath ) ) {
fs.readdirSync( dirPath ).forEach( ( file ) => {
const curPath = path.join( dirPath, file );
if ( fs.lstatSync( curPath ).isDirectory() ) {
removeDirectory( curPath );
} else {
fs.unlinkSync( curPath );
}
} );
fs.rmdirSync( dirPath );
}
}

class PaymentMethodsPlugin {
apply( compiler ) {
// Track if we've built the payment methods in this session
let hasBuilt = false;
// Track if we're currently building
let isBuilding = false;

// Create build directory if it doesn't exist
const buildDir = path.resolve( __dirname, './payment-methods' );
if ( ! fs.existsSync( buildDir ) ) {
fs.mkdirSync( buildDir, { recursive: true } );
}

// Add payment method files to webpack's watch list
compiler.hooks.afterCompile.tap(
'PaymentMethodsPlugin',
( compilation ) => {
const paymentMethodsDir = path.resolve(
__dirname,
'../includes/payment-methods/Configs'
);

// Add all files and directories recursively
if ( fs.existsSync( paymentMethodsDir ) ) {
// Add the root directory
compilation.fileDependencies.add( paymentMethodsDir );

// Add all files recursively
const allFiles = getAllFiles( paymentMethodsDir );
allFiles.forEach( ( file ) => {
compilation.fileDependencies.add( file );
} );

// Add all directories recursively
const addDirectory = ( dir ) => {
compilation.fileDependencies.add( dir );
fs.readdirSync( dir ).forEach( ( file ) => {
const fullPath = path.join( dir, file );
if ( fs.statSync( fullPath ).isDirectory() ) {
addDirectory( fullPath );
}
} );
};
addDirectory( paymentMethodsDir );
}
}
);

const buildPaymentMethods = ( callback ) => {
if ( isBuilding ) {
callback();
return;
}

isBuilding = true;
console.log( '\nBuilding payment method definitions...' );

Check warning on line 91 in build/payment-methods-plugin.js

View workflow job for this annotation

GitHub Actions / JS linting

Unexpected console statement

// Split the long path into multiple lines
const phpScriptPath = path.join(
'/var/www/html/wp-content/plugins',
'woocommerce-payments/includes/payment-methods',
'Configs/scripts/generate-payment-method-configs.php'
);

// Run the PHP script inside Docker
const phpScript = spawn( 'docker', [
'compose',
'exec',
'-T',
'wordpress',
'php',
phpScriptPath,
] );

phpScript.stdout.on( 'data', ( data ) => {
console.log( `PHP: ${ data }` );

Check warning on line 111 in build/payment-methods-plugin.js

View workflow job for this annotation

GitHub Actions / JS linting

Unexpected console statement
} );

phpScript.stderr.on( 'data', ( data ) => {
console.error( `PHP Error: ${ data }` );

Check warning on line 115 in build/payment-methods-plugin.js

View workflow job for this annotation

GitHub Actions / JS linting

Unexpected console statement
} );

phpScript.on( 'close', ( code ) => {
if ( code !== 0 ) {
console.error( 'PHP script failed' );

Check warning on line 120 in build/payment-methods-plugin.js

View workflow job for this annotation

GitHub Actions / JS linting

Unexpected console statement
isBuilding = false;
callback( new Error( 'PHP script failed' ) );
return;
}

console.log( 'PHP script completed successfully' );

Check warning on line 126 in build/payment-methods-plugin.js

View workflow job for this annotation

GitHub Actions / JS linting

Unexpected console statement

// Then run the JavaScript script
const jsScript = spawn(
'node',
[
path.resolve(
__dirname,
'../includes/payment-methods/Configs/scripts/generate-payment-method-types.js'
),
],
{
stdio: 'inherit',
}
);

jsScript.on( 'close', ( closeCode ) => {
if ( closeCode !== 0 ) {
console.error( 'JavaScript script failed' );

Check warning on line 144 in build/payment-methods-plugin.js

View workflow job for this annotation

GitHub Actions / JS linting

Unexpected console statement
isBuilding = false;
callback( new Error( 'JavaScript script failed' ) );
return;
}

console.log( 'Running eslint...' );

Check warning on line 150 in build/payment-methods-plugin.js

View workflow job for this annotation

GitHub Actions / JS linting

Unexpected console statement

// Run eslint --fix on the generated file
const eslintScript = spawn(
'npx',
[
'eslint',
'--fix',
'client/payment-methods/types.ts',
],
{
stdio: 'inherit',
}
);

eslintScript.on( 'close', ( eslintCode ) => {
isBuilding = false;
if ( eslintCode !== 0 ) {
console.error( 'Eslint failed' );

Check warning on line 168 in build/payment-methods-plugin.js

View workflow job for this annotation

GitHub Actions / JS linting

Unexpected console statement
callback( new Error( 'Eslint failed' ) );
return;
}

// Clean up build artifacts
removeDirectory( buildDir );

console.log(

Check warning on line 176 in build/payment-methods-plugin.js

View workflow job for this annotation

GitHub Actions / JS linting

Unexpected console statement
'Payment method definitions built successfully\n'
);
hasBuilt = true;
callback();
} );
} );
} );
};

// Run build when files change during watch
compiler.hooks.watchRun.tapAsync(
'PaymentMethodsPlugin',
( compilation, callback ) => {
// Build on initial run or when files have changed
if ( ! hasBuilt || compilation.modifiedFiles ) {
// Only check for payment method changes if we have modified files
if ( compilation.modifiedFiles ) {
const modifiedFiles = Array.from(
compilation.modifiedFiles || []
);

// Ignore changes to generated files
const hasPaymentMethodChanges = modifiedFiles.some(
( file ) =>
file.includes( '/payment-methods/Configs/' ) &&
! file.includes( 'build/' ) &&
! file.includes(
'client/payment-methods/types.ts'
)
);

if ( ! hasPaymentMethodChanges ) {
callback();
return;
}

console.log(

Check warning on line 213 in build/payment-methods-plugin.js

View workflow job for this annotation

GitHub Actions / JS linting

Unexpected console statement
'\nPayment method files changed, rebuilding...'
);
}

hasBuilt = false; // Reset build flag
buildPaymentMethods( callback );
} else {
callback();
}
}
);
}
}

module.exports = PaymentMethodsPlugin;
43 changes: 2 additions & 41 deletions client/payment-methods-map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@ import { __ } from '@wordpress/i18n';
*/

import {
AffirmIcon,
AfterpayIcon,
ClearpayIcon,
BancontactIcon,
BankDebitIcon,
CreditCardIcon,
Expand All @@ -24,7 +21,7 @@ import {
SofortIcon,
} from 'wcpay/payment-methods-icons';

const accountCountry = window.wcpaySettings?.accountStatus?.country || 'US';
import GeneratedPaymentMethodInformationObject from './payment-methods/generated-map';

export interface PaymentMethodMapEntry {
id: string;
Expand Down Expand Up @@ -168,43 +165,6 @@ const PaymentMethodInformationObject: Record<
allows_pay_later: false,
accepts_only_domestic_payment: false,
},
affirm: {
id: 'affirm',
label: __( 'Affirm', 'woocommerce-payments' ),
description: __(
'Allow customers to pay over time with Affirm.',
'woocommerce-payments'
),
icon: AffirmIcon,
currencies: [ 'USD', 'CAD' ],
stripe_key: 'affirm_payments',
allows_manual_capture: false,
allows_pay_later: true,
accepts_only_domestic_payment: true,
},
afterpay_clearpay: {
id: 'afterpay_clearpay',
label:
'GB' === accountCountry
? __( 'Clearpay', 'woocommerce-payments' )
: __( 'Afterpay', 'woocommerce-payments' ),
description:
'GB' === accountCountry
? __(
'Allow customers to pay over time with Clearpay.',
'woocommerce-payments'
)
: __(
'Allow customers to pay over time with Afterpay.',
'woocommerce-payments'
),
icon: 'GB' === accountCountry ? ClearpayIcon : AfterpayIcon,
currencies: [ 'USD', 'AUD', 'CAD', 'NZD', 'GBP' ],
stripe_key: 'afterpay_clearpay_payments',
allows_manual_capture: false,
allows_pay_later: true,
accepts_only_domestic_payment: true,
},
jcb: {
id: 'jcb',
label: __( 'JCB', 'woocommerce-payments' ),
Expand Down Expand Up @@ -233,6 +193,7 @@ const PaymentMethodInformationObject: Record<
allows_pay_later: true,
accepts_only_domestic_payment: true,
},
...GeneratedPaymentMethodInformationObject,
};

export default PaymentMethodInformationObject;
21 changes: 21 additions & 0 deletions client/payment-methods/generated-map.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* Internal dependencies
*/
import type { PaymentMethodMapEntry } from '../payment-methods-map';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not exactly sure how this is compiling (maybe because it's just importing the TS type?), but there seem to be a circular dependency, here. client/payment-methods/generated-map.ts is importing client/payment-methods-map.tsx. But client/payment-methods-map.tsx is also importing client/payment-methods/generated-map.ts.

Maybe PaymentMethodMapEntry should be separated in a different file.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, good catch! Resolved this in a16c8f3

import { PaymentMethodDefinitions } from './types';
import { mapDefinitionToEntry } from './mapping';

/**
* Generated payment method information using the backend-defined types
*/
const GeneratedPaymentMethodInformationObject: Record<
string,
PaymentMethodMapEntry
> = Object.fromEntries(
Object.entries( PaymentMethodDefinitions ).map( ( [ key, def ] ) => [
key,
mapDefinitionToEntry( def ),
] )
);

export default GeneratedPaymentMethodInformationObject;
54 changes: 54 additions & 0 deletions client/payment-methods/mapping.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* External dependencies
*/
import type { ImgHTMLAttributes, FunctionComponent } from 'react';

/**
* Internal dependencies
*/
import type { PaymentMethodDefinition } from './types';
import type { PaymentMethodMapEntry } from '../payment-methods-map';
import {
AffirmIcon,
AfterpayIcon,
ClearpayIcon,
} from '../payment-methods-icons';

type ReactImgFuncComponent = FunctionComponent<
ImgHTMLAttributes< HTMLImageElement >
>;

const accountCountry = window.wcpaySettings?.accountStatus?.country || 'US';

/**
* Maps payment method IDs to their corresponding icon components
*/
function getIconComponent( id: string ): ReactImgFuncComponent {
const iconMap: Record< string, ReactImgFuncComponent > = {
affirm: AffirmIcon,
afterpay_clearpay:
accountCountry === 'GB' ? ClearpayIcon : AfterpayIcon,
};
return iconMap[ id ];
}

/**
* Maps a PaymentMethodDefinition to a PaymentMethodMapEntry
*/
export function mapDefinitionToEntry(
def: PaymentMethodDefinition
): PaymentMethodMapEntry {
return {
id: def.id,
label: def.title,
description: def.description,
icon: getIconComponent( def.id ),
currencies: def.currencies,
stripe_key: def.stripeId,
allows_manual_capture: def.capabilities.includes( 'capture_later' ),
allows_pay_later: def.capabilities.includes( 'buy_now_pay_later' ),
accepts_only_domestic_payment: def.capabilities.includes(
'domestic_transactions_only'
),
};
}
Loading
Loading