Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "processout.js",
"version": "1.9.19",
"version": "1.9.20",
"description": "ProcessOut.js is a JavaScript library for ProcessOut's payment processing API.",
"scripts": {
"build:processout": "tsc -p src/processout && uglifyjs --compress --keep-fnames --ie8 dist/processout.js -o dist/processout.js",
Expand Down
3 changes: 3 additions & 0 deletions src/nativeapm/config/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,9 @@ const defaultTheme: NativeApmThemeConfigType = {
actionImage: {
marginTop: "1.3rem",
marginBottom: "1.3rem",
// auto side margins center the fixed-width action image/QR within its container
marginLeft: "auto",
marginRight: "auto",
width: "15rem",
height: "auto",
display: "flex",
Expand Down
61 changes: 60 additions & 1 deletion src/nativeapm/nativeapm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,13 @@ module ProcessOut {
*/
markdownLibraryInstance: any

/**
* Instance of JS library which handles markdown on the form view.
* Its links open in a new window, so a payment link doesn't replace the checkout page.
* @type {any}
*/
formMarkdownLibraryInstance: any

/**
* Native APM container element for mounting the widget
* @type {Element}
Expand Down Expand Up @@ -102,6 +109,18 @@ module ProcessOut {
*/
captureStart = null

/**
* Markdown and QR code libraries still loading
* @type {number}
*/
pendingInstructionLibraries = 2

/**
* Callbacks to run once the markdown and QR code libraries are loaded (or failed to load)
* @type {Function[]}
*/
instructionLibrariesCallbacks: Function[] = []

/**
* NativeAPM constructor
* @param {NativeApmConfig} config
Expand Down Expand Up @@ -185,14 +204,19 @@ module ProcessOut {
) {
const nativeApmFormView = new NativeApmFormView(
this.processOutInstance,
this.gatewayConfiguration.native_apm,
{
...this.gatewayConfiguration.native_apm,
customerInstructions: this.gatewayConfiguration.native_apm.parameter_values,
},
this.proceedPayment.bind(this),
this.theme,
this.prefilledData,
this.paymentConfig.payButtonText,
this.formMarkdownLibraryInstance,
)

EventsUtils.dispatchWidgetReadyEvent()
this.refreshInstructionsWhenLibrariesLoad(nativeApmFormView)

return this.loadView(nativeApmFormView.getViewElement())
}
Expand Down Expand Up @@ -299,14 +323,17 @@ module ProcessOut {
gateway: this.gatewayConfiguration.native_apm.gateway,
parameters: data.native_apm.parameterDefinitions,
invoice: this.gatewayConfiguration.native_apm.invoice,
customerInstructions: data.native_apm.parameterValues,
},
this.proceedPayment.bind(this),
this.theme,
this.prefilledData,
this.paymentConfig.payButtonText,
this.formMarkdownLibraryInstance,
)

EventsUtils.dispatchPaymentAdditionalInputEvent()
this.refreshInstructionsWhenLibrariesLoad(customerInputView)

return this.loadView(customerInputView.getViewElement())
}
Expand Down Expand Up @@ -516,15 +543,47 @@ module ProcessOut {
window.globalThis && window.globalThis.showdown
? new window.globalThis.showdown.Converter()
: null
this.formMarkdownLibraryInstance =
window.globalThis && window.globalThis.showdown
? new window.globalThis.showdown.Converter({ openLinksInNewWindow: true })
: null
this.markInstructionLibraryLoaded()
}
markdownScript.onerror = () => this.markInstructionLibraryLoaded()
document.head.appendChild(markdownScript)
}

private loadQrCodesLibrary() {
const qrCodeScript = document.createElement("script")
qrCodeScript.src = this.processOutInstance.endpoint("js", "/js/libraries/qrcode.min.js")
qrCodeScript.onload = () => this.markInstructionLibraryLoaded()
qrCodeScript.onerror = () => this.markInstructionLibraryLoaded()
document.head.appendChild(qrCodeScript)
}

private markInstructionLibraryLoaded() {
this.pendingInstructionLibraries--
if (this.pendingInstructionLibraries > 0) {
return
}

const callbacks = this.instructionLibrariesCallbacks
this.instructionLibrariesCallbacks = []
callbacks.forEach(callback => callback())
}

/**
* A form can render before the markdown and QR code libraries are loaded: its instructions are drawn again then
*/
private refreshInstructionsWhenLibrariesLoad(formView: NativeApmFormView) {
if (this.pendingInstructionLibraries <= 0) {
return
}

this.instructionLibrariesCallbacks.push(() =>
formView.refreshCustomerInstructions(this.formMarkdownLibraryInstance),
)
}
private loadPhoneNumberInputLibrary() {
const phoneInputScript = document.createElement("script")
phoneInputScript.src = this.processOutInstance.endpoint(
Expand Down
1 change: 1 addition & 0 deletions src/nativeapm/references.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
/// <reference path="elements/inputs/phone-with-country.ts" />
/// <reference path="elements/inputs/select.ts" />
/// <reference path="elements/inputs/input.ts" />
/// <reference path="views/customer-instructions.ts" />
/// <reference path="views/form.ts" />
/// <reference path="views/pending.ts" />
/// <reference path="views/error.ts" />
Expand Down
121 changes: 121 additions & 0 deletions src/nativeapm/views/customer-instructions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/// <reference path="../references.ts" />

type NativeApmCustomerActionBarcode = {
type: string
value: string
}

/**
* Customer instructions sent by the API in native APM parameter values.
* The barcode comes as an object from GET native-payment and as flat keys from process native-payment.
*/
type NativeApmCustomerInstructionsValues = {
customer_action_message?: string
customer_action_barcode?: NativeApmCustomerActionBarcode
customer_action_barcode_type?: string
customer_action_barcode_value?: string
}

/**
* ProcessOut module/namespace
*/
module ProcessOut {
/**
* ProcessOut Native APM class for rendering customer instructions (message and QR code)
*/
export class NativeApmCustomerInstructions {
/**
* This function returns the customer action message, or an empty string
*/
static getMessage(values?: NativeApmCustomerInstructionsValues): string {
return values && values.customer_action_message ? values.customer_action_message : ""
}

/**
* This function returns the customer action barcode from the object or the flat keys
*/
static getBarcode(
values?: NativeApmCustomerInstructionsValues,
): NativeApmCustomerActionBarcode | undefined {
if (!values) {
return undefined
}

if (values.customer_action_barcode && values.customer_action_barcode.value) {
return values.customer_action_barcode
}

if (values.customer_action_barcode_value) {
return {
type: values.customer_action_barcode_type || "qr",
value: values.customer_action_barcode_value,
}
}

return undefined
}

/**
* This function creates the customer message element, rendered as markdown when the library is loaded
*/
static createMessageElement(
markdownLibraryInstance: any,
theme: NativeApmThemeConfigType,
message: string,
) {
const customerMessage = document.createElement("p")

customerMessage.setAttribute("class", "native-apm-message")

StylesUtils.styleElement(customerMessage, theme.message)

if (markdownLibraryInstance && markdownLibraryInstance.makeHtml) {
customerMessage.innerHTML = markdownLibraryInstance.makeHtml(message)
} else {
customerMessage.textContent = message
}

return customerMessage
}

/**
* This function creates the QR code element from a base64 encoded barcode value
*/
static createQrCodeElement(
theme: NativeApmThemeConfigType,
barcode?: NativeApmCustomerActionBarcode,
) {
const qrCodeElement = document.createElement("div")

qrCodeElement.setAttribute("class", "native-apm-qr-code")

StylesUtils.styleElement(qrCodeElement, theme.actionImage)

const text =
barcode && barcode.value
? NativeApmCustomerInstructions.decodeBarcodeValue(barcode.value)
: null

window.globalThis.QRCode &&
text &&
new window.globalThis.QRCode(qrCodeElement, {
text,
width: 128,
height: 128,
})

return qrCodeElement
}

/**
* This function decodes the base64 barcode value. An invalid value draws no QR code instead of breaking the view.
*/
private static decodeBarcodeValue(value: string): string | null {
try {
return atob(value)
} catch {
return null
}
}
}
}
80 changes: 80 additions & 0 deletions src/nativeapm/views/form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ type NativeApmFormData = {
amount: string
currency_code: string
}
customerInstructions?: NativeApmCustomerInstructionsValues
}

/**
Expand Down Expand Up @@ -71,6 +72,18 @@ module ProcessOut {
*/
payButtonText?: string

/**
* Markdown library instance used to render the customer message
* @type {any}
*/
markdownLibraryInstance: any

/**
* Container of the customer message and QR code
* @type {HTMLElement}
*/
instructionsElement: HTMLElement

/**
* Native APM Form constructor
*/
Expand All @@ -81,6 +94,7 @@ module ProcessOut {
theme: NativeApmThemeConfigType,
prefilledData: PrefilledData,
payButtonText?: string,
markdownLibraryInstance?: any,
) {
if (!formData) {
throw new Exception(
Expand All @@ -94,6 +108,7 @@ module ProcessOut {
this.prefilledData = prefilledData
this.formData = formData
this.payButtonText = payButtonText
this.markdownLibraryInstance = markdownLibraryInstance
this.onSubmitHandler = onSubmitHandler
this.formInputs = this.createFormInputs()
this.submitButton = this.createSubmitButton()
Expand Down Expand Up @@ -126,6 +141,9 @@ module ProcessOut {
})

form.appendChild(paymentProviderImage)
if (this.hasCustomerInstructions()) {
form.appendChild(this.createInstructionsElement())
}
form.appendChild(inputsWrapper)
form.appendChild(this.submitButton.getButtonElement())

Expand All @@ -134,6 +152,68 @@ module ProcessOut {
return form
}

/**
* This function draws the customer instructions again, once the markdown and QR code libraries are loaded
*/
public refreshCustomerInstructions(markdownLibraryInstance: any) {
if (!this.instructionsElement) {
return
}

this.markdownLibraryInstance = markdownLibraryInstance
this.instructionsElement.innerHTML = ""
this.renderCustomerInstructions()
}

/**
* This function checks whether the step sent a customer message or a QR code
*/
private hasCustomerInstructions() {
return (
!!NativeApmCustomerInstructions.getMessage(this.formData.customerInstructions) ||
!!NativeApmCustomerInstructions.getBarcode(this.formData.customerInstructions)
)
}

/**
* This function creates the container of the customer message and QR code sent with the step
*/
private createInstructionsElement() {
this.instructionsElement = document.createElement("div")
this.instructionsElement.setAttribute("class", "native-apm-instructions")
this.renderCustomerInstructions()

return this.instructionsElement
}

/**
* A rendering error must never break the form, so the instructions are skipped instead.
*/
private renderCustomerInstructions() {
const message = NativeApmCustomerInstructions.getMessage(this.formData.customerInstructions)
const barcode = NativeApmCustomerInstructions.getBarcode(this.formData.customerInstructions)

try {
if (message) {
this.instructionsElement.appendChild(
NativeApmCustomerInstructions.createMessageElement(
this.markdownLibraryInstance,
this.theme,
message,
),
)
}

if (barcode) {
this.instructionsElement.appendChild(
NativeApmCustomerInstructions.createQrCodeElement(this.theme, barcode),
)
}
} catch (error) {
console.error("Failed to render native APM customer instructions", error)
}
}

/**
* This function creates the form inputs
*/
Expand Down
Loading
Loading