initial commit

This commit is contained in:
2021-12-10 12:03:04 +00:00
commit c46c7ddbf0
3643 changed files with 582794 additions and 0 deletions

View File

@ -0,0 +1,28 @@
export const errorTypes = {
INVALID_EMAIL: 'email_invalid',
INVALID_REQUEST: 'invalid_request_error',
API_CONNECTION: 'api_connection_error',
API_ERROR: 'api_error',
AUTHENTICATION_ERROR: 'authentication_error',
RATE_LIMIT_ERROR: 'rate_limit_error',
CARD_ERROR: 'card_error',
VALIDATION_ERROR: 'validation_error',
};
export const errorCodes = {
INVALID_NUMBER: 'invalid_number',
INVALID_EXPIRY_MONTH: 'invalid_expiry_month',
INVALID_EXPIRY_YEAR: 'invalid_expiry_year',
INVALID_CVC: 'invalid_cvc',
INCORRECT_NUMBER: 'incorrect_number',
INCOMPLETE_NUMBER: 'incomplete_number',
INCOMPLETE_CVC: 'incomplete_cvc',
INCOMPLETE_EXPIRY: 'incomplete_expiry',
EXPIRED_CARD: 'expired_card',
INCORRECT_CVC: 'incorrect_cvc',
INCORRECT_ZIP: 'incorrect_zip',
INVALID_EXPIRY_YEAR_PAST: 'invalid_expiry_year_past',
CARD_DECLINED: 'card_declined',
MISSING: 'missing',
PROCESSING_ERROR: 'processing_error',
};

View File

@ -0,0 +1,3 @@
export * from './normalize';
export * from './utils';
export * from './load-stripe';

View File

@ -0,0 +1,22 @@
/**
* External dependencies
*/
import { loadStripe } from '@stripe/stripe-js';
/**
* Internal dependencies
*/
import { getApiKey } from './utils';
const stripePromise = () =>
new Promise( ( resolve ) => {
try {
resolve( loadStripe( getApiKey() ) );
} catch ( error ) {
// In order to avoid showing console error publicly to users,
// we resolve instead of rejecting when there is an error.
resolve( { error } );
}
} );
export { stripePromise as loadStripe };

View File

@ -0,0 +1,176 @@
/**
* @typedef {import('./type-defs').StripePaymentItem} StripePaymentItem
* @typedef {import('./type-defs').StripeShippingOption} StripeShippingOption
* @typedef {import('./type-defs').StripeShippingAddress} StripeShippingAddress
* @typedef {import('./type-defs').StripePaymentResponse} StripePaymentResponse
* @typedef {import('@woocommerce/type-defs/registered-payment-method-props').PreparedCartTotalItem} CartTotalItem
* @typedef {import('@woocommerce/type-defs/cart').CartShippingOption} CartShippingOption
* @typedef {import('@woocommerce/type-defs/shipping').ShippingAddress} CartShippingAddress
* @typedef {import('@woocommerce/type-defs/billing').BillingData} CartBillingAddress
*/
/**
* Normalizes incoming cart total items for use as a displayItems with the
* Stripe api.
*
* @param {CartTotalItem[]} cartTotalItems CartTotalItems to normalize
* @param {boolean} pending Whether to mark items as pending or
* not
*
* @return {StripePaymentItem[]} An array of PaymentItems
*/
const normalizeLineItems = ( cartTotalItems, pending = false ) => {
return cartTotalItems
.map( ( cartTotalItem ) => {
return cartTotalItem.value
? {
amount: cartTotalItem.value,
label: cartTotalItem.label,
pending,
}
: false;
} )
.filter( Boolean );
};
/**
* Normalizes incoming cart shipping option items for use as shipping options
* with the Stripe api.
*
* @param {CartShippingOption[]} shippingOptions An array of CartShippingOption items.
*
* @return {StripeShippingOption[]} An array of Stripe shipping option items.
*/
const normalizeShippingOptions = ( shippingOptions ) => {
const rates = shippingOptions[ 0 ].shipping_rates;
return rates.map( ( rate ) => {
return {
id: rate.rate_id,
label: rate.name,
detail: rate.description,
amount: parseInt( rate.price, 10 ),
};
} );
};
/**
* Normalize shipping address information from stripe's address object to
* the cart shipping address object shape.
*
* @param {StripeShippingAddress} shippingAddress Stripe's shipping address item
*
* @return {CartShippingAddress} The shipping address in the shape expected by
* the cart.
*/
const normalizeShippingAddressForCheckout = ( shippingAddress ) => {
const address = {
first_name: shippingAddress.recipient
.split( ' ' )
.slice( 0, 1 )
.join( ' ' ),
last_name: shippingAddress.recipient
.split( ' ' )
.slice( 1 )
.join( ' ' ),
company: '',
address_1:
typeof shippingAddress.addressLine[ 0 ] === 'undefined'
? ''
: shippingAddress.addressLine[ 0 ],
address_2:
typeof shippingAddress.addressLine[ 1 ] === 'undefined'
? ''
: shippingAddress.addressLine[ 1 ],
city: shippingAddress.city,
state: shippingAddress.region,
country: shippingAddress.country,
postcode: shippingAddress.postalCode.replace( ' ', '' ),
};
return address;
};
/**
* Normalizes shipping option shape selection from Stripe's shipping option
* object to the expected shape for cart shipping option selections.
*
* @param {StripeShippingOption} shippingOption The customer's selected shipping
* option.
*
* @return {string[]} An array of ids (in this case will just be one)
*/
const normalizeShippingOptionSelectionsForCheckout = ( shippingOption ) => {
return shippingOption.id;
};
/**
* Returns the billing data extracted from the stripe payment response to the
* CartBillingData shape.
*
* @param {StripePaymentResponse} paymentResponse Stripe's payment response
* object.
*
* @return {CartBillingAddress} The cart billing data
*/
const getBillingData = ( paymentResponse ) => {
const source = paymentResponse.source;
const name = source && source.owner.name;
const billing = source && source.owner.address;
const payerEmail = paymentResponse.payerEmail || '';
const payerPhone = paymentResponse.payerPhone || '';
return {
first_name: name ? name.split( ' ' ).slice( 0, 1 ).join( ' ' ) : '',
last_name: name ? name.split( ' ' ).slice( 1 ).join( ' ' ) : '',
email: ( source && source.owner.email ) || payerEmail,
phone:
( source && source.owner.phone ) ||
payerPhone.replace( '/[() -]/g', '' ),
country: ( billing && billing.country ) || '',
address_1: ( billing && billing.line1 ) || '',
address_2: ( billing && billing.line2 ) || '',
city: ( billing && billing.city ) || '',
state: ( billing && billing.state ) || '',
postcode: ( billing && billing.postal_code ) || '',
company: '',
};
};
/**
* This returns extra payment method data to add to the payment method update
* request made by the checkout processor.
*
* @param {StripePaymentResponse} paymentResponse A stripe payment response
* object.
* @param {string} paymentRequestType The payment request type
* used for payment.
*
* @return {Object} An object with the extra payment data.
*/
const getPaymentMethodData = ( paymentResponse, paymentRequestType ) => {
return {
payment_method: 'stripe',
stripe_source: paymentResponse.source
? paymentResponse.source.id
: null,
payment_request_type: paymentRequestType,
};
};
const getShippingData = ( paymentResponse ) => {
return paymentResponse.shippingAddress
? {
address: normalizeShippingAddressForCheckout(
paymentResponse.shippingAddress
),
}
: null;
};
export {
normalizeLineItems,
normalizeShippingOptions,
normalizeShippingAddressForCheckout,
normalizeShippingOptionSelectionsForCheckout,
getBillingData,
getPaymentMethodData,
getShippingData,
};

View File

@ -0,0 +1,324 @@
/**
* Stripe PaymentItem object
*
* @typedef {Object} StripePaymentItem
*
* @property {string} label The label for the payment item.
* @property {number} amount The amount for the payment item (in subunits)
* @property {boolean} [pending] Whether or not the amount is pending update on
* recalculation.
*/
/**
* Stripe ShippingOption object
*
* @typedef {Object} StripeShippingOption
*
* @property {string} id A unique ID for the shipping option.
* @property {string} label A short label for the shipping option.
* @property {string} detail A longer description for the shipping option.
* @property {number} amount The amount to show for the shipping option
* (in subunits)
*/
/**
* @typedef {Object} StripeShippingAddress
*
* @property {string} country Two letter country code, capitalized
* (ISO3166 alpha-2).
* @property {Array} addressLine An array of address line items.
* @property {string} region The most coarse subdivision of a
* country. (state etc)
* @property {string} city The name of a city, town, village etc.
* @property {string} postalCode The postal or ZIP code.
* @property {string} recipient The name of the recipient.
* @property {string} phone The phone number of the recipient.
* @property {string} [sortingCode] The sorting code as used in France.
* Not present on Apple platforms.
* @property {string} [dependentLocality] A logical subdivision of a city.
* Not present on Apple platforms.
*/
/**
* @typedef {Object} StripeBillingDetails
*
* @property {Object} address The billing address
* @property {string} address.city The billing address city
* @property {string} address.country The billing address country
* @property {string} address.line1 The first line for the address
* @property {string} address.line2 The second line fro the address
* @property {string} address.postal_code The postal/zip code
* @property {string} address.state The state
* @property {string} email The billing email
* @property {string} name The billing name
* @property {string} phone The billing phone
* @property {Object} [verified_address] The verified address of the owner.
* @property {string} [verified_email] Provided by the payment provider.
* @property {string} [verified_phone] Provided by the payment provider.
* @property {string} [verified_name] Provided by the payment provider.
*/
/**
* @typedef {Object} StripeBillingCard
*
* @property {string} brand The card brand
* @property {Object} checks Various security checks
* @property {string} checks.address_line1_check If an address line1 was
* provided, results of the
* check.
* @property {string} checks.address_postal_code_check If a postal code was
* provided, results of the
* check.
* @property {string} checks.cvc_check If CVC provided, results
* of the check.
* @property {string} country Two-letter ISO code for
* the country on the card.
* @property {number} exp_month Two-digit number for
* card expiry month.
* @property {number} exp_year Two-digit number for
* card expiry year.
* @property {string} fingerprint Uniquely identifies this
* particular card number
* @property {string} funding The card funding type
* @property {Object} generated_from Details of the original
* PaymentMethod that
* created this object.
* @property {string} last4 The last 4 digits of the
* card
* @property {Object} three_d_secure_usage Contains details on how
* this card may be used for
* 3d secure
* @property {Object} wallet If this card is part of a
* card wallet, this
* contains the details of
* the card wallet.
*/
/**
* @typedef {Object} StripePaymentMethod
*
* @property {string} id Unique identifier for the
* object
* @property {StripeBillingDetails} billing_details The billing details for the
* payment method
* @property {StripeBillingCard} card Details on the card used to
* pay
* @property {string} customer The ID of the customer to
* which this payment method
* is saved.
* @property {Object} metadata Set of key-value pairs that
* can be attached to the
* object.
* @property {string} type Type of payment method
* @property {string} object The type of object. Always
* 'payment_method'. Can use
* to validate!
* @property {Object} card_present If this is a card present
* payment method, contains
* details about that card
* @property {number} created The timestamp for when the
* card was created.
* @property {Object} fpx If this is an fpx payment
* method, contains details
* about it.
* @property {Object} ideal If this is an ideal payment
* method, contains details
* about it.
* @property {boolean} livemode True if the object exists
* in live mode or if in test
* mode.
* @property {Object} sepa_debit If this is a sepa_debit
* payment method, contains
* details about it.
*/
/**
* @typedef {Object} StripeSource
*
* @property {string} id Unique identifier for
* object
* @property {number} amount A positive number in
* the smallest currency
* unit.
* @property {string} currency The three-letter ISO
* code for the currency
* @property {string} customer The ID of the customer
* to which this source
* is attached.
* @property {Object} metadata Arbitrary key-value
* pairs that can be
* attached.
* @property {StripeBillingDetails} owner Information about the
* owner of the payment
* made.
* @property {Object} [redirect] Information related to
* the redirect flow
* (present if the source
* is authenticated by
* redirect)
* @property {string} statement_descriptor Extra information
* about a source (will
* appear on customer's
* statement)
* @property {string} status The status of the
* source.
* @property {string} type The type of the source
* (it is a payment
* method type)
* @property {string} object Value is "source" can
* be used to validate.
* @property {string} client_secret The client secret of
* the source. Used for
* client-side retrieval
* using a publishable
* key.
* @property {Object} [code_verification] Information related to
* the code verification
* flow.
* @property {number} created When the source object
* was instantiated
* (timestamp).
* @property {string} flow The authentication
* flow of the source.
* @property {boolean} livemode If true then payment
* is made in live mode
* otherwise test mode.
* @property {Object} [receiver] Information related to
* the receiver flow.
* @property {Object} source_order Information about the
* items and shipping
* associated with the
* source.
* @property {string} usage Whether source should
* be reusable or not.
*/
/**
* @typedef {Object} StripePaymentResponse
*
* @property {Object} token A stripe token object
* @property {StripePaymentMethod} paymentMethod The stripe payment method
* object
* @property {?StripeSource} source Present if this was the
* result of a source event
* listener
* @property {Function} complete Call this when the token
* data has been processed.
* @property {string} [payerName] The customer's name.
* @property {string} [payerEmail] The customer's email.
* @property {string} [payerPhone] The customer's phone.
* @property {StripeShippingAddress} [shippingAddress] The final shipping
* address the customer
* indicated
* @property {StripeShippingOption} [shippingOption] The final shipping
* option the customer
* selected.
* @property {string} methodName The unique name of the
* payment handler the
* customer chose to
* authorize payment
*/
/**
* @typedef {Object} StripePaymentRequestOptions The configuration of stripe
* payment request options to
* pass in.
*
* @property {string} country Two-letter (ISO)
* country code.
* @property {string} currency Three letter currency
* code.
* @property {StripePaymentItem} total Shown to the customer.
* @property {StripePaymentItem[]} displayItems Line items shown to the
* customer.
* @property {boolean} requestPayerName Whether or not to
* collect the payer's
* name.
* @property {boolean} requestPayerEmail Whether or not to
* collect the payer's
* email.
* @property {boolean} requestPayerPhone Whether or not to
* collect the payer's
* phone.
* @property {boolean} requestShipping Whether to collect
* shipping address.
* @property {StripeShippingOption[]} shippingOptions Available shipping
* options.
*/
/**
* @typedef {Object} StripePaymentRequest Stripe payment request object.
*
* @property {function():Promise} canMakePayment Returns a promise that resolves
* with an object detailing if a
* browser payment API is
* available.
* @property {function()} show Shows the browser's payment
* interface (called automatically
* if payment request button in
* use)
* @property {function()} update Used to update a PaymentRequest
* object.
* @property {function()} on For registering callbacks on
* payment request events.
*/
/**
* @typedef {Object} Stripe Stripe api object.
* @property {any} api Various api properties
*/
/**
* @typedef {Object} CreditCardIcon
*
* @property {string} url Url to icon.
* @property {string} alt Alt text for icon.
*/
/* eslint-disable jsdoc/valid-types */
// [k:string]:CreditCardIcon triggers the above rule even though VSCode interprets it fine.
/**
* @typedef {Object} StripeServerData
*
* @property {string} stripeTotalLabel The string used for payment
* descriptor.
* @property {string} publicKey The public api key for stripe
* requests.
* @property {boolean} allowPrepaidCard True means that prepaid cards
* can be used for payment.
* @property {Object} button Contains button styles
* @property {string} button.type The type of button.
* @property {string} button.theme The theme for the button.
* @property {string} button.height The height (in pixels) for
* the button.
* @property {string} button.locale The locale to use for stripe
* elements.
* @property {boolean} inline_cc_form Whether stripe cc should use
* inline cc
* form or separate inputs.
* @property {{[k:string]:CreditCardIcon}} icons Contains supported cc icons.
* @property {boolean} showSavedCards Used to indicate whether saved cards
* can be used.
* @property {boolean} showSaveOption Used to indicate whether the option to
* save card can be displayed.
* @property {boolean} allowPaymentRequest True if merchant has enabled payment
* request (Chrome/Apple Pay).
* @property {Object} supports List of features supported by the payment gateway
*/
/* eslint-enable jsdoc/valid-types */
/**
* @typedef {Object} StripeElementOptions
*
* @property {Object} options The configuration object for stripe
* elements.
* @property {function(boolean)} onActive A callback for setting whether an
* element is active or not. "Active"
* means it's not empty.
* @property {string} error Any error message from the stripe
* element.
* @property {function(string)} setError A callback for setting an error
* message.
*/
export {};

View File

@ -0,0 +1,279 @@
/**
* External dependencies
*/
import { getSetting } from '@woocommerce/settings';
import { __ } from '@wordpress/i18n';
/**
* Internal dependencies
*/
import { normalizeLineItems } from './normalize';
import { errorTypes, errorCodes } from './constants';
/**
* @typedef {import('./type-defs').StripeServerData} StripeServerData
* @typedef {import('./type-defs').StripePaymentItem} StripePaymentItem
* @typedef {import('./type-defs').StripePaymentRequest} StripePaymentRequest
* @typedef {import('@woocommerce/type-defs/registered-payment-method-props').PreparedCartTotalItem} CartTotalItem
*/
/**
* Stripe data comes form the server passed on a global object.
*
* @return {StripeServerData} Stripe server data.
*/
const getStripeServerData = () => {
const stripeServerData = getSetting( 'stripe_data', null );
if ( ! stripeServerData ) {
throw new Error( 'Stripe initialization data is not available' );
}
return stripeServerData;
};
/**
* Returns the public api key for the stripe payment method
*
* @throws Error
* @return {string} The public api key for the stripe payment method.
*/
const getApiKey = () => {
const apiKey = getStripeServerData().publicKey;
if ( ! apiKey ) {
throw new Error(
'There is no api key available for stripe. Make sure it is available on the wc.stripe_data.stripe.key property.'
);
}
return apiKey;
};
/**
* The total PaymentItem object used for the stripe PaymentRequest object.
*
* @param {CartTotalItem} total The total amount.
*
* @return {StripePaymentItem} The PaymentItem object used for stripe.
*/
const getTotalPaymentItem = ( total ) => {
return {
label:
getStripeServerData().stripeTotalLabel ||
__( 'Total', 'woocommerce' ),
amount: total.value,
};
};
/**
* Returns a stripe payment request object
*
* @param {Object} config A configuration object for
* getting the payment request.
* @param {Object} config.stripe The stripe api.
* @param {CartTotalItem} config.total The amount for the total
* (in subunits) provided by
* checkout/cart.
* @param {string} config.currencyCode The currency code provided
* by checkout/cart.
* @param {string} config.countryCode The country code provided by
* checkout/cart.
* @param {boolean} config.shippingRequired Whether or not shipping is
* required.
* @param {CartTotalItem[]} config.cartTotalItems Array of line items provided
* by checkout/cart.
*
* @return {StripePaymentRequest} A stripe payment request object
*/
const getPaymentRequest = ( {
stripe,
total,
currencyCode,
countryCode,
shippingRequired,
cartTotalItems,
} ) => {
const options = {
total: getTotalPaymentItem( total ),
currency: currencyCode,
country: countryCode || 'US',
requestPayerName: true,
requestPayerEmail: true,
requestPayerPhone: true,
requestShipping: shippingRequired,
displayItems: normalizeLineItems( cartTotalItems ),
};
return stripe.paymentRequest( options );
};
/**
* Utility function for updating the Stripe PaymentRequest object
*
* @param {Object} update An object containing the
* things needed for the
* update
* @param {StripePaymentRequest} update.paymentRequest A Stripe payment request
* object
* @param {CartTotalItem} update.total A total line item.
* @param {string} update.currencyCode The currency code for the
* amount provided.
* @param {CartTotalItem[]} update.cartTotalItems An array of line items
* provided by the
* cart/checkout.
*/
const updatePaymentRequest = ( {
paymentRequest,
total,
currencyCode,
cartTotalItems,
} ) => {
paymentRequest.update( {
total: getTotalPaymentItem( total ),
currency: currencyCode,
displayItems: normalizeLineItems( cartTotalItems ),
} );
};
/**
* Returns whether or not the current session can do apple pay.
*
* @param {StripePaymentRequest} paymentRequest A Stripe PaymentRequest instance.
*
* @return {Promise<Object>} True means apple pay can be done.
*/
const canDoPaymentRequest = ( paymentRequest ) => {
return new Promise( ( resolve ) => {
paymentRequest.canMakePayment().then( ( result ) => {
if ( result ) {
const paymentRequestType = result.applePay
? 'apple_pay'
: 'payment_request_api';
resolve( { canPay: true, requestType: paymentRequestType } );
return;
}
resolve( { canPay: false } );
} );
} );
};
const isNonFriendlyError = ( type ) =>
[
errorTypes.INVALID_REQUEST,
errorTypes.API_CONNECTION,
errorTypes.API_ERROR,
errorTypes.AUTHENTICATION_ERROR,
errorTypes.RATE_LIMIT_ERROR,
].includes( type );
const getErrorMessageForCode = ( code ) => {
const messages = {
[ errorCodes.INVALID_NUMBER ]: __(
'The card number is not a valid credit card number.',
'woocommerce-gateway-stripe'
),
[ errorCodes.INVALID_EXPIRY_MONTH ]: __(
'The card expiration month is invalid.',
'woocommerce-gateway-stripe'
),
[ errorCodes.INVALID_EXPIRY_YEAR ]: __(
'The card expiration year is invalid.',
'woocommerce-gateway-stripe'
),
[ errorCodes.INVALID_CVC ]: __(
'The card security code is invalid.',
'woocommerce-gateway-stripe'
),
[ errorCodes.INCORRECT_NUMBER ]: __(
'The card number is incorrect.',
'woocommerce-gateway-stripe'
),
[ errorCodes.INCOMPLETE_NUMBER ]: __(
'The card number is incomplete.',
'woocommerce-gateway-stripe'
),
[ errorCodes.INCOMPLETE_CVC ]: __(
'The card security code is incomplete.',
'woocommerce-gateway-stripe'
),
[ errorCodes.INCOMPLETE_EXPIRY ]: __(
'The card expiration date is incomplete.',
'woocommerce-gateway-stripe'
),
[ errorCodes.EXPIRED_CARD ]: __(
'The card has expired.',
'woocommerce-gateway-stripe'
),
[ errorCodes.INCORRECT_CVC ]: __(
'The card security code is incorrect.',
'woocommerce-gateway-stripe'
),
[ errorCodes.INCORRECT_ZIP ]: __(
'The card zip code failed validation.',
'woocommerce-gateway-stripe'
),
[ errorCodes.INVALID_EXPIRY_YEAR_PAST ]: __(
'The card expiration year is in the past',
'woocommerce-gateway-stripe'
),
[ errorCodes.CARD_DECLINED ]: __(
'The card was declined.',
'woocommerce-gateway-stripe'
),
[ errorCodes.MISSING ]: __(
'There is no card on a customer that is being charged.',
'woocommerce-gateway-stripe'
),
[ errorCodes.PROCESSING_ERROR ]: __(
'An error occurred while processing the card.',
'woocommerce-gateway-stripe'
),
};
return messages[ code ] || null;
};
const getErrorMessageForTypeAndCode = ( type, code = '' ) => {
switch ( type ) {
case errorTypes.INVALID_EMAIL:
return __(
'Invalid email address, please correct and try again.',
'woo-gutenberg-product-blocks'
);
case isNonFriendlyError( type ):
return __(
'Unable to process this payment, please try again or use alternative method.',
'woo-gutenberg-product-blocks'
);
case errorTypes.CARD_ERROR:
return getErrorMessageForCode( code );
case errorTypes.VALIDATION_ERROR:
return ''; // These are shown inline.
}
return null;
};
/**
* pluckAddress takes a full address object and returns relevant fields for calculating
* shipping, so we can track when one of them change to update rates.
*
* @param {Object} address An object containing all address information
* @param {string} address.country
* @param {string} address.state
* @param {string} address.city
* @param {string} address.postcode
*
* @return {Object} pluckedAddress An object containing shipping address that are needed to fetch an address.
*/
const pluckAddress = ( { country, state, city, postcode } ) => ( {
country,
state,
city,
postcode: postcode.replace( ' ', '' ).toUpperCase(),
} );
export {
getStripeServerData,
getApiKey,
getTotalPaymentItem,
getPaymentRequest,
updatePaymentRequest,
canDoPaymentRequest,
getErrorMessageForTypeAndCode,
pluckAddress,
};