Skip to content

Instantly share code, notes, and snippets.

@eloisetaylor5693
Last active April 6, 2023 13:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save eloisetaylor5693/e56c27f9cedbfe059b8a2783ad9f4bc5 to your computer and use it in GitHub Desktop.
Save eloisetaylor5693/e56c27f9cedbfe059b8a2783ad9f4bc5 to your computer and use it in GitHub Desktop.
Paypal currency issue (Case # 12117471)

Paypal currency issue (Case # 12117471)

Issue

The pay later feature errors for currencies which are not the default currency in the paypal account. For our PP accounts, there's an error on rendering the components when the currency is not GBP. We found the "Invalid option value" occurs on the messaging component.

Our first release will only include the messaging, then we'll be showing a pay later button.

Errors

When testing locally or using a vpn to test different currencies we see this warning in the console (using our sandbox merchant account client ID):

paypal_messages_render_warning {
    "description": 'Invalid option value (currency). Expected GBP but received "EUR"', 
    "timestamp": '1680184043826'
}

when we use client id sb, we get the opposite error: USD is expected

paypal_messages_render_warning {
    "description": "Invalid option value (currency). Expected USD but received \"GBP\"",
    "timestamp": "1680520941497"
}

The pay later button is never eligible and shows this error when we comment out the isEligible check. We intend to implement before productionising this code, but want to see the error until we resolve the issues

{
    "err": "Error: paylater is not eligible\n    at https://www.paypal.com/sdk/js?client-id=AbADuvTHGOm702siHcZLuqVKfFt3bXRtKpFLTNa1IKQyKKI8As_JE2M6emsEZajHI6n38j-oHRPzaD15&enable-funding=paylater&components=buttons,messages,funding-eligibility&currency=GBP:2:187581\n    at e.try (https://www.paypal.com/sdk/js?client-id=AbADuvTHGOm702siHcZLuqVKfFt3bXRtKpFLTNa1IKQyKKI8As_JE2M6emsEZajHI6n38j-oHRPzaD15&enable-funding=paylater&components=buttons,messages,funding-eligibility&currency=GBP:2:77394)\n    at v (https://www.paypal.com/sdk/js?client-id=AbADuvTHGOm702siHcZLuqVKfFt3bXRtKpFLTNa1IKQyKKI8As_JE2M6emsEZajHI6n38j-oHRPzaD15&enable-funding=paylater&components=buttons,messages,funding-eligibility&currency=GBP:2:187555)\n    at Object.render (https://www.paypal.com/sdk/js?client-id=AbADuvTHGOm702siHcZLuqVKfFt3bXRtKpFLTNa1IKQyKKI8As_JE2M6emsEZajHI6n38j-oHRPzaD15&enable-funding=paylater&components=buttons,messages,funding-eligibility&currency=GBP:2:188815)\n    at https://assets.playground.futurelearn.com/packs/packs/checkout.compat-f04eebaefddaddce9509-1.5.js:1:4859",
    "timestamp": "1680517955072",
    "referer": "bnpl-pp-sdk-backend.playground.futurelearn.com",
    "uid": "uid_0070311f21_mta6mja6mti",
    "env": "sandbox"
}

Questions:

  1. How can we make the pay later messaging work for other currencies than GBP? For every other currency than GBP we get Invalid option value (currency). Expected GBP but received "EUR"
  2. How can we make the pay later button pass the eligibility check? For every currency we get paylater is not eligible, even though the transaction amount is more than the minimum for pay later (we're testing on our short courses which are £75 or more)

Simple webpage example where it's also occuring:

http://miniature-family.surge.sh/
https://github.com/eloisetaylor5693/paypal-pay-later-demo

Context

We have a ruby on rails app, where the checkout html is a rails haml view, with some javascript scripts to enhance the behaviour.

We are currently using braintree and paypal API, but they didn't support pay later components: messaging and button. Therefore we started using the Javascript SDK. The aim is to get rid of braintree and just use the SDK

All client IDs we used have the same behaviour. We've tried client id sb, the prod client ID and generated sandbox client ids by creating sandbox merchant accounts. SB ID is the worst for our use-case because our own client IDs from the accounts we created work for GBP, but the sb account wants USD. Our main customer base is in the UK, and India. So we thought if we don't find a resolution for this then we can launch to just the UK. I found the client IDs from here: https://developer.paypal.com/dashboard/applications/sandbox

Our Implementation

Key info

#pp-pay-later-message: ID for the pay later messaging element
#paypal-pay-later-button: ID for the pay later button
inferred_currency_code: Currency code derrived from user's currency code if browsing from UK = GBP, if france then EUR

Scripts which are injected into the checkout page

   config.future_learn.paypal.script_srcs = %w(
      https://js.braintreegateway.com/web/3.31.0/js/client.min.js
      https://js.braintreegateway.com/web/3.31.0/js/paypal-checkout.min.js
      https://www.paypalobjects.com/api/checkout.js
    )

Files

File contents are in this gist

  • new.html.haml - checkout view
  • _paypal_payment_method.html.haml - partial view with the braintree payment button
  • index.js - Javascript logic which intialises the various clients, and renders the components
  • package.json:
    "@paypal/paypal-js": "^5.1.6",
.js-paypal-container{ hidden: 'hidden', data: { payment_method: PaymentMethod::Paypal::PAYMENT_METHOD_NAME } }
- if paypal_temporarily_unavailable?
%p.old-text-typescale--small.u-no-margin-bottom
%span.error
PayPal is temporarily unavailable. Please refresh the page or try again later.
- else
%p.old-text-typescale--small.u-no-margin-bottom
Please click ‘Pay with PayPal’ to complete your payment on PayPal.
.u-interval
= render_react('components/Application/components/Checkout/PaymentDisclaimers', { context: checkout_context, currencyCode: currency_code })
.u-interval
#paypal-button{ hidden: 'hidden', data: { payment_method: PaymentMethod::Paypal::PAYMENT_METHOD_NAME } }
.u-interval
#paypal-pay-later-button
import requireAsync from 'application/application/view/checkout/requireAsync';
import { loadScript } from '@paypal/paypal-js';
import { emitPaymentSucceeded, on as onCheckoutEvent, CheckoutEvent } from '../checkout/events.ts';
import { checkFormValidity, validateCheckout, getOrderDetails } from '../checkout';
const paypalPaymentMethod = (
{ urls, clientToken, environment, paypalSdkClientID = 'sb', isEligibleForPayLater = true },
eventTracker,
) => {
let paypalApi = null;
let braintreeApi = null;
let paypalSDK = null;
const setEnabled = (actions, formValid) => {
actions.disable();
if (formValid) {
actions.enable();
}
};
const createPayPalButton = (paypalCheckoutInstance, currency, amount) => {
paypalApi.Button.render(
{
env: environment,
commit: true,
style: {
color: 'blue',
label: 'pay',
shape: 'rect',
size: 'large',
tagline: false,
},
payment: () => {
return paypalCheckoutInstance.createPayment({
amount,
currency,
displayName: 'FutureLearn',
enableShippingAddress: false,
flow: 'checkout',
intent: 'sale',
});
},
onAuthorize: (data) => {
paypalCheckoutInstance.tokenizePayment(data).then((payload) => {
eventTracker.track('paypal-token-generation-succeeded');
emitPaymentSucceeded({ gateway: 'paypal', nonce: payload.nonce });
});
},
onError: () => {
eventTracker.track('paypal-payment-error');
},
validate: (actions) => {
setEnabled(actions, checkFormValidity());
onCheckoutEvent(CheckoutEvent.OrderFormEdited, (event) => {
const { formValid } = event.detail;
setEnabled(actions, formValid);
});
},
onClick: () => {
validateCheckout();
},
},
'#paypal-button',
);
};
/** early version of the button */
const createPaypalPayLaterButton = async () => {
if (!isEligibleForPayLater || !paypalSDK) {
return;
}
try {
const payLaterButton = await paypalSDK.Buttons({
fundingSource: paypalSDK.FUNDING.PAYLATER,
style: {
layout: 'text',
color: 'gold',
},
});
// if (payLaterButton.isEligible) {
payLaterButton.render('#paypal-pay-later-button');
// }
} catch (error) {
eventTracker.track('paypal-pay-later-button-failed-to-load-error', error);
}
};
const createPaypalPayLaterMessagingElement = async (currency, amount) => {
if (!isEligibleForPayLater || !paypalSDK) {
return;
}
try {
const message = await paypalSDK.Messages({
amount,
currency,
style: {
layout: 'text',
logo: { type: 'none' },
},
});
message.render('#pp-pay-later-message');
} catch (error) {
eventTracker.track('paypal-pay-later-messaging-failed-to-load-error', error);
}
};
const createPayPalCheckout = (braintreeInstance) => {
braintreeApi.paypalCheckout.create({ client: braintreeInstance }, (error, instance) => {
if (error) {
return;
}
const { currency, amount } = getOrderDetails().priceTotal;
createPayPalButton(instance, currency, amount);
createPaypalPayLaterButton();
createPaypalPayLaterMessagingElement(currency, amount);
});
};
/**
* @deprecated We should use the paypal packages instead of Braintree. This is a constant cause of confusion
* */
const createBraintreeClient = () => {
braintreeApi.client.create({ authorization: clientToken }, (error, instance) => {
if (error) {
return;
}
createPayPalCheckout(instance);
});
};
const createPaypalSdkClient = () => {
const { currency } = getOrderDetails().priceTotal;
loadScript({
'client-id': paypalSdkClientID,
'data-namespace': 'paypalSDK',
'enable-funding': 'paylater',
components: 'buttons,messages,funding-eligibility',
currency,
})
.then((paypal) => {
paypalSDK = paypal;
})
.catch((err) => {
eventTracker.track('paypal-SDK-failed-to-initialise-error', err);
});
};
const init = async () => {
await requireAsync(urls);
createPaypalSdkClient();
braintreeApi = window.braintree;
paypalApi = window.paypal;
if (!braintreeApi) {
eventTracker.track('braintree-not-found-error');
return;
}
if (!paypalApi) {
eventTracker.track('paypal-not-found-error');
return;
}
createBraintreeClient();
};
return {
init,
};
};
export default paypalPaymentMethod;
- set_title_parts "Purchase #{@product.short_title}"
%section.a-section
.a-content.a-content--tight-top.a-content--contiguous-bottom.js-checkout-section
= header do
%noscript.js-do-not-unwrap
= molecule('feedback-message', variant: 'alert') do
To make a purchase, you must enable JavaScript in your web browser. See
%a{ href: 'https://futurelearn.zendesk.com/hc/en-us/articles/115008276347-Issues-with-the-Payment-System' } Issues with the Payment System
on our support site for more information.
- if @product.type == "MicrocredentialProduct"
= render_react 'components/Application/components/Checkout/MicrocredentialJourneyProgress', steps: @microcredential_application.enrolment_steps, activeStep: @microcredential_application.enrolment_step_number(MicrocredentialApplication::CHECKOUT_STEP)
%h1.a-heading Checkout
= render_react 'components/Application/components/Checkout/ProductSummary', React::Checkout::ProductSummaryPresenter.new(@product, @purchase_value, inferred_currency_code).props
= form_tag stripe_intents_purchases_path(product_code: @product.product_code), id: 'stripe-intent-form'
</form>
.u-hide-if-js-unavailable
%section.a-section.checkout{ class: ('js-discount-applied' if @purchase_value.has_discount?) }
- checkout_configuration_json = server_rendered_json_value(checkout_configuration)
= form_for @purchase_form, as: 'purchase', url: purchases_path({ product_code: @product.product_code, context_id: params[:context_id] }.compact), html: { class: 'm-client-validated-form', novalidate: true, id: 'payment-form', data: { configuration: checkout_configuration_json } } do |form|
= hidden_field_tag :stripe_card_token, nil, class: 'js-stripe-token-field'
= hidden_field_tag :paypal_token, nil, class: 'js-paypal-token-field'
= hidden_field_tag :expected_price, @product.operating_price_in_cents
.a-content.a-content--contiguous.u-clearfix
%h2.a-heading.a-heading--small.u-no-margin-top
Order details
.checkout-items__list
.checkout-items__product
%p.statement= @product.itemised_title
%p.js-list-price
= humanized_money_with_symbol(@purchase_value.purchase_list_price)
.checkout-items__discount.js-discount-amount-container
%p.statement Discount
%p.js-discount-amount
= "-"
= humanized_money_with_symbol(@purchase_value.purchase_discount_amount)
.checkout-items__tax_container{ class: tax_container_visibility }
.checkout-items__total_before_tax.js-total-before-tax-row
%p Total before tax
%p.js-total-before-tax
- if @purchase_value.taxed?
= humanized_money_with_symbol(@purchase_value.purchase_total_before_tax)
.checkout-items__tax_amount.js-tax-amount-row
%p.js-tax-title
- if @purchase_value.taxed?
#{tax_display_name(@purchase_value.tax_name)} @ #{@purchase_value.tax_rate}%
%p.js-tax-amount
- if @purchase_value.taxed?
= humanized_money_with_symbol(@purchase_value.purchase_tax)
.checkout-items__totals
.checkout-items__prices
.checkout-items__total
%p Order total
%p.js-price-total{ data: { amount: @purchase_value.purchase_price.format(symbol: false), currency: @purchase_value.currency_code } }
= humanized_money_with_symbol(@purchase_value.purchase_price)
- if @purchase_value.taxed?
%p.u-no-margin Total price includes #{tax_sentence_description(@purchase_value.tax_name)}
%hr.a-divider.u-no-margin.u-hide-if-js-unavailable
= render partial: 'shared/discount_form', locals: { discount_can_be_applied: @product.can_be_discounted?, discount_code_value: @purchase_value.discount_code, validate_discount_url: validate_discount_code_purchases_path(product_code: @product.product_code) }
- if @product.ask_for_certificate_name?
.a-content.a-content--contiguous
= render_react 'components/Application/components/Checkout/CertificateName', nameForCertificates: current_user.name_for_certificates, patch: { action: update_certificate_name_user_path, method: :patch }, profilePath: profile_path(current_user), alerts: { confirmation: I18n.t('checkout.confirmation_alert'), networkFailure: I18n.t('checkout.network_fail_alert') }
%fieldset.m-form__fieldset#shipping-address{ 'data-testid' => 'address-fields' }
%legend
- if @product.requires_shipping_address?
%h2.a-heading.a-heading--small.u-no-margin-top Shipping address for certificate
- else
%h2.a-heading.a-heading--small.u-no-margin-top Billing address
= form.fields_for(:shipping_address) do |address_form|
- address = address_form.object
= control_group_for address, :country do
= address_form.label :country, class: 'm-form__label m-form__label--required'
.a-select-container.a-select-container--selectbox-wide
= address_form.select :country, country_options_for_select_with_metadata(address: address_form.object), { prompt: 'Please select' }, { class: 'a-input a-input--wide js-country-input', required: '', 'aria-required' => 'true', 'data-country-tax-url': calculate_country_tax_purchases_path(product_code: @product.product_code) }
%label.error.m-client-validated-form__error-message{ for: 'purchase_shipping_address_attributes_country' }
Please fill in this field.
= error_message_for address, :country
- if @product.requires_shipping_address?
.m-form__field-hint.checkout__postal-service-container.is-visually-hidden
%span
(Your certificate will be delivered by
= succeed ")." do
%span.checkout__postal-service your local postal service
%br
%span.checkout__postal-service-additional-info.is-visually-hidden
Please note, items sent to Saudi Arabia require a PO Box address.
- if @product.requires_shipping_address?
= control_group_for address, :name do
= address_form.label :name, class: 'm-form__label'
= address_form.text_field :name_for_certificates, class: 'a-input a-input--wide', value: current_user.name_for_certificates, disabled: 'true', data: { address_name: 'true' }
.m-form__field-hint
%span (Your certificate name will be printed at the top of the shipping address)
= control_group_for address, :line_1 do
= address_form.label :line_1, class: 'm-form__label m-form__label--required'
= address_form.text_field :line_1, class: 'a-input a-input--wide', required: '', 'aria-required' => 'true'
%label.error.m-client-validated-form__error-message{ for: "purchase_shipping_address_attributes_line_1" }
Please fill in this field.
= error_message_for address, :line_1
.m-form__field-hint
%span (Street address / P.O. box / company name / c/o)
= control_group_for address, :line_2 do
= address_form.label :line_2, class: 'm-form__label'
= address_form.text_field :line_2, class: 'a-input a-input--wide'
= error_message_for address, :line_2
.m-form__field-hint
%span (Apartment / suite / unit / building floor etc.)
= control_group_for address, :city do
= address_form.label :city, class: 'm-form__label'
= address_form.text_field :city, class: 'a-input a-input--wide'
= error_message_for address, :city
= control_group_for address, :state, id: 'free_text_state', additional_class: ('u-hidden' unless use_free_text_state_field?(address: address)) do
= address_form.label :state, class: 'm-form__label'
= address_form.text_field :state, class: 'a-input a-input--wide', disabled: !use_free_text_state_field?(address: address)
= error_message_for address, :state
- Purchase::COUNTRIES_WITH_ENUMERATED_STATES.each do |country_code|
- use_dropdown_state_field = use_dropdown_state_field?(for_country_code: country_code, address: address)
= control_group_for address, :state, id: "#{country_code}_state", additional_class: (!use_dropdown_state_field ? 'restricted-state-list u-hidden' : 'restricted-state-list') do
= address_form.label :state, class: "m-form__label m-form__label--required"
.a-select-container.a-select-container--selectbox-wide
= address_form.select :state, state_options_for_select(iso_country_code: country_code, address: address), { prompt: 'Please select' }, { class: 'a-input a-input--wide', required: '', 'aria-required' => 'true', disabled: !use_dropdown_state_field }
= error_message_for address, :state
= control_group_for address, :zip, additional_class: 'u-no-margin-bottom' do
= address_form.label :zip, class: 'm-form__label'
= address_form.text_field :zip, class: 'a-input a-input--wide'
= error_message_for address, :zip
.a-content.a-content--tight
%fieldset.payment-details.m-form__fieldset#js-payment-details
%legend
%h2.a-heading.a-heading--small.u-no-margin-top
Your Payment Details
= render_svg_icon_as_html('lock_close')
.m-form__control-group.m-form__control-group--inline{ hidden: paypal_available? ? nil : 'hidden' }
.a-radio-button.a-radio-button--inline
%label.a-radio-button__label{ "data-testid" => 'select-stripe-payment-method' }
= form.radio_button :payment_method, PaymentMethod::StripeSca::PAYMENT_METHOD_NAME, class: 'a-radio-button__input js-payment-method'
%span.a-radio-button__label-text Credit / Debit Card
%span.a-radio-button__button
.a-radio-button.a-radio-button--inline
%label.a-radio-button__label{ "data-testid" => 'select-paypal-payment-method' }
= form.radio_button :payment_method, PaymentMethod::Paypal::PAYMENT_METHOD_NAME, class: 'a-radio-button__input js-payment-method'
%span.a-radio-button__label-text
%img.checkout__paypal-logo{ alt: 'PayPal', src: image_url('payment-icons/paypal-logo-100px.png'), srcset: "#{image_url('payment-icons/paypal-logo-100px.png')} 1x, #{image_url('payment-icons/paypal-logo-200px.png')} 2x" }
%span.a-radio-button__button
#pp-pay-later-message
= form.radio_button :payment_method, 'full_discount', class: 'a-radio-button__input js-payment-method', hidden: 'hidden'
= form.radio_button :payment_method, 'none', class: 'a-radio-button__input js-payment-method', hidden: 'hidden'
- if paypal_available?
.js-no-payment-method-selected-container{ data: { payment_method: 'none' } }
%p.old-text-typescale--small
Please select your preferred method of payment above.
%p.old-text-typescale--small.u-no-margin-bottom
You can pay using Visa, Mastercard or American Express debit/credit cards or PayPal. We’re sorry, but we can’t accept Western Union, payment over the phone, or any other method.
= render partial: 'shared/paypal_payment_method', locals: { checkout_context: 'purchase_checkout', currency_code: inferred_currency_code }
= render_react('components/Application/components/Checkout/PaymentTypes/Stripe', @stripe_props)
.u-interval
= render partial: 'shared/full_discount_payment_method', locals: { checkout_context: 'purchase_checkout', currency_code: inferred_currency_code }
- bundle_pack_tags('honeybadger')
- bundle_pack_tags('checkout.compat')
- content_for :modal do
.m-order-processing-overlay__background.js-order-processing-overlay.u-hidden
.m-order-processing-overlay.js-order-processing-overlay.u-hidden
%div
= render 'shared/loader'
%p Please wait while we process your order
.u-hide-if-js-available.a-content--tight-bottom
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment