with a known ID. This
* gets replaced by Stripe with an IFrame with their custom inputs.
* see: {@link|https://stripe.com/docs/stripe-js/reference#element-mount}
*/
genInput (): VNode {
return this.$createElement('div', { attrs: { id: this.computedId, tabindex: -1 } })
},
/**
* Maintains the ability for users of the component to control the
* loading/progress indicator of the component, but also shows the
* progress bar while the Stripe library is being loaded.
*/
genProgress (): VNode | VNode[] | null {
if (this.loading === false && this.isReady) return null
return this.$slots.progress || this.$createElement(VProgressLinear, {
props: {
absolute: true,
color: (this.loading === true || this.loading === '')
? (this.color || 'primary')
: (this.loading || 'primary'),
height: this.loaderHeight,
indeterminate: true,
},
})
},
/**
* Generate styles for Stripe elements
*
* @param {string} font
*/
genStyle: (customStyle: ElementStyles, fontName: string, isDark: boolean, theme: VuetifyThemeVariant): ElementStyles => {
const defaults: ElementStyles = {
base: {
color: isDark ? '#ffffff' : '#000000',
fontFamily: `'${fontName}', sans-serif`,
fontSize: '16px',
fontSmoothing: 'antialiased',
iconColor: isDark ? '#eceff1' : '#455a64',
'::placeholder': {
color: isDark ? 'rgb(255,255,255,0.7)' : 'rgb(0,0,0,0.54)',
},
},
invalid: {
color: theme.error as string || '#ff5252',
iconColor: theme.error as string || '#ff5252',
},
}
return merge(defaults, customStyle)
},
/**
* Loosely validates a URL
* Based on: {@link|https://github.com/segmentio/is-url}
*
* @param {string} url The string to be tested
* @returns {boolean} True if the url string passes the test
*/
isURL: (url: string): boolean => {
const protocolAndDomainRegex = /^(?:\w+:)?\/\/(\S+)$/
const localhostDomainRegex = /^localhost[:?\d]*(?:[^:?\d]\S*)?$/
const nonLocalhostDomainRegex = /^[^\s.]+\.\S{2,}$/
if (typeof url !== 'string') return false
const match = url.match(protocolAndDomainRegex)
if (!match) return false
const everythingAfterProtocol = match[1]
if (!everythingAfterProtocol) return false
if (
localhostDomainRegex.test(everythingAfterProtocol) ||
nonLocalhostDomainRegex.test(everythingAfterProtocol)
) {
return true
}
return false
},
/**
* Check to see if the Stripe.js library has been loaded into the
* browser. If it has not, try to load it. If Stripe cannot be
* loaded or there was a problem with loading, an error is thrown.
*
* @throws {Error} Could not load Stripe because `vue-plugin-load-script` is not available
* @throws {Error} Could not load Stripe because of a network (or other) error
*/
loadStripe () {
// Is Stripe already loaded?
if (typeof Stripe === 'function') {
// Yes. Generate the card.
this.genCard()
} else {
// No. Set the Stripe URL.
const src = 'https://js.stripe.com/v3/'
// Is it already being loaded by another component?
if (document.querySelector(`script[src="${src}"]`)) {
// Yes, it's being loaded, so listen for it.
this.$root.$once('stripe-loaded', () => {
// instantiate the card
this.genCard()
})
} else {
// No. Is the script loader installed?
if (typeof this.$loadScript === 'undefined') {
// No.
this.loading = false
this.errorBucket.push('Stripe could not be loaded')
throw new Error('[VStripeCard Error]: Stripe is not available and could not be loaded. Please make sure that you have installed and configured all of the necessary dependencies to use this component.')
} else {
// Yes, so try to load it.
this.$loadScript(src).then(() => {
// Let other potential components know...
this.$root.$emit('stripe-loaded')
// and generate the card
this.genCard()
}).catch((error: Error) => {
this.loading = false
this.errorBucket.push('Error loading stripe')
throw new Error('[VStripeCard Error] There was a problem loading Stripe: ' + error.message)
})
}
}
}
},
/**
* Handles card blur events. Propagates (emits) a blur event to
* allow other components to register event handlers that can
* respond to the card being blurred.
*
* @param {ElementChangeResponse} e Event data from the card element
*/
onCardBlur (e: stripe.elements.ElementChangeResponse) {
this.isFocused = false
// if we have enough info, process the card
if (this.okToSubmit) {
this.processCard()
}
this.$emit('blur', e)
},
/**
* Handles card change events. Clears or sets errors in the
* `errorBucket`. If the card is empty, sets `lazyValue` to false.
*
* @param {ElementChangeResponse} e Event data from the card element
*/
onCardChange (e: stripe.elements.ElementChangeResponse) {
if (e.error) {
// handle card errors
this.okToSubmit = false
e.error.message && this.errorBucket.push(e.error.message)
}
if (e.complete) {
// handle card input is complete
this.okToSubmit = true
this.errorBucket = []
}
if (e.empty) {
this.okToSubmit = false
this.lazyValue = !e.empty
}
},
/**
* TODO: Should this function emit? Does it emit the right value?
* Handles card focus events. Propagates (emits) a focus event to
* allow other components to register event handlers that can
* respond to the card being focused.
*
* @param {ElementChangeResponse} e Event data from the card element
*/
onCardFocus (e: stripe.elements.ElementChangeResponse) {
this.isFocused = true
this.$emit('focus', true)
},
/**
* Handles card initialization events. Propagates (emits) a ready event to
* allow other components to register event handlers that can
* respond to the card being ready. Will also focus the card input
* if `autofocus` is true.
*
* @param {ElementChangeResponse} e Event data from the card element
*/
onCardReady (e: stripe.elements.ElementChangeResponse) {
this.isReady = true
this.autofocus && this.card !== null && this.card.focus()
this.$emit('ready', e)
},
/**
* Converts the collected payment information into a single-use token
* or a multi-use source that can safely be passed to your backend
* API server where a payment request can be processed.
*
* See {@link|https://stripe.com/docs/stripe-js/reference#stripe-create-token}
*/
async processCard () {
if (this.stripe && this.card) {
if (this.create === 'token') {
const { token, error } = await this.stripe.createToken(this.card, this.options)
if (!error) {
this.errorBucket = []
this.$emit('input', token)
} else {
// handle error
error.message && this.errorBucket.push(error.message)
}
} else if(this.create === 'paymentMethod') {
const { paymentMethod, error } = await this.stripe.createPaymentMethod({
type: 'card',
card: this.card,
billing_details: this.customerData
});
if (!error) {
this.errorBucket = []
this.$emit('input', paymentMethod)
} else {
// handle error
error.message && this.errorBucket.push(error.message)
}
} else {
const { source, error } = await this.stripe.createSource(this.card, this.options)
if (!error) {
this.errorBucket = []
this.$emit('input', source)
} else {
// handle error
error.message && this.errorBucket.push(error.message)
}
}
}
},
},
})