diff --git a/README.md b/README.md index e3d9902..ed3c06e 100644 --- a/README.md +++ b/README.md @@ -15,10 +15,10 @@ The simplest method for integration is to load the compiled and distributed `giv "environment": "staging" } - + ``` -The easiest way to do this if you're using a Liquid-based Shopify theme is to copy-paste the Liquid snippet found in `snippets/givex-js.liquid` into your theme, and then rendering that snippet in your `theme.liquid` and `checkout.liquid`: +The easiest way to do this if you're using a Liquid-based Shopify theme is to copy-paste the Liquid snippet found in `snippets/givex-js.liquid` into your theme, and then render that snippet in your `theme.liquid` and `checkout.liquid`: ```liquid @@ -28,8 +28,12 @@ The easiest way to do this if you're using a Liquid-based Shopify theme is to co ``` -The library will automatically instantiate and initialise a top-level `givex` object in the document window, which your own code can then use to leverage Givex functionality. -When loaded in the checkout, the library will also automatically hook into the gift card input present on the page to add Givex gift card support for redemptions. +The library will automatically instantiate and initialise a top-level `givex` object in the document window, which your own code can then use to leverage Givex functionality, if you need it. + +However, the library will also automatically hook into key elements on your theme's pages to provide key functionality: + +* When loaded in the checkout, the library will automatically hook into the gift card input present on the page to add Givex gift card support for redemptions. See [Integrating Gift Card Redemption in the Checkout](#integrating-gift-card-redemption-in-the-checkout). +* When loaded on a page with elements marked up with specific data attributes, the library will automatically hook into a form to provide out of the box balance checking functionality. See [Integrating Balance Checking on a Page](#integrating-balance-checking-on-a-page). ### Making API Calls Once you have an initialised Givex object, making API calls is pretty simple: @@ -83,6 +87,31 @@ givex.api.preauth({ }); ``` +### Integrating Gift Card Redemption in the Checkout +If you've copied the `givex-js.liquid` snippet into your theme, and rendered it anywhere inside your `checkout.liquid` layout file, you should be done! + +The library will automatically hook into the gift card application box in the checkout, show and hide the gift card security code input as needed, and pass requests off to the Givex Integration without any further coding required. + +### Integrating Balance Checking on a Page +While you're welcome to use the API client directly to make requests to the `checkBalance` API endpoint, if you render the `givex-js.liquid` snippet on a page with a specially-marked-up form, the library will automatically hook into it and provide an out of the box balance checker for you. + +You're welcome to style your form however you like, the key data attributes you'll need to add are: + +```html +
+
+ + + +
+``` + +If these are present, then when a user submits the form via button click or hitting enter, the submission will be automatically intercepted and passeed to the Givex Integration. +The result of the balance check lookup will be rendered into the `
` DOM element. +If you'd like to customise how that rendered message is displayed, you can update the default templates in `givex-js.liquid`. + +The most common approach we've seen for balance checkers is to create a new specific page template in your theme (`page.balance-checker.liquid` or `page.balance-checker.json`) and create a dedicated balance checker page on your store. + ### Translations All text rendered by the library is translatable via Shopify's default locale functionality -- indeed, there's an expectation that translation keys will be added to the store's default locale file, whether that's `en.default.json` or something else. @@ -91,6 +120,15 @@ To apply the default translations, the following can be copied as a top-level ob ```json { "givex": { + "balance_checker": { + "title": "Check your gift card balance", + "number_label": "Enter card number", + "number_placeholder": "Enter card number", + "security_code_label": "Enter PIN", + "security_code_placeholder": "Enter PIN", + "submit": "Submit", + "loading": "Checking balance..." + }, "checkout": { "security_code_label": "Gift card? Enter PIN", "security_code_placeholder": "Gift card? Enter PIN" diff --git a/package.json b/package.json index a633ebf..6827603 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "givex-js", - "version": "0.2.0", + "version": "0.3.0", "description": "Javascript library for Disco Labs' Givex Integration for Shopify.", "repository": { "type": "git", diff --git a/snippets/givex-js.liquid b/snippets/givex-js.liquid index 147e031..6afc1b6 100644 --- a/snippets/givex-js.liquid +++ b/snippets/givex-js.liquid @@ -13,14 +13,14 @@ {%- liquid assign DEFAULT_CARD_CODE_LENGTH = 20 - assign DEFAULT_CDN_URL = 'https://cdn.jsdelivr.net/npm/givex-js@0.2.0/dist/givex.js' + assign DEFAULT_CDN_URL = 'https://cdn.jsdelivr.net/npm/givex-js@0.3.0/dist/givex.js' assign DEFAULT_ENDPOINT = 'https://givex-integration.discolabs.com/api/v1' assign DEFAULT_SECURITY_CODE_POLICY = 'security_code_is_required' -%} {%- comment -%} - HTML templates that may be rendered by the integration. + HTML templates that may be rendered by the integration on the checkout or balance checker pages. {%- endcomment -%} {%- capture HTML_TEMPLATE_CHECKOUT_SECURITY_CODE -%}
@@ -41,6 +41,24 @@
{%- endcapture -%} +{%- capture HTML_TEMPLATE_BALANCE_CHECKER_LOADING -%} +
+ {{ 'givex.balance_checker.loading' | t }} +
+{%- endcapture -%} + +{%- capture HTML_TEMPLATE_BALANCE_CHECKER_SUCCESS -%} +
+ {% raw %}{{ message }}{% endraw %} +
+{%- endcapture -%} + +{%- capture HTML_TEMPLATE_BALANCE_CHECKER_ERROR -%} +
+ {% raw %}{{ message }}{% endraw %} +
+{%- endcapture -%} + {%- comment -%} Attempt to parse translations as a JSON object. {%- endcomment -%} @@ -86,7 +104,10 @@ "endpoint": "{{ shop.metafields.givex.endpoint | default: DEFAULT_ENDPOINT }}", "security_code_policy": "{{ shop.metafields.givex.security_code_policy | default: DEFAULT_SECURITY_CODE_POLICY }}", "templates": { - "checkout_security_code": {{ HTML_TEMPLATE_CHECKOUT_SECURITY_CODE | json }} + "checkout_security_code": {{ HTML_TEMPLATE_CHECKOUT_SECURITY_CODE | json }}, + "balance_checker_loading": {{ HTML_TEMPLATE_BALANCE_CHECKER_LOADING | json }}, + "balance_checker_success": {{ HTML_TEMPLATE_BALANCE_CHECKER_SUCCESS | json }}, + "balance_checker_error": {{ HTML_TEMPLATE_BALANCE_CHECKER_ERROR | json }} }, "translations": {{ givex_translations }} } diff --git a/src/lib/balance_checker/balance_checker.js b/src/lib/balance_checker/balance_checker.js new file mode 100644 index 0000000..446431e --- /dev/null +++ b/src/lib/balance_checker/balance_checker.js @@ -0,0 +1,40 @@ +import { + SELECTOR_BALANCE_CHECKER_FORM, +} from "../constants"; +import { BalanceCheckerForm } from "./balance_checker_form"; + +export class BalanceChecker { + + constructor(document, api, config) { + this.document = document; + this.api = api; + this.config = config; + + this.initialise(); + } + + initialise() { + this.debug('initialise()'); + + const { document, api, config } = this; + + // define an event handler for page changes + document.querySelectorAll(SELECTOR_BALANCE_CHECKER_FORM).forEach(formElement => { + // skip if the form is already initialised + if(formElement.dataset.givex === 'true') { + return; + } + + new BalanceCheckerForm(formElement, api, config); + }); + } + + debug(...args) { + if(!this.config.debug) { + return; + } + + console.log('[Givex Balance Checker]', ...args); + } + +} diff --git a/src/lib/balance_checker/balance_checker_form.js b/src/lib/balance_checker/balance_checker_form.js new file mode 100644 index 0000000..fd0c786 --- /dev/null +++ b/src/lib/balance_checker/balance_checker_form.js @@ -0,0 +1,110 @@ +import { + SECURITY_CODE_POLICY_IS_REQUIRED, + SELECTOR_BALANCE_CHECKER_NUMBER, + SELECTOR_BALANCE_CHECKER_PIN, + SELECTOR_BALANCE_CHECKER_RESULT, + SELECTOR_BALANCE_CHECKER_SUBMIT +} from "../constants"; +import {renderHtmlTemplate} from "../helpers"; + +export class BalanceCheckerForm { + + constructor(formElement, api, config) { + this.formElement = formElement; + this.api = api; + this.config = config; + + this.initialise(); + } + + initialise() { + this.debug('initialise()'); + + const { formElement } = this; + + // store references to other elements + this.numberElement = formElement.querySelector(SELECTOR_BALANCE_CHECKER_NUMBER); + this.pinElement = formElement.querySelector(SELECTOR_BALANCE_CHECKER_PIN); + this.resultElement = formElement.querySelector(SELECTOR_BALANCE_CHECKER_RESULT); + this.submitElement = formElement.querySelector(SELECTOR_BALANCE_CHECKER_SUBMIT); + + // register event listeners + this.formElement.addEventListener('submit', this.handleSubmit.bind(this)); + + // mark this form element as initialised + formElement.dataset.givex = 'true'; + } + + handleSubmit(e) { + this.debug('handleSubmit()', e); + + const { numberElement, pinElement, submitElement, resultElement, api, config } = this; + + // prevent form submission + e.preventDefault(); + e.stopPropagation(); + + // if the security code input is required, present and empty, focus it and return + if((config.security_code_policy === SECURITY_CODE_POLICY_IS_REQUIRED) && pinElement && pinElement.value.trim().length === 0) { + pinElement.focus(); + return false; + } + + // add loading spinner and disable the button to prevent resubmission. + submitElement.classList.add('btn--loading'); + submitElement.disabled = true; + + // render the blank result state + this.resultElement = renderHtmlTemplate(config, resultElement, "balance_checker_loading", {}, "replaceWith"); + + // build values for preauthorisation request + const number = numberElement.value; + const pin = pinElement ? pinElement.value : null; + + // make balance check request + api.checkBalance({ + number, + pin, + onSuccess: this.handleBalanceCheckSuccess.bind(this), + onFailure: this.handleBalanceCheckFailure.bind(this) + }); + } + + handleBalanceCheckSuccess(result) { + this.debug('handleBalanceCheckSuccess', result); + + const { config, resultElement } = this; + this.resultElement = renderHtmlTemplate(config, resultElement, "balance_checker_success", { + message: result.message + }, "replaceWith"); + + this.handleBalanceCheckComplete(); + } + + handleBalanceCheckFailure(error) { + this.debug('handleBalanceCheckFailure', error); + + const { config, resultElement } = this; + this.resultElement = renderHtmlTemplate(config, resultElement, "balance_checker_error", { + message: error.message + }, "replaceWith"); + + this.handleBalanceCheckComplete(); + } + + handleBalanceCheckComplete() { + const { submitElement } = this; + + submitElement.classList.remove('btn--loading'); + submitElement.disabled = false; + } + + debug(...args) { + if(!this.config.debug) { + return; + } + + console.log('[Givex BalanceCheckerForm]', ...args); + } + +} diff --git a/src/lib/constants.js b/src/lib/constants.js index e7a9306..8954f54 100644 --- a/src/lib/constants.js +++ b/src/lib/constants.js @@ -2,6 +2,12 @@ export const STEP_CONTACT_INFORMATION = 'contact_information'; export const STEP_SHIPPING_METHOD = 'shipping_method'; export const STEP_PAYMENT_METHOD = 'payment_method'; +export const SELECTOR_BALANCE_CHECKER_FORM = '[data-givex-balance-checker="form"]'; +export const SELECTOR_BALANCE_CHECKER_NUMBER = '[data-givex-balance-checker="number"]'; +export const SELECTOR_BALANCE_CHECKER_PIN = '[data-givex-balance-checker="pin"]'; +export const SELECTOR_BALANCE_CHECKER_RESULT = '[data-givex-balance-checker="result"]'; +export const SELECTOR_BALANCE_CHECKER_SUBMIT = '[data-givex-balance-checker="submit"]'; + export const SELECTOR_DISCOUNT_INPUT = '[data-discount-field="true"]'; export const SELECTOR_FIELDSET = '.fieldset'; export const SELECTOR_FORM = 'form'; diff --git a/src/lib/givex.js b/src/lib/givex.js index e2f179f..fea9628 100644 --- a/src/lib/givex.js +++ b/src/lib/givex.js @@ -1,5 +1,6 @@ import { ApiClient } from "./api_client"; import { Checkout } from "./checkout/checkout"; +import { BalanceChecker } from "./balance_checker/balance_checker"; export class Givex { @@ -7,6 +8,7 @@ export class Givex { const api = new ApiClient(config); this.api = api; this.checkout = new Checkout(document, Shopify, api, config); + this.balanceChecker = new BalanceChecker(document, api, config); } } diff --git a/src/lib/helpers.js b/src/lib/helpers.js index e36e5ca..e2e6c69 100644 --- a/src/lib/helpers.js +++ b/src/lib/helpers.js @@ -6,10 +6,18 @@ export const parseJSONScript = (document, id) => { } catch { return null; } }; -// render a HTML template after the given target element -export const renderHtmlTemplate = (config, targetElement, templateName) => { - const templateDocument = new DOMParser().parseFromString(config.templates[templateName], 'text/html'); +// render a HTML template after the given target element, with optional context interpolation +// the newly rendered element is returned +export const renderHtmlTemplate = (config, targetElement, templateName, context = {}, renderMethod = 'after') => { + const interpolatedTemplate = Object.entries(context).reduce((output, value) => { + const [k, v] = value; + return output.replace(new RegExp(`{{ ${k} }}`, 'g'), v); + }, config.templates[templateName]); + + const templateDocument = new DOMParser().parseFromString(interpolatedTemplate, 'text/html'); const templateElement = templateDocument.querySelector('body > *'); - targetElement.after(templateElement); + targetElement[renderMethod](templateElement); + + return templateElement; };