/**
 * Copyright 2020 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */ import { loadScript } from '../lib/load-script' ; export interface ReadyToPayChangeResponse { isButtonVisible : boolean ; isReadyToPay : boolean ; paymentMethodPresent ? : boolean ; } export interface Config { environment : google . payments . api . Environment ; existingPaymentMethodRequired ? : boolean ; paymentRequest : google . payments . api . PaymentDataRequest ; onPaymentDataChanged ? : google . payments . api . PaymentDataChangedHandler ; onPaymentAuthorized ? : google . payments . api . PaymentAuthorizedHandler ; onLoadPaymentData ? : ( paymentData : google . payments . api . PaymentData ) => void ; onCancel ? : ( reason : google . payments . api . PaymentsError ) => void ; onError ? : ( error : Error | google . payments . api . PaymentsError ) => void ; onReadyToPayChange ? : ( result : ReadyToPayChangeResponse ) => void ; onClick ? : ( event : Event ) => void ; buttonColor ? : google . payments . api . ButtonColor ; buttonType ? : google . payments . api . ButtonType ; buttonSizeMode ? : google . payments . api . ButtonSizeMode ; buttonLocale ? : string ; } interface ButtonManagerOptions { cssSelector : string ; softwareInfoId : string ; softwareInfoVersion : string ; } /**
 * Manages the lifecycle of the Google Pay button.
 *
 * Includes lifecycle management of the `PaymentsClient` instance,
 * `isReadyToPay`, `onClick`, `loadPaymentData`, and other callback methods.
 */ export class ButtonManager { private client ? : google . payments . api . PaymentsClient ; private config ? : Config ; private element ? : Element ; private options : ButtonManagerOptions ; private oldInvalidationValues ? : any [ ] ; isReadyToPay ? : boolean ; paymentMethodPresent ? : boolean ; constructor ( options ) { this . options = options ; } getElement ( ) { return this . element ; } private isGooglePayLoaded ( ) { return 'google' in ( window || global ) && ! ! google ?. payments ?. api ?. PaymentsClient ; } async mount ( element ) { if ( ! this . isGooglePayLoaded ( ) ) { await loadScript ( 'https://pay.google.com/gp/p/js/pay.js' ) ; } this . element = element ; if ( element ) { this . appendStyles ( ) ; if ( this . config ) { this . updateElement ( ) ; } } } unmount ( ) { this . element = undefined ; } configure ( newConfig ) { let promise = undefined ; this . config = newConfig ; if ( ! this . oldInvalidationValues || this . isClientInvalidated ( newConfig ) ) { promise = this . updateElement ( ) ; } this . oldInvalidationValues = this . getInvalidationValues ( newConfig ) ; return promise ?? Promise . resolve ( ) ; } private createClientOptions ( config ) { const clientConfig = { environment : config . environment , merchantInfo : this . createMerchantInfo ( config ) , } ; if ( config . onPaymentDataChanged || config . onPaymentAuthorized ) { clientConfig . paymentDataCallbacks = { } ; if ( config . onPaymentDataChanged ) { clientConfig . paymentDataCallbacks . onPaymentDataChanged = paymentData => { const result = config . onPaymentDataChanged ! ( paymentData ) ; return result || ( { } as google . payments . api . PaymentDataRequestUpdate ) ; } ; } if ( config . onPaymentAuthorized ) { clientConfig . paymentDataCallbacks . onPaymentAuthorized = paymentData => { const result = config . onPaymentAuthorized ! ( paymentData ) ; return result || ( { } as google . payments . api . PaymentAuthorizationResult ) ; } ; } } return clientConfig ; } private createIsReadyToPayRequest ( config ) { const paymentRequest = config . paymentRequest ; const request = { apiVersion : paymentRequest . apiVersion , apiVersionMinor : paymentRequest . apiVersionMinor , allowedPaymentMethods : paymentRequest . allowedPaymentMethods , existingPaymentMethodRequired : config . existingPaymentMethodRequired , } ; return request ; } private createLoadPaymentDataRequest ( config ) { const request = { ... config . paymentRequest , merchantInfo : this . createMerchantInfo ( config ) , } ; return request ; } private createMerchantInfo ( config ) { const merchantInfo = { ... config . paymentRequest . merchantInfo , } ; if ( ! merchantInfo . softwareInfo ) { merchantInfo . softwareInfo = { id : this . options . softwareInfoId , version : this . options . softwareInfoVersion , } ; } return merchantInfo ; } private isMounted ( ) { return this . element != null && this . element . isConnected !== false ; } private removeButton ( ) { if ( this . element instanceof ShadowRoot || this . element instanceof Element ) { for ( const child of Array . from ( this . element . children ) ) { if ( child . tagName !== 'STYLE' ) { child . remove ( ) ; } } } } private async updateElement ( ) { if ( ! this . isMounted ( ) ) return ; const element = this . getElement ( ) ! ; if ( ! this . config ) { throw new Error ( 'google-pay-button: Missing configuration' ) ; } this . removeButton ( ) ; this . client = new google . payments . api . PaymentsClient ( this . createClientOptions ( this . config ) ) ; const buttonOptions = { buttonType : this . config . buttonType , buttonColor : this . config . buttonColor , buttonSizeMode : this . config . buttonSizeMode , buttonLocale : this . config . buttonLocale , onClick : this . handleClick , allowedPaymentMethods : this . config . paymentRequest . allowedPaymentMethods , } ; const rootNode = element . getRootNode ( ) ; if ( rootNode instanceof ShadowRoot ) { buttonOptions . buttonRootNode = rootNode ; } const button = this . client . createButton ( buttonOptions ) ; this . setClassName ( element , [ element . className , 'not-ready' ] ) ; element . appendChild ( button ) ; let showButton = false ; let readyToPay ; try { readyToPay = await this . client . isReadyToPay ( this . createIsReadyToPayRequest ( this . config ) ) ; showButton = ( readyToPay . result && ! this . config . existingPaymentMethodRequired ) || ( readyToPay . result && readyToPay . paymentMethodPresent && this . config . existingPaymentMethodRequired ) || false ; } catch ( err ) { if ( this . config . onError ) { this . config . onError ( err as Error ) ; } else { console . error ( err ) ; } } if ( ! this . isMounted ( ) ) return ; if ( showButton ) { try { this . client . prefetchPaymentData ( this . createLoadPaymentDataRequest ( this . config ) ) ; } catch ( err ) { console . log ( 'Error with prefetch' , err ) ; } this . setClassName ( element , ( element . className || '' ) . split ( ' ' ) . filter ( className => className && className !== 'not-ready' ) ) ; } if ( this . isReadyToPay !== readyToPay ?. result || this . paymentMethodPresent !== readyToPay ?. paymentMethodPresent ) { this . isReadyToPay = ! ! readyToPay ?. result ; this . paymentMethodPresent = readyToPay ?. paymentMethodPresent ; if ( this . config . onReadyToPayChange ) { const readyToPayResponse = { isButtonVisible : showButton , isReadyToPay : this . isReadyToPay , } ; if ( this . paymentMethodPresent ) { readyToPayResponse . paymentMethodPresent = this . paymentMethodPresent ; } this . config . onReadyToPayChange ( readyToPayResponse ) ; } } } private handleClick = async ( event ) : Promise < void > => { const config = this . config ; if ( ! config ) { throw new Error ( 'google-pay-button: Missing configuration' ) ; } const request = this . createLoadPaymentDataRequest ( config ) ; try { if ( config . onClick ) { config . onClick ( event ) ; } if ( event . defaultPrevented ) { return ; } const result = await this . client ! . loadPaymentData ( request ) ; if ( config . onLoadPaymentData ) { config . onLoadPaymentData ( result ) ; } } catch ( err ) { if ( ( err as google . payments . api . PaymentsError ) . statusCode === 'CANCELED' ) { if ( config . onCancel ) { config . onCancel ( err as google . payments . api . PaymentsError ) ; } } else if ( config . onError ) { config . onError ( err as google . payments . api . PaymentsError ) ; } else { console . error ( err ) ; } } } ; private setClassName ( element , classNames ) { const className = classNames . filter ( name => name ) . join ( ' ' ) ; if ( className ) { element . className = className ; } else { element . removeAttribute ( 'class' ) ; } } private appendStyles ( ) { if ( typeof document === 'undefined' ) return ; const rootNode = this . element ?. getRootNode ( ) as Document | ShadowRoot | undefined ; const styleId = `default-google-style-${ this . options . cssSelector . replace ( /[^\w-]+/g , '' ) }-${ this . config ?. buttonLocale }` ; if ( rootNode ) { if ( ! rootNode . getElementById ?. ( styleId ) ) { const style = document . createElement ( 'style' ) ; style . id = styleId ; style . type = 'text/css' ; style . innerHTML = `
          ${ this . options . cssSelector } {
            display: inline-block;
          }
          ${ this . options . cssSelector }.not-ready {
            width: 0;
            height: 0;
            overflow: hidden;
          }
        ` ; if ( rootNode instanceof Document && rootNode . head ) { rootNode . head . appendChild ( style ) ; } else { rootNode . appendChild ( style ) ; } } } } private isClientInvalidated ( newConfig ) { if ( ! this . oldInvalidationValues ) return true ; const newValues = this . getInvalidationValues ( newConfig ) ; return newValues . some ( ( value , index ) => value !== this . oldInvalidationValues ! [ index ] ) ; } private getInvalidationValues ( config ) { return [ config . environment , config . existingPaymentMethodRequired , ! ! config . onPaymentDataChanged , ! ! config . onPaymentAuthorized , config . buttonColor , config . buttonType , config . buttonLocale , config . buttonSizeMode , config . paymentRequest . merchantInfo . merchantId , config . paymentRequest . merchantInfo . merchantName , config . paymentRequest . merchantInfo . softwareInfo ?. id , config . paymentRequest . merchantInfo . softwareInfo ?. version , config . paymentRequest . allowedPaymentMethods , ] ; } }