From 60ac3c735b29e7966045f1963cb71a923bd08737 Mon Sep 17 00:00:00 2001 From: Sag Date: Wed, 8 May 2024 20:56:17 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20Fixed=20updating=20payment=20met?= =?UTF-8?q?hod=20when=20beta=20flag=20is=20on=20(#20171)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit refs https://linear.app/tryghost/issue/ONC-20 refs https://linear.app/tryghost/issue/ENG-867 - when using dynamic payment methods in Stripe, we need to provide a currency. Stripe uses that parameter to determine which payment methods to render - docs: https://docs.stripe.com/api/checkout/sessions/create --- .../lib/controllers/RouterController.js | 6 ++- ghost/stripe/lib/StripeAPI.js | 10 +++- ghost/stripe/test/unit/lib/StripeAPI.test.js | 54 ++++++++++++++++--- 3 files changed, 60 insertions(+), 10 deletions(-) diff --git a/ghost/members-api/lib/controllers/RouterController.js b/ghost/members-api/lib/controllers/RouterController.js index 6230607e8a..8755a675fa 100644 --- a/ghost/members-api/lib/controllers/RouterController.js +++ b/ghost/members-api/lib/controllers/RouterController.js @@ -129,10 +129,14 @@ module.exports = class RouterController { customer = await this._stripeAPIService.getCustomer(subscription.get('customer_id')); } + const defaultTier = await this._tiersService.api.readDefaultTier(); + const currency = defaultTier?.currency?.toLowerCase() || 'usd'; + const session = await this._stripeAPIService.createCheckoutSetupSession(customer, { successUrl: req.body.successUrl, cancelUrl: req.body.cancelUrl, - subscription_id: req.body.subscription_id + subscription_id: req.body.subscription_id, + currency }); const publicKey = this._stripeAPIService.getPublicKey(); const sessionInfo = { diff --git a/ghost/stripe/lib/StripeAPI.js b/ghost/stripe/lib/StripeAPI.js index 68d98fa755..576f885359 100644 --- a/ghost/stripe/lib/StripeAPI.js +++ b/ghost/stripe/lib/StripeAPI.js @@ -556,7 +556,9 @@ module.exports = class StripeAPI { /** * @param {ICustomer} customer * @param {object} options - * + * @param {string} options.successUrl + * @param {string} options.cancelUrl + * @param {string} options.currency - 3-letter ISO code in lowercase, e.g. `usd` * @returns {Promise} */ async createCheckoutSetupSession(customer, options) { @@ -571,7 +573,11 @@ module.exports = class StripeAPI { metadata: { customer_id: customer.id } - } + }, + + // Note: this is required for dynamic payment methods + // https://docs.stripe.com/api/checkout/sessions/create#create_checkout_session-currency + currency: this.labs.isSet('additionalPaymentMethods') ? options.currency : undefined }); return session; diff --git a/ghost/stripe/test/unit/lib/StripeAPI.test.js b/ghost/stripe/test/unit/lib/StripeAPI.test.js index c720c9a3d4..61d7d942d6 100644 --- a/ghost/stripe/test/unit/lib/StripeAPI.test.js +++ b/ghost/stripe/test/unit/lib/StripeAPI.test.js @@ -61,13 +61,6 @@ describe('StripeAPI', function () { should.exist(mockStripe.checkout.sessions.create.firstCall.firstArg.cancel_url); }); - it('createCheckoutSetupSession sends success_url and cancel_url', async function () { - await api.createCheckoutSetupSession('priceId', {}); - - should.exist(mockStripe.checkout.sessions.create.firstCall.firstArg.success_url); - should.exist(mockStripe.checkout.sessions.create.firstCall.firstArg.cancel_url); - }); - it('sets valid trialDays', async function () { await api.createCheckoutSession('priceId', null, { trialDays: 12 @@ -162,6 +155,53 @@ describe('StripeAPI', function () { }); }); + describe('createCheckoutSetupSession', function () { + beforeEach(function () { + mockStripe = { + checkout: { + sessions: { + create: sinon.stub().resolves() + } + } + }; + sinon.stub(mockLabs, 'isSet'); + const mockStripeConstructor = sinon.stub().returns(mockStripe); + StripeAPI.__set__('Stripe', mockStripeConstructor); + api.configure({ + checkoutSessionSuccessUrl: '/success', + checkoutSessionCancelUrl: '/cancel', + checkoutSetupSessionSuccessUrl: '/setup-success', + checkoutSetupSessionCancelUrl: '/setup-cancel', + secretKey: '' + }); + }); + + afterEach(function () { + sinon.restore(); + }); + + it('createCheckoutSetupSession sends success_url and cancel_url', async function () { + await api.createCheckoutSetupSession('priceId', {}); + + should.exist(mockStripe.checkout.sessions.create.firstCall.firstArg.success_url); + should.exist(mockStripe.checkout.sessions.create.firstCall.firstArg.cancel_url); + }); + + it('createCheckoutSetupSession does not send currency if additionalPaymentMethods flag is off', async function () { + mockLabs.isSet.withArgs('additionalPaymentMethods').returns(false); + await api.createCheckoutSetupSession('priceId', {currency: 'usd'}); + + should.not.exist(mockStripe.checkout.sessions.create.firstCall.firstArg.currency); + }); + + it('createCheckoutSetupSession sends currency if additionalPaymentMethods flag is on', async function () { + mockLabs.isSet.withArgs('additionalPaymentMethods').returns(true); + await api.createCheckoutSetupSession('priceId', {currency: 'usd'}); + + should.equal(mockStripe.checkout.sessions.create.firstCall.firstArg.currency, 'usd'); + }); + }); + describe('getCustomerIdByEmail', function () { describe('when no customer is found', function () { beforeEach(function () {