braintree / braintree-web

A suite of tools for integrating Braintree in the browser
https://developer.paypal.com/braintree/docs/start/hello-client/javascript/v3
MIT License
440 stars 133 forks source link

Paypal Braintree Button is not loading on Safari, Firefox #616

Closed HakkiUlkuDev closed 2 years ago

HakkiUlkuDev commented 2 years ago

General information

Issue description

Paypal button is not loading on Safari (this is the first browser that we observe the issue). We have implemented everything according to the documents, however it is not loading for some browsers (eg: loading for Chrome successfully). Scripts are also added under nuxt.config.js to make sure they are loading.

Implemented PaypalBrainTreeButton in vuejs as seen below:

<template>
  <div>
    <div v-show="isNewPaymentFeatureEnabled()">
      <div
        v-show="isButtonLoading || !isPaymentOptionsLoaded"
        class="skeleton-loading-info loading t-h-11 fix-right"
      />
      <div class="t-relative t-z-0">
        <div
          v-show="!isButtonLoading && isPaymentOptionsLoaded"
          id="paypal-button"
        ></div>
      </div>
    </div>
    <div v-show="!isNewPaymentFeatureEnabled()">
      <div
        v-show="isButtonLoading"
        class="skeleton-loading-info loading t-h-11 fix-right"
      />
      <div class="t-relative t-z-0">
        <div v-show="!isButtonLoading" id="paypal-button"></div>
      </div>
    </div>
    <div>
      <v-overlay
        :value="$store.getters.spinnerDisabled"
        color="#FFFFFF"
        opacity="0.7"
      >
        <img
          src="../assets/image/modanisa-flower.png"
          alt="Modanisa logo for Paypal Button"
          class="t-h-20 t-w-20 rotate linear infinite t-m-auto t-block"
        />
      </v-overlay>
    </div>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from 'nuxt-property-decorator'
import { mapGetters } from 'vuex'
import utils from '~/utils/utils'

declare global {
  interface Paypal {
    client: any
    paypalCheckout: any
    FUNDING: any
    Buttons: any
  }
  interface Braintree {
    paypalCheckout: any
    client: any
  }
  interface Window {
    paypal: Paypal
    braintree: Braintree
  }
}

@Component({
  computed: {
    ...mapGetters({
      isFeatureEnabled: 'isFeatureEnabled',
      isFeatureFlagsLoaded: 'isFeatureFlagsLoaded'
    })
  }
})
export default class PaypalBraintreeButton extends Vue {
  private currency: string =
    this.$store.getters[`${this.getActiveStoreModuleName()}/currency`]

  private totalPrice: string =
    this.$store.getters[`${this.getActiveStoreModuleName()}/totalPrice`]

  private authorizationToken =
    this.$store.getters['payment/selectedPaymentOption']?.token?.braintree

  private paypalClientId = utils.getPaypalClientId()

  private locale = this.$store.getters.checkoutParams.language.startsWith('AR')
    ? 'ar_SA'
    : 'en_US'

  private paymentSdkScript: any = {
    scripts: [
      {
        url: 'https://js.braintreegateway.com/web/3.76.4/js/client.min.js',
        name: 'braintree-client'
      },
      {
        url: 'https://js.braintreegateway.com/web/3.76.4/js/paypal-checkout.min.js',
        name: 'braintree-paypal-checkout'
      }
    ]
  }

  beforeMount(): void {
    this.setupPayPal()
    this.$store.dispatch('payment/buttonLoadStarted')
  }

  loadAllScripts(paymentSdkScript: any) {
    const promises: Array<Promise<any>> = []
    paymentSdkScript.scripts.forEach((script: any) => {
      if (document.getElementById(script.name) !== null) {
        return
      }
      promises.push(this.loadScript(script.url, script.name))
    })
  }

  getActiveStoreModuleName() {
    return this.$store.getters.pageParams.checkoutVersion.toLowerCase() === 'v2'
      ? 'checkoutV2'
      : 'checkout'
  }

  loadScript(url: string, name: string) {
    return new Promise(function (resolve, reject) {
      const script = document.createElement('script')
      script.src = url
      script.async = false
      script.setAttribute('id', name)
      script.onload = function () {
        resolve(url)
      }
      script.onerror = function () {
        reject(url)
      }
      document.body.appendChild(script)
    })
  }

  isNewPaymentFeatureEnabled() {
    if (this.$store.getters.isFeatureFlagsLoaded) {
      return this.$store.getters.isFeatureEnabled('new_payment_options')
    } else {
      return false
    }
  }

  get isButtonLoading() {
    return this.$store.getters['payment/spinnerEnabled']
  }

  get isPaymentOptionsLoaded() {
    return this.$store.getters.isPaymentOptionsLoaded
  }

  // loadPayPalSDK(onReady: any) {
  //   const paypalSDK = document.createElement('script')
  //   paypalSDK.async = false
  //   paypalSDK.setAttribute(
  //     'src',
  //     `https://www.paypal.com/sdk/js?components=buttons&client-id=${this.paypalClientId}&currency=${this.currency}&intent=capture&locale=${this.locale}`
  //   )
  //   paypalSDK.addEventListener('load', onReady)
  //   document.head.appendChild(paypalSDK)
  // }

  setupPayPal() {
    const placeOrder = (payload: any): void => {
      this.$store.dispatch('spinnerDisabled', { isValid: true })
      const callback = this.$store.getters.placeOrderCallback
      if (callback) {
        callback(this.$store.getters['payment/selectedPaymentOption'], payload)
      }
    }

    const validate = (): boolean => {
      const callback = this.$store.getters.placeOrderCallback
      if (callback) {
        return callback(null)
      }
      return false
    }

    const braintree = window.braintree
    if (braintree === undefined || braintree.client == null) {
      return this.loadAllScripts(this.paymentSdkScript)
    }
    const currency = this.currency
    const amount = this.totalPrice
    const store = this.$store
    const locale = this.$store.getters.checkoutParams.language.startsWith('AR')
      ? 'ar_SA'
      : 'en_US'
    const clientID = this.paypalClientId

    // Create a client.
    braintree.client
      .create({
        authorization: this.authorizationToken
      })
      .then(function (clientInstance: any) {
        // Create a PayPal Checkout component.
        return braintree.paypalCheckout.create({
          client: clientInstance
        })
      })
      .then(function (paypalCheckoutInstance: any) {
        return paypalCheckoutInstance.loadPayPalSDK({
          'client-id': clientID,
          currency,
          intent: 'capture',
          locale
        })
      })
      .then(function (paypalCheckoutInstance: any) {
        let intentId = ''

        return window.paypal
          .Buttons({
            style: {
              layout: 'horizontal',
              size: 'responsive',
              label: 'checkout',
              tagline: 'false',
              color: 'blue',
              shape: 'rect'
            },

            fundingSource: window.paypal.FUNDING.PAYPAL,
            onClick() {
              return validate()
            },
            createOrder() {
              const createPaymentIntent =
                store.getters['payment/createPaymentIntent']
              return createPaymentIntent().then((paymentIntentId: string) => {
                intentId = paymentIntentId
                return paypalCheckoutInstance.createPayment({
                  flow: 'checkout',
                  amount,
                  currency,
                  intent: 'capture'
                })
              })
            },

            onApprove(data: any) {
              return paypalCheckoutInstance
                .tokenizePayment(data)
                .then((payload: any) => {
                  placeOrder({ nonce: payload.nonce, intentId })
                })
            },

            onCancel() {
              // TODO failure payment call
            },

            onError(err: any) {
              // TODO failure payment call
              // eslint-disable-next-line no-console
              console.error('PayPal error', err)
            }
          })
          .render('#paypal-button')
      })
      .then(function () {
        store.dispatch('payment/buttonLoadCompleted')
      })
  }
}
</script>

<style scoped>
@media screen and (max-width: 400px) {
  #paypal-button-container {
    width: 100%;
  }
}

@media screen and (min-width: 400px) {
  #paypal-button-container {
    width: 250px;
  }
}

.rotate {
  animation: rotation 4s;
}

.linear {
  animation-timing-function: linear;
}

.infinite {
  animation-iteration-count: infinite;
}

@keyframes rotation {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(359deg);
  }
}
</style>

nuxt.config.js 's head:

 head: {
    title: '',
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      {
        hid: 'description',
        name: 'description',
        content: process.env.npm_package_description || 'checkout'
      }
    ],
    link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }],
    script: [
      {
        src: 'https://js.braintreegateway.com/web/3.76.4/js/client.min.js'
      },
      {
        src: 'https://js.braintreegateway.com/web/3.76.4/js/paypal-checkout.min.js'
      }
    ]
  },

Also, for Firefox we got this error in the console:

image
HakkiUlkuDev commented 2 years ago

As far as we continue with investigating, loadPaypalSdk method is not doing its job. We observed this by checking the loaded scripts on head of the page and paypal.com.... related link is not on the page.

crookedneighbor commented 2 years ago

Do you have a nuxt repo on Github that minimally reproduces the issue so we can inspect it?

HakkiUlkuDev commented 2 years ago

@crookedneighbor , sorry but we are working in a private repository which is a business project. Therefore, we cannot share the repository. Maybe we can arrange a short meeting about the issue so that we can reproduce and show the issue to you on our locals. Would that be proper for you?

crookedneighbor commented 2 years ago

I'm not asking you to share your private repository, just that you set up a new nuxt project that reproduces the issue. By doing that, you should be able to determine if it's something happening in the nuxt platform that is causing it, or a plugin you are using, or what. That should significantly narrow down where the issue is.

As far as I know, the loadPayPalSDK method works just fine everywhere else, so there must be something specific to your codebase that is making it behave in this unexpected way. Perhaps Nuxt prevents scripts from being loaded into the head dynamically?

If that's the case, I'd recommend manually including the PayPal script instead of using the helper method we provide.

cgdibble commented 2 years ago

If further details/information become available, feel free to re-open this issue. Otherwise, please create a new issue if problems arise.