= T & { readonly __preserve__: 'preserve' }
+
+/*
+ Transforms `T` into an identical shape, but with all `Preserve` types mapped back to `P`:
+
+ ```
+ ResolvePreserve> == T
+ ```
+
+ It can be useful to make an interface more dev-friendly. Consider this:
+
+ ```
+ interface Test {
+ className?: string
+ container?: Preserve
+ }
+ ```
+
+ Because `container` uses `Preserve`, its type is actually:
+
+ ```
+ HTMLElement & { readonly __preserve__: 'preserve' }
+ ```
+
+ Since this is not assignable to `HTMLElement`, using `Test` directly requires using an explicit
+ casting for all its "preserved" properties, which is cumbersome:
+
+ ```
+ declare const test: Test
+ const container: HTMLElement = test.container as HTMLElement
+ ```
+
+ Using `ResolvePreserve`
+ */
+export type ResolvePreserve = NonNullable extends Preserve
+ ? P
+ : {
+ [K in keyof T]: NonNullable extends Builtin
+ ? T[K]
+ : NonNullable extends AnyArray
+ ? T[K]
+ : IsObject> extends true
+ ? ResolvePreserve
+ : T[K]
+ }
+
+// Just like DeepRequired, but will not recurse into `Preserve` types: will map them back to `P`
+export type PreservedDeepRequired = NonNullable extends Preserve
+ ? P
+ : {
+ [K in keyof T]-?: NonNullable extends Builtin
+ ? T[K]
+ : NonNullable extends AnyArray
+ ? T[K]
+ : IsObject> extends true
+ ? PreservedDeepRequired
+ : T[K]
+ }
diff --git a/src/utils/index.ts b/src/utils/index.ts
index 3a5c340..ee989f3 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -1,97 +1,108 @@
-import {DOMContent, integer} from "../types";
-
-export function createRootElement(className: string = "", tagName: keyof HTMLElementTagNameMap = "div"): HTMLElement {
- let element = document.createElement(tagName);
- element.className = className;
- return element;
+import { DOMContent, integer } from '../types'
+
+export function createRootElement(
+ className = '',
+ tagName: keyof HTMLElementTagNameMap = 'div'
+): HTMLElement {
+ const element = document.createElement(tagName)
+ element.className = className
+ return element
}
-export function createListElement(items: DOMContent[], listClass: string = "", itemClass: string = ""): HTMLUListElement {
- let list = document.createElement("ul");
- list.className = listClass;
+export function createListElement(
+ items: DOMContent[],
+ listClass = '',
+ itemClass = ''
+): HTMLUListElement {
+ const list = document.createElement('ul')
+ list.className = listClass
- let listItems = items.map(i => {
- let li = document.createElement("li");
- li.className = itemClass;
- setDOMContent(li, i);
- return li;
- });
+ const listItems = items.map((i) => {
+ const li = document.createElement('li')
+ li.className = itemClass
+ setDOMContent(li, i)
+ return li
+ })
- setDOMContent(list, listItems);
+ setDOMContent(list, listItems)
- return list;
+ return list
}
-
export function priceToCents(price: number): integer {
- return Math.round(price * 100);
+ return Math.round(price * 100)
}
export function priceFromCents(cents: integer): number {
- return Number((cents / 100).toFixed(2));
+ return Number((cents / 100).toFixed(2))
}
export function formatCents(cents: integer): string {
- return String(priceFromCents(cents)).replace(".", ",");
+ return String(priceFromCents(cents)).replace('.', ',')
}
-export function setDOMContent(container: HTMLElement, content: DOMContent) {
- if (typeof content === "string") {
- container.innerHTML = content;
+export function setDOMContent(container: HTMLElement, content: DOMContent): void {
+ if (typeof content === 'string') {
+ container.innerHTML = content
} else {
- container.innerHTML = "";
+ container.innerHTML = ''
if (!Array.isArray(content)) {
- content = [content];
+ content = [content]
}
- for (let el of content) {
- container.appendChild(el);
+ for (const el of content) {
+ container.appendChild(el)
}
}
}
-export function imageWithSrc(src: string, className: string = ""): HTMLImageElement {
- let image = new Image();
- image.src = src;
- image.className = className;
- return image;
+export function imageWithSrc(src: string, className = ''): HTMLImageElement {
+ const image = new Image()
+ image.src = src
+ image.className = className
+ return image
}
-
-export function isSameDate(date1: Date, date2: Date) {
- return date1.getUTCDate() === date2.getUTCDate() && date1.getUTCMonth() === date2.getUTCMonth() && date1.getUTCFullYear() === date2.getUTCFullYear()
+export function isSameDate(date1: Date, date2: Date): boolean {
+ return (
+ date1.getUTCDate() === date2.getUTCDate() &&
+ date1.getUTCMonth() === date2.getUTCMonth() &&
+ date1.getUTCFullYear() === date2.getUTCFullYear()
+ )
}
-export function isToday(date: Date) {
- return isSameDate(date, new Date());
+export function isToday(date: Date): boolean {
+ return isSameDate(date, new Date())
}
-export function isYesterday(date: Date) {
- let today = new Date();
- return isSameDate(date, new Date(today.setDate(today.getDate() - 1)));
+export function isYesterday(date: Date): boolean {
+ const today = new Date()
+ return isSameDate(date, new Date(today.setDate(today.getDate() - 1)))
}
-
-export function humanizedDate(date: Date, addArticle: boolean = false, forceDate = false) {
+export function humanizedDate(date: Date, addArticle = false, forceDate = false): string {
if (isToday(date) && !forceDate) {
- return "aujourd'hui";
+ return "aujourd'hui"
}
if (isYesterday(date) && !forceDate) {
- return "hier";
+ return 'hier'
}
- let article = addArticle ? "le " : "";
- return article + date.toLocaleDateString();
+ const article = addArticle ? 'le ' : ''
+ return article + date.toLocaleDateString()
}
export function joinInstallmentsCounts(installmentsCounts: integer[]): string {
if (installmentsCounts.length === 1) {
- return String(installmentsCounts[0]);
+ return String(installmentsCounts[0])
} else if (installmentsCounts.length === 2) {
- return installmentsCounts.join(" ou ");
+ return installmentsCounts.join(' ou ')
} else {
- return installmentsCounts.slice(0, installmentsCounts.length - 1).join(", ") + ` ou ${installmentsCounts[installmentsCounts.length - 1]}`;
+ return (
+ installmentsCounts.slice(0, installmentsCounts.length - 1).join(', ') +
+ ` ou ${installmentsCounts[installmentsCounts.length - 1]}`
+ )
}
}
diff --git a/src/utils/polyfills.js b/src/utils/polyfills.js
index 10cf296..092f5b4 100644
--- a/src/utils/polyfills.js
+++ b/src/utils/polyfills.js
@@ -1,248 +1,339 @@
+/* eslint-disable */
+
/*
* Element.prototype.closest & Element.prototype.matches polyfills
*/
-(function (ElementProto) {
+;(function (ElementProto) {
if (typeof ElementProto.matches !== 'function') {
- ElementProto.matches = ElementProto.msMatchesSelector || ElementProto.mozMatchesSelector || ElementProto.webkitMatchesSelector || function matches(selector) {
- var element = this;
- var elements = (element.document || element.ownerDocument).querySelectorAll(selector);
- var index = 0;
+ ElementProto.matches =
+ ElementProto.msMatchesSelector ||
+ ElementProto.mozMatchesSelector ||
+ ElementProto.webkitMatchesSelector ||
+ function matches(selector) {
+ var element = this
+ var elements = (element.document || element.ownerDocument).querySelectorAll(selector)
+ var index = 0
- while (elements[index] && elements[index] !== element) {
- ++index;
- }
+ while (elements[index] && elements[index] !== element) {
+ ++index
+ }
- return Boolean(elements[index]);
- };
+ return Boolean(elements[index])
+ }
}
if (typeof ElementProto.closest !== 'function') {
ElementProto.closest = function closest(selector) {
- var element = this;
+ var element = this
while (element && element.nodeType === 1) {
if (element.matches(selector)) {
- return element;
+ return element
}
- element = element.parentNode;
+ element = element.parentNode
}
- return null;
- };
+ return null
+ }
}
-})(window.Element.prototype);
-
+})(window.Element.prototype)
/*
* Element.prototype.classList polyfill
*/
// 1. String.prototype.trim polyfill
-if (!"".trim) String.prototype.trim = function () {
- return this.replace(/^[\s]+|[\s]+$/g, '');
-};
-
-(function (window) {
- "use strict"; // prevent global namespace pollution
- if (!window.DOMException) (DOMException = function (reason) {
- this.message = reason
- }).prototype = new Error;
- var wsRE = /[\11\12\14\15\40]/, wsIndex = 0, checkIfValidClassListEntry = function (O, V) {
- if (V === "") throw new DOMException(
- "Failed to execute '" + O + "' on 'DOMTokenList': The token provided must not be empty.");
- if ((wsIndex = V.search(wsRE)) !== -1) throw new DOMException("Failed to execute '" + O + "' on 'DOMTokenList': " +
- "The token provided ('" + V[wsIndex] + "') contains HTML space characters, which are not valid in tokens.");
+if (!''.trim)
+ String.prototype.trim = function () {
+ return this.replace(/^[\s]+|[\s]+$/g, '')
}
-// 2. Implement the barebones DOMTokenList livelyness polyfill
- if (typeof DOMTokenList !== "function") (function (window) {
- var document = window.document, Object = window.Object,
- hasOwnProp = Object.prototype.hasOwnProperty;
- var defineProperty = Object.defineProperty, allowTokenListConstruction = 0, skipPropChange = 0;
-
- var DOMTokenList = function () {
- if (!allowTokenListConstruction) throw TypeError("Illegal constructor"); // internally let it through
- };
-
- DOMTokenList.prototype.toString = DOMTokenList.prototype.toLocaleString = function () {
- return this.value
- };
- DOMTokenList.prototype.add = function () {
- a: for (var v = 0, argLen = arguments.length, val = "", ele = this[" uCL"], proto = ele[" uCLp"]; v !== argLen; ++v) {
- val = arguments[v] + "", checkIfValidClassListEntry("add", val);
- for (var i = 0, Len = proto.length, resStr = val; i !== Len; ++i)
- if (this[i] === val) continue a; else resStr += " " + this[i];
- this[Len] = val, proto.length += 1, proto.value = resStr;
- }
- skipPropChange = 1, ele.className = proto.value, skipPropChange = 0;
- };
- DOMTokenList.prototype.remove = function () {
- for (var v = 0, argLen = arguments.length, val = "", ele = this[" uCL"], proto = ele[" uCLp"]; v !== argLen; ++v) {
- val = arguments[v] + "", checkIfValidClassListEntry("remove", val);
- for (var i = 0, Len = proto.length, resStr = "", is = 0; i !== Len; ++i)
- if (is) {
- this[i - 1] = this[i]
- } else {
- if (this[i] !== val) {
- resStr += this[i] + " ";
+;(function (window) {
+ 'use strict' // prevent global namespace pollution
+ if (!window.DOMException)
+ (DOMException = function (reason) {
+ this.message = reason
+ }).prototype = new Error()
+ var wsRE = /[\11\12\14\15\40]/,
+ wsIndex = 0,
+ checkIfValidClassListEntry = function (O, V) {
+ if (V === '')
+ throw new DOMException(
+ "Failed to execute '" + O + "' on 'DOMTokenList': The token provided must not be empty."
+ )
+ if ((wsIndex = V.search(wsRE)) !== -1)
+ throw new DOMException(
+ "Failed to execute '" +
+ O +
+ "' on 'DOMTokenList': " +
+ "The token provided ('" +
+ V[wsIndex] +
+ "') contains HTML space characters, which are not valid in tokens."
+ )
+ }
+ // 2. Implement the barebones DOMTokenList livelyness polyfill
+ if (typeof DOMTokenList !== 'function')
+ (function (window) {
+ var document = window.document,
+ Object = window.Object,
+ hasOwnProp = Object.prototype.hasOwnProperty
+ var defineProperty = Object.defineProperty,
+ allowTokenListConstruction = 0,
+ skipPropChange = 0
+
+ var DOMTokenList = function () {
+ if (!allowTokenListConstruction) throw TypeError('Illegal constructor') // internally let it through
+ }
+
+ DOMTokenList.prototype.toString = DOMTokenList.prototype.toLocaleString = function () {
+ return this.value
+ }
+ DOMTokenList.prototype.add = function () {
+ a: for (
+ var v = 0, argLen = arguments.length, val = '', ele = this[' uCL'], proto = ele[' uCLp'];
+ v !== argLen;
+ ++v
+ ) {
+ ;(val = arguments[v] + ''), checkIfValidClassListEntry('add', val)
+ for (var i = 0, Len = proto.length, resStr = val; i !== Len; ++i)
+ if (this[i] === val) continue a
+ else resStr += ' ' + this[i]
+ ;(this[Len] = val), (proto.length += 1), (proto.value = resStr)
+ }
+ ;(skipPropChange = 1), (ele.className = proto.value), (skipPropChange = 0)
+ }
+ DOMTokenList.prototype.remove = function () {
+ for (
+ var v = 0, argLen = arguments.length, val = '', ele = this[' uCL'], proto = ele[' uCLp'];
+ v !== argLen;
+ ++v
+ ) {
+ ;(val = arguments[v] + ''), checkIfValidClassListEntry('remove', val)
+ for (var i = 0, Len = proto.length, resStr = '', is = 0; i !== Len; ++i)
+ if (is) {
+ this[i - 1] = this[i]
} else {
- is = 1;
+ if (this[i] !== val) {
+ resStr += this[i] + ' '
+ } else {
+ is = 1
+ }
}
- }
- if (!is) continue;
- delete this[Len], proto.length -= 1, proto.value = resStr;
- }
- skipPropChange = 1, ele.className = proto.value, skipPropChange = 0;
- };
- window.DOMTokenList = DOMTokenList;
-
- function whenPropChanges() {
- var evt = window.event, prop = evt.propertyName;
- if (!skipPropChange && (prop === "className" || (prop === "classList" && !defineProperty))) {
- var target = evt.srcElement, protoObjProto = target[" uCLp"], strval = "" + target[prop];
- var tokens = strval.trim().split(wsRE),
- resTokenList = target[prop === "classList" ? " uCL" : "classList"];
- var oldLen = protoObjProto.length;
- a: for (var cI = 0, cLen = protoObjProto.length = tokens.length, sub = 0; cI !== cLen; ++cI) {
- for (var innerI = 0; innerI !== cI; ++innerI) if (tokens[innerI] === tokens[cI]) {
- sub++;
- continue a;
- }
- resTokenList[cI - sub] = tokens[cI];
+ if (!is) continue
+ delete this[Len], (proto.length -= 1), (proto.value = resStr)
}
- for (var i = cLen - sub; i < oldLen; ++i) delete resTokenList[i]; //remove trailing indexs
- if (prop !== "classList") return;
- skipPropChange = 1, target.classList = resTokenList, target.className = strval;
- skipPropChange = 0, resTokenList.length = tokens.length - sub;
+ ;(skipPropChange = 1), (ele.className = proto.value), (skipPropChange = 0)
}
- }
+ window.DOMTokenList = DOMTokenList
- function polyfillClassList(ele) {
- if (!ele || !("innerHTML" in ele)) throw TypeError("Illegal invocation");
- ele.detachEvent("onpropertychange", whenPropChanges); // prevent duplicate handler infinite loop
- allowTokenListConstruction = 1;
- try {
- var protoObj = function () {
- };
-
- protoObj.prototype = new DOMTokenList();
- } finally {
- allowTokenListConstruction = 0
- }
- var protoObjProto = protoObj.prototype, resTokenList = new protoObj();
- a: for (var toks = ele.className.trim().split(wsRE), cI = 0, cLen = toks.length, sub = 0; cI !== cLen; ++cI) {
- for (var innerI = 0; innerI !== cI; ++innerI) if (toks[innerI] === toks[cI]) {
- sub++;
- continue a;
+ function whenPropChanges() {
+ var evt = window.event,
+ prop = evt.propertyName
+ if (
+ !skipPropChange &&
+ (prop === 'className' || (prop === 'classList' && !defineProperty))
+ ) {
+ var target = evt.srcElement,
+ protoObjProto = target[' uCLp'],
+ strval = '' + target[prop]
+ var tokens = strval.trim().split(wsRE),
+ resTokenList = target[prop === 'classList' ? ' uCL' : 'classList']
+ var oldLen = protoObjProto.length
+ a: for (
+ var cI = 0, cLen = (protoObjProto.length = tokens.length), sub = 0;
+ cI !== cLen;
+ ++cI
+ ) {
+ for (var innerI = 0; innerI !== cI; ++innerI)
+ if (tokens[innerI] === tokens[cI]) {
+ sub++
+ continue a
+ }
+ resTokenList[cI - sub] = tokens[cI]
+ }
+ for (var i = cLen - sub; i < oldLen; ++i) delete resTokenList[i] //remove trailing indexs
+ if (prop !== 'classList') return
+ ;(skipPropChange = 1), (target.classList = resTokenList), (target.className = strval)
+ ;(skipPropChange = 0), (resTokenList.length = tokens.length - sub)
}
- this[cI - sub] = toks[cI];
}
- protoObjProto.length = cLen - sub, protoObjProto.value = ele.className, protoObjProto[" uCL"] = ele;
- if (defineProperty) {
- defineProperty(ele, "classList", { // IE8 & IE9 allow defineProperty on the DOM
- enumerable: 1, get: function () {
- return resTokenList
- },
- configurable: 0, set: function (newVal) {
- skipPropChange = 1, ele.className = protoObjProto.value = (newVal += ""), skipPropChange = 0;
- var toks = newVal.trim().split(wsRE), oldLen = protoObjProto.length;
- a: for (var cI = 0, cLen = protoObjProto.length = toks.length, sub = 0; cI !== cLen; ++cI) {
- for (var innerI = 0; innerI !== cI; ++innerI) if (toks[innerI] === toks[cI]) {
- sub++;
- continue a;
- }
- resTokenList[cI - sub] = toks[cI];
+
+ function polyfillClassList(ele) {
+ if (!ele || !('innerHTML' in ele)) throw TypeError('Illegal invocation')
+ ele.detachEvent('onpropertychange', whenPropChanges) // prevent duplicate handler infinite loop
+ allowTokenListConstruction = 1
+ try {
+ var protoObj = function () {}
+
+ protoObj.prototype = new DOMTokenList()
+ } finally {
+ allowTokenListConstruction = 0
+ }
+ var protoObjProto = protoObj.prototype,
+ resTokenList = new protoObj()
+ a: for (
+ var toks = ele.className.trim().split(wsRE), cI = 0, cLen = toks.length, sub = 0;
+ cI !== cLen;
+ ++cI
+ ) {
+ for (var innerI = 0; innerI !== cI; ++innerI)
+ if (toks[innerI] === toks[cI]) {
+ sub++
+ continue a
}
- for (var i = cLen - sub; i < oldLen; ++i) delete resTokenList[i]; //remove trailing indexs
- }
- });
- defineProperty(ele, " uCLp", { // for accessing the hidden prototype
- enumerable: 0, configurable: 0, writeable: 0, value: protoObj.prototype
- });
- defineProperty(protoObjProto, " uCL", {
- enumerable: 0, configurable: 0, writeable: 0, value: ele
- });
- } else {
- ele.classList = resTokenList, ele[" uCL"] = resTokenList, ele[" uCLp"] = protoObj.prototype;
- }
- ele.attachEvent("onpropertychange", whenPropChanges);
- }
+ this[cI - sub] = toks[cI]
+ }
+ ;(protoObjProto.length = cLen - sub),
+ (protoObjProto.value = ele.className),
+ (protoObjProto[' uCL'] = ele)
+ if (defineProperty) {
+ defineProperty(ele, 'classList', {
+ // IE8 & IE9 allow defineProperty on the DOM
+ enumerable: 1,
+ get: function () {
+ return resTokenList
+ },
+ configurable: 0,
+ set: function (newVal) {
+ ;(skipPropChange = 1),
+ (ele.className = protoObjProto.value = newVal += ''),
+ (skipPropChange = 0)
+ var toks = newVal.trim().split(wsRE),
+ oldLen = protoObjProto.length
+ a: for (
+ var cI = 0, cLen = (protoObjProto.length = toks.length), sub = 0;
+ cI !== cLen;
+ ++cI
+ ) {
+ for (var innerI = 0; innerI !== cI; ++innerI)
+ if (toks[innerI] === toks[cI]) {
+ sub++
+ continue a
+ }
+ resTokenList[cI - sub] = toks[cI]
+ }
+ for (var i = cLen - sub; i < oldLen; ++i) delete resTokenList[i] //remove trailing indexs
+ },
+ })
+ defineProperty(ele, ' uCLp', {
+ // for accessing the hidden prototype
+ enumerable: 0,
+ configurable: 0,
+ writeable: 0,
+ value: protoObj.prototype,
+ })
+ defineProperty(protoObjProto, ' uCL', {
+ enumerable: 0,
+ configurable: 0,
+ writeable: 0,
+ value: ele,
+ })
+ } else {
+ ;(ele.classList = resTokenList),
+ (ele[' uCL'] = resTokenList),
+ (ele[' uCLp'] = protoObj.prototype)
+ }
+ ele.attachEvent('onpropertychange', whenPropChanges)
+ }
- try { // Much faster & cleaner version for IE8 & IE9:
- // Should work in IE8 because Element.prototype instanceof Node is true according to the specs
- window.Object.defineProperty(window.Element.prototype, "classList", {
- enumerable: 1, get: function (val) {
- if (!hasOwnProp.call(this, "classList")) polyfillClassList(this);
- return this.classList;
- },
- configurable: 0, set: function (val) {
- this.className = val
+ try {
+ // Much faster & cleaner version for IE8 & IE9:
+ // Should work in IE8 because Element.prototype instanceof Node is true according to the specs
+ window.Object.defineProperty(window.Element.prototype, 'classList', {
+ enumerable: 1,
+ get: function (val) {
+ if (!hasOwnProp.call(this, 'classList')) polyfillClassList(this)
+ return this.classList
+ },
+ configurable: 0,
+ set: function (val) {
+ this.className = val
+ },
+ })
+ } catch (e) {
+ // Less performant fallback for older browsers (IE 6-8):
+ window[' uCL'] = polyfillClassList
+ // the below code ensures polyfillClassList is applied to all current and future elements in the doc.
+ document.documentElement.firstChild.appendChild(
+ document.createElement('style')
+ ).styleSheet.cssText =
+ '_*{x-uCLp:expression(!this.hasOwnProperty("classList")&&window[" uCL"](this))}' + // IE6
+ '[class]{x-uCLp/**/:expression(!this.hasOwnProperty("classList")&&window[" uCL"](this))}' //IE7-8
+ }
+ })(window)
+ // 3. Patch in unsupported methods in DOMTokenList
+ ;(function (DOMTokenListProto, testClass) {
+ if (!DOMTokenListProto.item)
+ DOMTokenListProto.item = function (i) {
+ function NullCheck(n) {
+ return n === void 0 ? null : n
}
- });
- } catch (e) { // Less performant fallback for older browsers (IE 6-8):
- window[" uCL"] = polyfillClassList;
- // the below code ensures polyfillClassList is applied to all current and future elements in the doc.
- document.documentElement.firstChild.appendChild(document.createElement('style')).styleSheet.cssText = (
- '_*{x-uCLp:expression(!this.hasOwnProperty("classList")&&window[" uCL"](this))}' + // IE6
- '[class]{x-uCLp/**/:expression(!this.hasOwnProperty("classList")&&window[" uCL"](this))}' //IE7-8
- );
- }
- })(window);
-// 3. Patch in unsupported methods in DOMTokenList
- (function (DOMTokenListProto, testClass) {
- if (!DOMTokenListProto.item) DOMTokenListProto.item = function (i) {
- function NullCheck(n) {
- return n === void 0 ? null : n
- }
-
- return NullCheck(this[i]);
- };
- if (!DOMTokenListProto.toggle || testClass.toggle("a", 0) !== false) DOMTokenListProto.toggle = function (val) {
- if (arguments.length > 1) return (this[arguments[1] ? "add" : "remove"](val), !!arguments[1]);
- var oldValue = this.value;
- return (this.remove(oldValue), oldValue === this.value && (this.add(val), true) /*|| false*/);
- };
- if (!DOMTokenListProto.replace || typeof testClass.replace("a", "b") !== "boolean")
+
+ return NullCheck(this[i])
+ }
+ if (!DOMTokenListProto.toggle || testClass.toggle('a', 0) !== false)
+ DOMTokenListProto.toggle = function (val) {
+ if (arguments.length > 1) return this[arguments[1] ? 'add' : 'remove'](val), !!arguments[1]
+ var oldValue = this.value
+ return this.remove(oldValue), oldValue === this.value && (this.add(val), true) /*|| false*/
+ }
+ if (!DOMTokenListProto.replace || typeof testClass.replace('a', 'b') !== 'boolean')
DOMTokenListProto.replace = function (oldToken, newToken) {
- checkIfValidClassListEntry("replace", oldToken), checkIfValidClassListEntry("replace", newToken);
- var oldValue = this.value;
- return (this.remove(oldToken), this.value !== oldValue && (this.add(newToken), true));
- };
- if (!DOMTokenListProto.contains) DOMTokenListProto.contains = function (value) {
- for (var i = 0, Len = this.length; i !== Len; ++i) if (this[i] === value) return true;
- return false;
- };
- if (!DOMTokenListProto.forEach) DOMTokenListProto.forEach = function (f) {
- if (arguments.length === 1) for (var i = 0, Len = this.length; i !== Len; ++i) f(this[i], i, this);
- else for (var i = 0, Len = this.length, tArg = arguments[1]; i !== Len; ++i) f.call(tArg, this[i], i, this);
- };
- if (!DOMTokenListProto.entries) DOMTokenListProto.entries = function () {
- var nextIndex = 0, that = this;
- return {
- next: function () {
- return nextIndex < that.length ? {
- value: [nextIndex, that[nextIndex]],
- done: false
- } : {done: true};
+ checkIfValidClassListEntry('replace', oldToken),
+ checkIfValidClassListEntry('replace', newToken)
+ var oldValue = this.value
+ return this.remove(oldToken), this.value !== oldValue && (this.add(newToken), true)
+ }
+ if (!DOMTokenListProto.contains)
+ DOMTokenListProto.contains = function (value) {
+ for (var i = 0, Len = this.length; i !== Len; ++i) if (this[i] === value) return true
+ return false
+ }
+ if (!DOMTokenListProto.forEach)
+ DOMTokenListProto.forEach = function (f) {
+ if (arguments.length === 1)
+ for (var i = 0, Len = this.length; i !== Len; ++i) f(this[i], i, this)
+ else
+ for (var i = 0, Len = this.length, tArg = arguments[1]; i !== Len; ++i)
+ f.call(tArg, this[i], i, this)
+ }
+ if (!DOMTokenListProto.entries)
+ DOMTokenListProto.entries = function () {
+ var nextIndex = 0,
+ that = this
+ return {
+ next: function () {
+ return nextIndex < that.length
+ ? {
+ value: [nextIndex, that[nextIndex]],
+ done: false,
+ }
+ : { done: true }
+ },
}
- };
- };
- if (!DOMTokenListProto.values) DOMTokenListProto.values = function () {
- var nextIndex = 0, that = this;
- return {
- next: function () {
- return nextIndex < that.length ? {value: that[nextIndex], done: false} : {done: true};
+ }
+ if (!DOMTokenListProto.values)
+ DOMTokenListProto.values = function () {
+ var nextIndex = 0,
+ that = this
+ return {
+ next: function () {
+ return nextIndex < that.length
+ ? { value: that[nextIndex], done: false }
+ : { done: true }
+ },
}
- };
- };
- if (!DOMTokenListProto.keys) DOMTokenListProto.keys = function () {
- var nextIndex = 0, that = this;
- return {
- next: function () {
- return nextIndex < that.length ? {value: nextIndex, done: false} : {done: true};
+ }
+ if (!DOMTokenListProto.keys)
+ DOMTokenListProto.keys = function () {
+ var nextIndex = 0,
+ that = this
+ return {
+ next: function () {
+ return nextIndex < that.length ? { value: nextIndex, done: false } : { done: true }
+ },
}
- };
- };
- })(window.DOMTokenList.prototype, window.document.createElement("div").classList);
-})(window);
+ }
+ })(window.DOMTokenList.prototype, window.document.createElement('div').classList)
+})(window)
diff --git a/src/widgets/base.ts b/src/widgets/base.ts
index 00a9526..fedf528 100644
--- a/src/widgets/base.ts
+++ b/src/widgets/base.ts
@@ -1,74 +1,80 @@
-import {DOMContent} from "../types";
-import {Client} from "alma-js-client";
-import {setDOMContent} from "../utils";
-import WidgetsController from "../widgets_controller";
-import {RenderingFunc, WidgetFactoryFunc} from "./types";
-
-export interface WidgetConstructor {
- new(almaClient: Client, options: any): Widget;
-}
-
-export interface WidgetSettings {
- // CSS selector to a single DOM element, or a DOM Element itself into which the widget must render
- container: string | HTMLElement;
- // Override the rendering of a widget by providing your own rendering function.
- render?: RenderingFunc;
-}
-
-type WidgetConfig = WidgetSettings;
-
-export abstract class Widget {
- protected _config: WidgetConfig;
- protected readonly _almaClient: Client;
-
- protected constructor(almaClient: Client, options: WidgetSettings) {
- this._config = options;
- this._almaClient = almaClient;
+import { DOMContent } from '@/types'
+import { Client } from '@alma/client'
+import { setDOMContent } from '@/utils'
+import { WidgetsController } from '@/widgets_controller'
+import { WidgetFactoryFunc } from './types'
+import {
+ DefaultWidgetConfig,
+ makeConfig,
+ SettingsLiteral,
+ WidgetConfig,
+ WidgetSettings,
+} from '@/widgets/config'
+
+export type ConstructorFor = T extends Widget
+ ? new (almaClient: Client, settings: SettingsLiteral) => T
+ : never
+
+export type SettingsFor = T extends Widget
+ ? SettingsLiteral
+ : never
+
+export abstract class Widget {
+ private readonly _config: WidgetConfig
+
+ constructor(protected readonly almaClient: Client, settings: SettingsLiteral) {
+ this._config = makeConfig(this.defaultConfig(), settings)
}
- get config(): WidgetConfig {
- return {...this._config};
+ abstract defaultConfig(): DefaultWidgetConfig
+
+ get config(): WidgetConfig {
+ return { ...this._config }
}
private get container(): HTMLElement {
- let container: HTMLElement | null;
+ let container: HTMLElement
- if (typeof this._config.container === "string") {
- container = document.querySelector(this._config.container);
+ if (typeof this._config.container === 'string') {
+ const foundElement = document.querySelector(this._config.container)
- if (!container) {
- throw new Error(`Container element '${this._config.container}' not found`);
- }
+ if (!foundElement) {
+ throw new Error(`Container element '${this._config.container}' not found`)
+ } else {
+ container = foundElement as HTMLElement
+ }
} else {
container = this._config.container
}
- return container;
+ return container
+ }
+
+ protected abstract prepare(almaClient: Client): Promise
+ protected abstract render(
+ renderingContext: unknown,
+ createWidget: WidgetFactoryFunc
+ ): Promise
+
+ mount(dom: DOMContent): void {
+ setDOMContent(this.container, dom)
}
async refresh(): Promise {
- const renderingContext = await this.prepare(this._almaClient);
- const nestedWidgets = new WidgetsController(this._almaClient);
+ const renderingContext = await this.prepare(this.almaClient)
+ const nestedWidgets = new WidgetsController(this.almaClient)
- let dom: DOMContent;
- let createWidget = nestedWidgets.create.bind(nestedWidgets);
- if (typeof this._config.render === "function") {
- dom = await this._config.render(renderingContext, createWidget);
+ let dom: DOMContent
+ const createWidget = nestedWidgets.create.bind(nestedWidgets)
+ if (typeof this._config.render === 'function') {
+ dom = await this._config.render(renderingContext, createWidget)
} else {
- dom = await this.render(renderingContext, createWidget);
+ dom = await this.render(renderingContext, createWidget)
}
- this.mount(dom);
+ this.mount(dom)
// Render any nested widget that might have been added by the rendering of the widget
- await nestedWidgets.render();
+ await nestedWidgets.render()
}
-
- mount(dom: DOMContent) {
- setDOMContent(this.container, dom);
- }
-
-
- protected abstract async prepare(almaClient: Client): Promise;
- protected abstract async render(renderingContext: any, createWidget: WidgetFactoryFunc): Promise;
}
diff --git a/src/widgets/config.ts b/src/widgets/config.ts
new file mode 100644
index 0000000..f705ed6
--- /dev/null
+++ b/src/widgets/config.ts
@@ -0,0 +1,89 @@
+import isPlainObject from 'lodash.isplainobject'
+import { MarkOptional } from 'ts-essentials'
+
+import { DOMContent, IObject, Preserve, PreservedDeepRequired, ResolvePreserve } from '@/types'
+import { RenderingFunc } from '@/widgets/types'
+
+/*
+ * *Settings: interfaces for lib consumers
+ */
+export interface BaseTemplateSettings {
+ [K: string]: ((...args: never[]) => DOMContent) | undefined
+}
+
+export interface BaseClassesSettings {
+ root?: string
+}
+
+// Widget Settings are composed of top-level settings + optional template settings & classes settings
+export interface BaseWidgetSettings<
+ Tpl extends BaseTemplateSettings,
+ Cls extends BaseClassesSettings
+> {
+ container: Preserve
+ render?: RenderingFunc
+
+ templates?: Tpl
+ classes?: Cls
+}
+
+export type WidgetSettings = BaseWidgetSettings
+
+export type SettingsLiteral = ResolvePreserve
+
+/*
+ * *Config: "complete" representations of settings for internal use
+ */
+
+// A Widget config is derived from its Settings type, making all keys
+// required except `render` ()
+export type WidgetConfig = T extends BaseWidgetSettings
+ ? MarkOptional, 'render'>
+ : never
+
+// A "default widget config" represents the shape of the default values object used to initialize a
+// config. It's the Config type itself, with 'container' marked optional as it does not make a lot of
+// sense to have a default value for this property.
+export type DefaultWidgetConfig = T extends BaseWidgetSettings
+ ? MarkOptional, 'render' | 'container'>
+ : never
+
+/**
+ * Merges a default config object with a settings object, to build a widget's internal config object
+ *
+ * @param defaults The default values to use as a base config
+ * @param settings The settings that should override the default values
+ *
+ * @return WidgetConfig the fully merged config object
+ */
+export function makeConfig(
+ defaults: DefaultWidgetConfig,
+ settings: SettingsLiteral
+): WidgetConfig {
+ const result: WidgetConfig = {} as WidgetConfig
+ const sources = [defaults, settings]
+
+ function mergeObjects(base: IObject, source: IObject): IObject {
+ for (const key of Object.keys(source)) {
+ if (Object.prototype.hasOwnProperty.call(source, key)) {
+ if (isPlainObject(source[key])) {
+ if (!base[key] || !isPlainObject(base[key])) {
+ base[key] = {}
+ }
+ mergeObjects(base[key] as IObject, source[key] as IObject)
+ } else {
+ base[key] = source[key]
+ }
+ }
+ }
+
+ return base
+ }
+
+ for (let idx = 0; idx < 2; idx++) {
+ const src = sources[idx]
+ mergeObjects(result, src as IObject)
+ }
+
+ return result
+}
diff --git a/src/widgets/how_it_works/default_templates.ts b/src/widgets/how_it_works/default_templates.ts
index 5c67d67..d460eb8 100644
--- a/src/widgets/how_it_works/default_templates.ts
+++ b/src/widgets/how_it_works/default_templates.ts
@@ -1,5 +1,5 @@
-import {HowItWorksTemplates, HowItWorksWidgetClasses} from "./types";
-import {DOMContent} from "../../types";
+import { HowItWorksTemplatesConfig, HowItWorksClassesConfig } from './types'
+import { DOMContent } from '@/types'
import {
createListElement,
createRootElement,
@@ -7,166 +7,182 @@ import {
humanizedDate,
imageWithSrc,
priceFromCents,
- setDOMContent
-} from "../../utils";
+ setDOMContent,
+} from '../../utils'
-import {IPaymentPlan} from "alma-js-client/dist/types/entities/eligibility";
+import { IPaymentPlan } from '@alma/client/dist/types/entities/eligibility'
-import almaLogo from '../../assets/alma.svg';
-import infoLogo from '../../assets/info.svg';
-import cbLogo from '../../assets/cards/cb.svg';
-import visaLogo from '../../assets/cards/visa.svg';
-import mastercardLogo from '../../assets/cards/mastercard.svg';
-import amexLogo from '../../assets/cards/amex.svg';
+import almaLogo from '../../assets/alma.svg'
+import infoLogo from '../../assets/info.svg'
+import cbLogo from '../../assets/cards/cb.svg'
+import visaLogo from '../../assets/cards/visa.svg'
+import mastercardLogo from '../../assets/cards/mastercard.svg'
+import amexLogo from '../../assets/cards/amex.svg'
function logo(): DOMContent {
- return imageWithSrc(almaLogo);
+ return imageWithSrc(almaLogo)
}
-function cta(openModal: EventHandlerNonNull, classes: HowItWorksWidgetClasses): DOMContent {
- let cta = createRootElement(classes.cta);
- cta.appendChild(document.createTextNode("Comment ça marche ?"));
- cta.appendChild(imageWithSrc(infoLogo));
- cta.onclick = openModal;
- return cta;
+function cta(openModal: EventHandlerNonNull, classes: HowItWorksClassesConfig): DOMContent {
+ const cta = createRootElement(classes.cta)
+ cta.appendChild(document.createTextNode('Comment ça marche ?'))
+ cta.appendChild(imageWithSrc(infoLogo))
+ cta.onclick = openModal
+ return cta
}
-function modal(content: DOMContent, closeModal: EventHandlerNonNull, classes: HowItWorksWidgetClasses): DOMContent {
- let wrapper = createRootElement(classes.modal.wrapper);
+function modal(
+ content: DOMContent,
+ closeModal: EventHandlerNonNull,
+ classes: HowItWorksClassesConfig
+): DOMContent {
+ const wrapper = createRootElement(classes.modal.wrapper)
- let frame = createRootElement(classes.modal.frame);
- setDOMContent(frame, content);
+ const frame = createRootElement(classes.modal.frame)
+ setDOMContent(frame, content)
- let closeButton = document.createElement("button");
- closeButton.innerHTML = "×";
- closeButton.className = classes.modal.closeButton;
- closeButton.onclick = closeModal;
- frame.appendChild(closeButton);
+ const closeButton = document.createElement('button')
+ closeButton.innerHTML = '×'
+ closeButton.className = classes.modal.closeButton
+ closeButton.onclick = closeModal
+ frame.appendChild(closeButton)
- wrapper.appendChild(frame);
+ wrapper.appendChild(frame)
wrapper.onclick = (e) => {
if (e.target !== wrapper) {
- e.preventDefault();
- return false;
+ e.preventDefault()
+ return false
}
- return closeModal(e);
- };
+ return closeModal(e)
+ }
- return wrapper;
+ return wrapper
}
-function modalContent(paymentPlans: IPaymentPlan[], closeModal: EventHandlerNonNull, classes: HowItWorksWidgetClasses): DOMContent {
- let contentRoot = createRootElement(classes.modal.content.wrapper);
+function modalContent(
+ paymentPlans: IPaymentPlan[],
+ closeModal: EventHandlerNonNull,
+ classes: HowItWorksClassesConfig
+): DOMContent {
+ const contentRoot = createRootElement(classes.modal.content.wrapper)
- let logoRoot = createRootElement(classes.modal.content.logoContainer);
- setDOMContent(logoRoot, imageWithSrc(almaLogo));
- contentRoot.appendChild(logoRoot);
+ const logoRoot = createRootElement(classes.modal.content.logoContainer)
+ setDOMContent(logoRoot, imageWithSrc(almaLogo))
+ contentRoot.appendChild(logoRoot)
- let intro = createRootElement("", "p");
- intro.innerText = "Payer avec Alma, c'est simple et immédiat :";
- contentRoot.appendChild(intro);
+ const intro = createRootElement('', 'p')
+ intro.innerText = "Payer avec Alma, c'est simple et immédiat :"
+ contentRoot.appendChild(intro)
- let steps = createListElement([
- "Validez votre panier",
+ const steps = createListElement([
+ 'Validez votre panier',
"Sélectionnez l'option de paiement en plusieurs fois Alma",
- "Entrez votre numéro de carte bancaire, et c'est tout !"
- ]);
- contentRoot.appendChild(steps);
+ "Entrez votre numéro de carte bancaire, et c'est tout !",
+ ])
+ contentRoot.appendChild(steps)
- let paymentPlansWrapper = createRootElement(classes.modal.content.paymentPlansWrapper);
+ const paymentPlansWrapper = createRootElement(classes.modal.content.paymentPlansWrapper)
- let paymentPlansButtons: DOMContent[] = [];
- let planDetails: HTMLElement[][] = [];
+ const paymentPlansButtons: DOMContent[] = []
+ const planDetails: HTMLElement[][] = []
- paymentPlans = paymentPlans.sort((a, b) => a.length - b.length);
- paymentPlans.forEach(p => {
- let installmentsCount = p.length;
- let totalAmount = formatCents(p.reduce((amount, i) => amount + i.purchase_amount, 0));
- let totalFees = priceFromCents(p.reduce((amount, i) => amount + i.customer_fee, 0));
+ paymentPlans = paymentPlans.sort((a, b) => a.length - b.length)
+ paymentPlans.forEach((p) => {
+ const installmentsCount = p.length
+ const totalAmount = formatCents(p.reduce((amount, i) => amount + i.purchase_amount, 0))
+ const totalFees = priceFromCents(p.reduce((amount, i) => amount + i.customer_fee, 0))
- let countClass = classes.modal.content.paymentPlanButton.installmentsCount;
- let planButton = `${totalAmount} € en ${installmentsCount}×`;
- paymentPlansButtons.push(planButton);
+ const countClass = classes.modal.content.paymentPlanButton.installmentsCount
+ const planButton = `${totalAmount} € en ${installmentsCount}×`
+ paymentPlansButtons.push(planButton)
- let details: HTMLElement[] = [];
+ const details: HTMLElement[] = []
- let cardLogos = createRootElement(classes.modal.cardLogos.wrapper);
- cardLogos.appendChild(imageWithSrc(cbLogo, classes.modal.cardLogos.logo));
- cardLogos.appendChild(imageWithSrc(visaLogo, classes.modal.cardLogos.logo));
- cardLogos.appendChild(imageWithSrc(mastercardLogo, classes.modal.cardLogos.logo));
- cardLogos.appendChild(imageWithSrc(amexLogo, classes.modal.cardLogos.logo));
- details.push(cardLogos);
+ const cardLogos = createRootElement(classes.modal.cardLogos.wrapper)
+ cardLogos.appendChild(imageWithSrc(cbLogo, classes.modal.cardLogos.logo))
+ cardLogos.appendChild(imageWithSrc(visaLogo, classes.modal.cardLogos.logo))
+ cardLogos.appendChild(imageWithSrc(mastercardLogo, classes.modal.cardLogos.logo))
+ cardLogos.appendChild(imageWithSrc(amexLogo, classes.modal.cardLogos.logo))
+ details.push(cardLogos)
- let ccPayment = createRootElement(classes.modal.content.creditCardPayment, "p");
- let ccPaymentHtml = "Paiement ";
+ const ccPayment = createRootElement(classes.modal.content.creditCardPayment, 'p')
+ let ccPaymentHtml = 'Paiement '
if (totalFees === 0) {
- ccPaymentHtml += "sans frais, par carte bancaire";
+ ccPaymentHtml += 'sans frais, par carte bancaire'
} else {
- ccPaymentHtml += "par carte bancaire";
+ ccPaymentHtml += 'par carte bancaire'
}
- ccPaymentHtml += `, en ${installmentsCount} échéances :`;
- ccPayment.innerHTML = ccPaymentHtml;
- details.push(ccPayment);
-
- details.push(createListElement(p.map(i => {
- let amount = formatCents(i.purchase_amount + i.customer_fee);
- let fees = i.customer_fee === 0 ? "" : `(dont frais : ${formatCents(i.customer_fee)} €)`;
- let date = humanizedDate(new Date(i.due_date * 1000), true);
-
- let installment = `${amount} €`;
- installment += ` ${date} `;
- installment += ` ${fees} `;
-
- return installment;
- })));
-
- planDetails.push(details);
- });
-
- let paymentPlansButtonsList = createListElement(
+ ccPaymentHtml += `, en ${installmentsCount} échéances :`
+ ccPayment.innerHTML = ccPaymentHtml
+ details.push(ccPayment)
+
+ details.push(
+ createListElement(
+ p.map((i) => {
+ const amount = formatCents(i.purchase_amount + i.customer_fee)
+ const fees =
+ i.customer_fee === 0 ? '' : `(dont frais : ${formatCents(i.customer_fee)} €)`
+ const date = humanizedDate(new Date(i.due_date * 1000), true)
+
+ let installment = `${amount} €`
+ installment += ` ${date} `
+ installment += ` ${fees} `
+
+ return installment
+ })
+ )
+ )
+
+ planDetails.push(details)
+ })
+
+ const paymentPlansButtonsList = createListElement(
paymentPlansButtons,
classes.modal.content.paymentPlansButtons,
- classes.modal.content.paymentPlanButton.button,
- );
- paymentPlansWrapper.appendChild(paymentPlansButtonsList);
+ classes.modal.content.paymentPlanButton.button
+ )
+ paymentPlansWrapper.appendChild(paymentPlansButtonsList)
- let planDetailsWrapper = createRootElement(classes.modal.content.paymentPlanDetailsWrapper);
- paymentPlansWrapper.appendChild(planDetailsWrapper);
+ const planDetailsWrapper = createRootElement(classes.modal.content.paymentPlanDetailsWrapper)
+ paymentPlansWrapper.appendChild(planDetailsWrapper)
// Make first sample payment plan selected & visible
- setDOMContent(planDetailsWrapper, planDetails[0]);
- paymentPlansButtonsList.querySelector(`.${classes.modal.content.paymentPlanButton.button}`)!.classList.add(classes.modal.content.paymentPlanButton.selected);
+ setDOMContent(planDetailsWrapper, planDetails[0])
+
+ // TODO: Remove non-null assertion / handle null case
+ paymentPlansButtonsList
+ .querySelector(`.${classes.modal.content.paymentPlanButton.button}`)!
+ .classList.add(classes.modal.content.paymentPlanButton.selected)
paymentPlansButtonsList.onclick = function (e) {
- paymentPlansButtonsList.querySelector(`.${classes.modal.content.paymentPlanButton.selected}`)?.classList.remove(classes.modal.content.paymentPlanButton.selected);
- let btn = (e.target as Element)?.closest(`.${classes.modal.content.paymentPlanButton.button}`);
+ paymentPlansButtonsList
+ .querySelector(`.${classes.modal.content.paymentPlanButton.selected}`)
+ ?.classList.remove(classes.modal.content.paymentPlanButton.selected)
+ const btn = (e.target as Element)?.closest(`.${classes.modal.content.paymentPlanButton.button}`)
if (btn) {
- let index = Array.from(paymentPlansButtonsList.children).indexOf(btn);
- setDOMContent(planDetailsWrapper, planDetails[index]);
- btn.classList.add(classes.modal.content.paymentPlanButton.selected);
+ const index = Array.from(paymentPlansButtonsList.children).indexOf(btn)
+ setDOMContent(planDetailsWrapper, planDetails[index])
+ btn.classList.add(classes.modal.content.paymentPlanButton.selected)
}
- };
+ }
- contentRoot.appendChild(paymentPlansWrapper);
+ contentRoot.appendChild(paymentPlansWrapper)
- let footer = createRootElement(classes.modal.content.footer.wrapper);
- let closeBtn = document.createElement("button");
- closeBtn.className = classes.modal.content.footer.closeButton;
- closeBtn.innerText = "J'ai compris !";
- closeBtn.onclick = closeModal;
- footer.appendChild(closeBtn);
- contentRoot.appendChild(footer);
+ const footer = createRootElement(classes.modal.content.footer.wrapper)
+ const closeBtn = document.createElement('button')
+ closeBtn.className = classes.modal.content.footer.closeButton
+ closeBtn.innerText = "J'ai compris !"
+ closeBtn.onclick = closeModal
+ footer.appendChild(closeBtn)
+ contentRoot.appendChild(footer)
- return contentRoot;
+ return contentRoot
}
-
-export const templates: HowItWorksTemplates = {
+export const defaultTemplates: HowItWorksTemplatesConfig = {
logo,
cta,
modal,
modalContent,
-};
-
-export default templates;
+}
diff --git a/src/widgets/how_it_works/index.ts b/src/widgets/how_it_works/index.ts
index 8c2aa82..efdbc6d 100644
--- a/src/widgets/how_it_works/index.ts
+++ b/src/widgets/how_it_works/index.ts
@@ -1,143 +1,130 @@
-import './styles.scss';
-
-import {Widget} from "../base";
-import {DOMContent} from "../../types";
-import {Client} from "alma-js-client";
-import {setDOMContent, createRootElement} from "../../utils";
-import defaultTemplates from './default_templates';
-import {HowItWorksConfig, HowItWorksSettings, HowItWorksWidgetClasses} from "./types";
-import {
- IEligibility,
- IPaymentPlan
-} from "alma-js-client/dist/types/entities/eligibility";
-
-const defaultClasses: HowItWorksWidgetClasses = {
- root: "alma-how_it_works",
- logo: "alma-how_it_works--logo",
- cta: "alma-how_it_works--cta",
- modal: {
- root: 'alma-modal',
- wrapper: 'alma-modal--wrapper',
- frame: 'alma-modal--frame',
- closeButton: 'alma-modal--close-btn',
- cardLogos: {
- wrapper: 'alma-hiw_content--card-logos',
- logo: 'alma-hiw_content--card-logo',
- },
- content: {
- wrapper: 'alma-hiw_content--wrapper',
- logoContainer: 'alma-hiw_content--logo',
- paymentPlansWrapper: 'alma-hiw_content--plans',
- paymentPlansButtons: 'alma-hiw_content--plans-btns',
- paymentPlanButton: {
- button: 'alma-hiw_content--plan-btn',
- selected: 'alma-hiw_content--plan-btn__selected',
- installmentsCount: 'alma-hiw_content--plan-btn--installments_count',
- },
- paymentPlanDetailsWrapper: 'alma-hiw_content--plan-details',
- creditCardPayment: 'alma-hiw_content--cc-payment',
- installmentAmount: 'alma-hiw_content--installment-amount',
- installmentFees: 'alma-hiw_content--installment-fees',
- installmentDate: 'alma-hiw_content--installment-date',
- footer: {
- wrapper: 'alma-hiw_content--footer',
- closeButton: 'alma-hiw_content--close-btn',
- }
- }
- }
-};
+import './styles.scss'
-export class HowItWorksWidget extends Widget {
- private modalWrapper: HTMLElement | null;
+import { Widget } from '../base'
+import { DOMContent, ResolvePreserve } from '@/types'
+import { Client } from '@alma/client'
+import { setDOMContent, createRootElement } from '../../utils'
+import { defaultTemplates } from './default_templates'
+import { HowItWorksSettings } from './types'
+import { IEligibility, IPaymentPlan } from '@alma/client/dist/types/entities/eligibility'
+import { DefaultWidgetConfig } from '@/widgets/config'
- constructor(almaClient: Client, options: HowItWorksSettings) {
- // Inject default templates & classes into the given options
- options = {
- displayLogo: true,
- displayInfoIcon: true,
- ctaContent: "Comment ça marche ?",
- samplePlans: [],
- ...options,
- templates: {
- ...defaultTemplates,
- ...options.templates,
- },
- classes: {
- ...defaultClasses,
- ...options.classes,
- }
- };
+export class HowItWorksWidget extends Widget {
+ private readonly modalWrapper: HTMLElement
- super(almaClient, options);
+ constructor(almaClient: Client, settings: ResolvePreserve) {
+ super(almaClient, settings)
- this.modalWrapper = null;
+ this.modalWrapper = createRootElement(this.config.classes.modal.root)
+ this.modalWrapper.style.display = 'none'
}
-
- get config(): HowItWorksConfig {
- return {...this._config} as HowItWorksConfig;
+ defaultConfig(): DefaultWidgetConfig {
+ return {
+ displayLogo: true,
+ displayInfoIcon: true,
+ ctaContent: '',
+ samplePlans: [],
+ classes: {
+ root: 'alma-how_it_works',
+ logo: 'alma-how_it_works--logo',
+ cta: 'alma-how_it_works--cta',
+ modal: {
+ root: 'alma-modal',
+ wrapper: 'alma-modal--wrapper',
+ frame: 'alma-modal--frame',
+ closeButton: 'alma-modal--close-btn',
+ cardLogos: {
+ wrapper: 'alma-hiw_content--card-logos',
+ logo: 'alma-hiw_content--card-logo',
+ },
+ content: {
+ wrapper: 'alma-hiw_content--wrapper',
+ logoContainer: 'alma-hiw_content--logo',
+ paymentPlansWrapper: 'alma-hiw_content--plans',
+ paymentPlansButtons: 'alma-hiw_content--plans-btns',
+ paymentPlanButton: {
+ button: 'alma-hiw_content--plan-btn',
+ selected: 'alma-hiw_content--plan-btn__selected',
+ installmentsCount: 'alma-hiw_content--plan-btn--installments_count',
+ },
+ paymentPlanDetailsWrapper: 'alma-hiw_content--plan-details',
+ creditCardPayment: 'alma-hiw_content--cc-payment',
+ installmentAmount: 'alma-hiw_content--installment-amount',
+ installmentFees: 'alma-hiw_content--installment-fees',
+ installmentDate: 'alma-hiw_content--installment-date',
+ footer: {
+ wrapper: 'alma-hiw_content--footer',
+ closeButton: 'alma-hiw_content--close-btn',
+ },
+ },
+ },
+ },
+ templates: defaultTemplates,
+ }
}
- public openModal(e: Event) {
- e.preventDefault();
- this.modalWrapper!.style.display = "block";
- return false;
+ openModal(e: Event): boolean {
+ e.preventDefault()
+ this.modalWrapper.style.display = 'block'
+ return false
}
- public closeModal(e: Event) {
- e.preventDefault();
- this.modalWrapper!.style.display = "none";
- return false;
+ closeModal(e: Event): boolean {
+ e.preventDefault()
+ this.modalWrapper.style.display = 'none'
+ return false
}
protected async prepare(almaClient: Client): Promise {
if (this.config.samplePlans.length === 0) {
- let samplePlans = await almaClient.payments.eligibility(
- {
- payment: {
- purchase_amount: 30000,
- installments_count: [3, 4]
- }
- }
- ) as IEligibility[];
-
- return samplePlans.filter(p => p.eligible).map(p => p.payment_plan!);
+ const samplePlans = (await almaClient.payments.eligibility({
+ payment: {
+ purchase_amount: 30000,
+ installments_count: [3, 4],
+ },
+ })) as IEligibility[]
+
+ // TODO: Remove non-null assertion. Requires using type EligibleEligibility[] above, but can
+ // we actually guarantee that 300€ in 3 or 4 installments will always be eligible for any
+ // given merchant? It might be safer to just hardcode some samples!
+ return samplePlans.filter((p) => p.eligible).map((p) => p.payment_plan!)
} else {
- return this.config.samplePlans;
+ return this.config.samplePlans
}
}
protected async render(paymentPlans: IPaymentPlan[]): Promise {
- let root = document.createElement("div");
- root.className = this.config.classes.root;
+ const root = document.createElement('div')
+ root.className = this.config.classes.root
if (this.config.displayLogo) {
- let logoRoot = createRootElement(this.config.classes.logo);
- setDOMContent(logoRoot, this.config.templates.logo(this.config.classes));
- root.appendChild(logoRoot);
+ const logoRoot = createRootElement(this.config.classes.logo)
+ setDOMContent(logoRoot, this.config.templates.logo(this.config.classes))
+ root.appendChild(logoRoot)
}
- let modalLinkRoot = createRootElement();
+ const modalLinkRoot = createRootElement()
setDOMContent(
modalLinkRoot,
- this.config.templates.cta(
- this.openModal.bind(this),
- this.config.classes
- )
- );
- root.appendChild(modalLinkRoot);
+ this.config.templates.cta(this.openModal.bind(this), this.config.classes)
+ )
+ root.appendChild(modalLinkRoot)
- let closeModal = this.closeModal.bind(this);
- let content = this.config.templates.modalContent(paymentPlans, closeModal, this.config.classes);
+ const closeModal = this.closeModal.bind(this)
+ const content = this.config.templates.modalContent(
+ paymentPlans,
+ closeModal,
+ this.config.classes
+ )
- this.modalWrapper = createRootElement(this.config.classes.modal.root);
- this.modalWrapper.style.display = "none";
- setDOMContent(this.modalWrapper, this.config.templates.modal(content, closeModal, this.config.classes));
+ setDOMContent(
+ this.modalWrapper,
+ this.config.templates.modal(content, closeModal, this.config.classes)
+ )
- document.body.appendChild(this.modalWrapper);
+ document.body.appendChild(this.modalWrapper)
- return root;
+ return root
}
}
-
-export default HowItWorksWidget;
diff --git a/src/widgets/how_it_works/styles.scss b/src/widgets/how_it_works/styles.scss
index 3f702c4..9f120fe 100644
--- a/src/widgets/how_it_works/styles.scss
+++ b/src/widgets/how_it_works/styles.scss
@@ -22,7 +22,6 @@
}
}
-
.alma-modal--wrapper {
position: fixed;
@@ -63,7 +62,6 @@
}
}
-
.alma-modal--close-btn {
position: absolute;
right: 10px;
@@ -129,23 +127,24 @@
display: inline-block;
padding: 10px;
margin: 0 10px;
- border: 2px solid #DADFFB;
+ border: 2px solid #dadffb;
border-radius: 3px;
cursor: pointer;
color: #3651d2;
transition: all 300ms ease-in-out;
- &:hover, &.alma-hiw_content--plan-btn__selected {
+ &:hover,
+ &.alma-hiw_content--plan-btn__selected {
border-color: #3651d2;
- background: #F0F8FF;
+ background: #f0f8ff;
}
.alma-hiw_content--plan-btn--installments_count {
position: relative;
border-bottom: 3px solid #3651d2;
- font-family: "Helvetica Neue", Helvetica, Arial, "Segoe UI", sans-serif;
+ font-family: 'Helvetica Neue', Helvetica, Arial, 'Segoe UI', sans-serif;
font-weight: bold;
}
}
@@ -176,7 +175,8 @@
font-size: 0.8em;
transition: all 300ms ease-in-out;
- &:hover, &:focus {
+ &:hover,
+ &:focus {
background-color: #3651d2;
color: white;
outline: none;
diff --git a/src/widgets/how_it_works/types.ts b/src/widgets/how_it_works/types.ts
index 2277b25..e5d1a32 100644
--- a/src/widgets/how_it_works/types.ts
+++ b/src/widgets/how_it_works/types.ts
@@ -1,62 +1,65 @@
-import {DeepRequired, DOMContent} from "../../types";
-import {WidgetSettings} from "../base";
-import {IPaymentPlan} from "alma-js-client/dist/types/entities/eligibility";
+import { DOMContent } from '@/types'
+import { BaseClassesSettings, BaseTemplateSettings, BaseWidgetSettings } from '../config'
+import { IPaymentPlan } from '@alma/client/dist/types/entities/eligibility'
+import { DeepRequired } from 'ts-essentials'
-export type HowItWorksWidgetClassesOption = {
- root?: string;
- logo?: string;
- cta?: string;
+interface HIWClasses extends BaseClassesSettings {
+ logo?: string
+ cta?: string
modal?: {
- root?: string;
- wrapper?: string;
- frame?: string;
- closeButton?: string;
+ root?: string
+ wrapper?: string
+ frame?: string
+ closeButton?: string
cardLogos?: {
- wrapper?: string;
- logo?: string;
+ wrapper?: string
+ logo?: string
}
content?: {
- wrapper?: string;
- logoContainer?: string;
- paymentPlansWrapper?: string;
- paymentPlansButtons?: string;
+ wrapper?: string
+ logoContainer?: string
+ paymentPlansWrapper?: string
+ paymentPlansButtons?: string
paymentPlanButton?: {
- button?: string;
- selected?: string;
- installmentsCount?: string;
- };
- paymentPlanDetailsWrapper?: string;
- creditCardPayment?: string;
- installmentAmount?: string;
- installmentFees?: string;
- installmentDate?: string;
+ button?: string
+ selected?: string
+ installmentsCount?: string
+ }
+ paymentPlanDetailsWrapper?: string
+ creditCardPayment?: string
+ installmentAmount?: string
+ installmentFees?: string
+ installmentDate?: string
footer?: {
- wrapper?: string;
- closeButton?: string;
- };
+ wrapper?: string
+ closeButton?: string
+ }
}
}
}
-export type HowItWorksWidgetClasses = DeepRequired;
+export type HowItWorksClassesConfig = DeepRequired
-export type HowItWorksTemplatesOption = {
- logo?: (classes: HowItWorksWidgetClasses) => DOMContent;
- cta?: (openModal: EventHandlerNonNull, classes: HowItWorksWidgetClasses) => DOMContent;
- modal?: (content: DOMContent, closeModal: EventHandlerNonNull, classes: HowItWorksWidgetClasses) => DOMContent;
- modalContent?: (paymentPlans: IPaymentPlan[], closeModal: EventHandlerNonNull, classes: HowItWorksWidgetClasses) => DOMContent;
+interface HIWTemplates extends BaseTemplateSettings {
+ logo?: (classes: HowItWorksClassesConfig) => DOMContent
+ cta?: (openModal: EventHandlerNonNull, classes: HowItWorksClassesConfig) => DOMContent
+ modal?: (
+ content: DOMContent,
+ closeModal: EventHandlerNonNull,
+ classes: HowItWorksClassesConfig
+ ) => DOMContent
+ modalContent?: (
+ paymentPlans: IPaymentPlan[],
+ closeModal: EventHandlerNonNull,
+ classes: HowItWorksClassesConfig
+ ) => DOMContent
}
-export type HowItWorksTemplates = DeepRequired;
+export type HowItWorksTemplatesConfig = DeepRequired
-type HowItWorksOptions = {
- templates?: HowItWorksTemplatesOption,
- classes?: HowItWorksWidgetClassesOption;
- displayLogo?: boolean;
- displayInfoIcon?: boolean;
- ctaContent?: DOMContent;
- samplePlans?: IPaymentPlan[];
+export interface HowItWorksSettings extends BaseWidgetSettings {
+ displayLogo?: boolean
+ displayInfoIcon?: boolean
+ ctaContent?: DOMContent
+ samplePlans?: IPaymentPlan[]
}
-
-export type HowItWorksSettings = HowItWorksOptions & WidgetSettings;
-export type HowItWorksConfig = DeepRequired & WidgetSettings;
diff --git a/src/widgets/index.ts b/src/widgets/index.ts
index 3d4609d..bacaf00 100644
--- a/src/widgets/index.ts
+++ b/src/widgets/index.ts
@@ -1,2 +1,2 @@
-export {PaymentPlanWidget as PaymentPlan} from './payment_plan';
-export {HowItWorksWidget as HowItWorks} from './how_it_works';
+export { PaymentPlanWidget as PaymentPlan } from './payment_plan'
+export { HowItWorksWidget as HowItWorks } from './how_it_works'
diff --git a/src/widgets/payment_plan/default_templates.ts b/src/widgets/payment_plan/default_templates.ts
index e21e7ee..7e78e6a 100644
--- a/src/widgets/payment_plan/default_templates.ts
+++ b/src/widgets/payment_plan/default_templates.ts
@@ -1,111 +1,131 @@
-import {DOMContent, integer} from "../../types";
-import {joinInstallmentsCounts, createRootElement, formatCents, imageWithSrc} from "../../utils";
-import {EligibleEligibility} from "alma-js-client/dist/types/entities/eligibility";
-import {PaymentPlanConfig, PaymentPlanTemplatesOption, PaymentPlanWidgetClasses} from "./types";
-import {WidgetFactoryFunc} from "../types";
-import HowItWorksWidget from "../how_it_works";
-import {HowItWorksSettings, HowItWorksWidgetClasses} from "../how_it_works/types";
-import infoLogo from "../../assets/info.svg";
-
-function howItWorksCtaTemplate(title: HTMLElement, classes: PaymentPlanWidgetClasses) {
- return ((openModal: EventHandlerNonNull, nestedClasses: HowItWorksWidgetClasses): DOMContent => {
- let cta = createRootElement(nestedClasses.cta);
- cta.appendChild(title);
-
- let infoLogoImg = imageWithSrc(infoLogo);
- infoLogoImg.className = classes.infoButton;
- cta.appendChild(infoLogoImg);
-
- cta.onclick = openModal;
- return cta;
- });
+import { DOMContent, integer, ResolvePreserve } from '@/types'
+import { joinInstallmentsCounts, createRootElement, formatCents, imageWithSrc } from '../../utils'
+import { EligibleEligibility, IInstallment } from '@alma/client/dist/types/entities/eligibility'
+import { PaymentPlanClassesConfig, PaymentPlanSettings } from './types'
+import { WidgetFactoryFunc } from '../types'
+import { HowItWorksWidget } from '../how_it_works'
+import { HowItWorksSettings, HowItWorksClassesConfig } from '../how_it_works/types'
+import infoLogo from '../../assets/info.svg'
+import { WidgetConfig } from '@/widgets/config'
+
+function howItWorksCtaTemplate(title: HTMLElement, classes: PaymentPlanClassesConfig) {
+ return (openModal: EventHandlerNonNull, nestedClasses: HowItWorksClassesConfig): DOMContent => {
+ const cta = createRootElement(nestedClasses.cta)
+ cta.appendChild(title)
+
+ const infoLogoImg = imageWithSrc(infoLogo)
+ infoLogoImg.className = classes.infoButton
+ cta.appendChild(infoLogoImg)
+
+ cta.onclick = openModal
+ return cta
+ }
}
-function titleTemplate(eligiblePlans: EligibleEligibility[], config: PaymentPlanConfig, createWidget: WidgetFactoryFunc): HTMLElement {
- let titleWrapper = createRootElement(config.classes.title);
+function titleTemplate(
+ eligiblePlans: EligibleEligibility[],
+ config: WidgetConfig,
+ createWidget: WidgetFactoryFunc
+): HTMLElement {
+ const titleWrapper = createRootElement(config.classes.title)
- let title = document.createElement("strong");
+ const title = document.createElement('strong')
const totalFees = eligiblePlans
- .map(p => p.payment_plan.reduce((fees, i) => fees + i.customer_fee, 0))
- .reduce((total, fees) => total + fees, 0);
-
- title.innerHTML = `Payez ${formatCents(config.purchaseAmount)} € en ` +
- `${joinInstallmentsCounts(eligiblePlans.map(p => p.installments_count))} fois` +
- (totalFees === 0 ? " sans frais" : "");
-
- createWidget(
- HowItWorksWidget,
- {
- container: titleWrapper,
- displayLogo: false,
- samplePlans: eligiblePlans.map(p => p.payment_plan),
- templates: {
- cta: howItWorksCtaTemplate(title, config.classes),
- }
- } as HowItWorksSettings
- );
-
- return titleWrapper;
+ .map((p) => p.payment_plan.reduce((fees, i) => fees + i.customer_fee, 0))
+ .reduce((total, fees) => total + fees, 0)
+
+ title.innerHTML =
+ `Payez ${formatCents(config.purchaseAmount)} € en ` +
+ `${joinInstallmentsCounts(eligiblePlans.map((p) => p.installments_count))} fois` +
+ (totalFees === 0 ? ' sans frais' : '')
+
+ createWidget(HowItWorksWidget, {
+ container: titleWrapper,
+ displayLogo: false,
+ samplePlans: eligiblePlans.map((p) => p.payment_plan),
+ templates: {
+ cta: howItWorksCtaTemplate(title, config.classes),
+ },
+ } as HowItWorksSettings)
+
+ return titleWrapper
}
-function _installmentTemplate(content: string, classes: PaymentPlanWidgetClasses): HTMLElement {
- let installment = document.createElement("span");
- installment.className = classes.paymentPlan.installmentAmount;
- installment.innerHTML = content;
+function _installmentTemplate(content: string, classes: PaymentPlanClassesConfig): HTMLElement {
+ const installment = document.createElement('span')
+ installment.className = classes.paymentPlan.installmentAmount
+ installment.innerHTML = content
- return installment;
+ return installment
}
-function paymentPlanTemplate(eligibility: EligibleEligibility, config: PaymentPlanConfig, createWidget: WidgetFactoryFunc): HTMLElement[] {
- let installmentsCountLabel = document.createElement("span");
- installmentsCountLabel.className = config.classes.paymentPlan.installmentsCount;
- installmentsCountLabel.innerHTML = `${eligibility.installments_count}×`;
-
- let installments = document.createElement("span");
- installments.className = config.classes.paymentPlan.installmentsWrapper;
-
- let installmentsData = [...eligibility.payment_plan];
- let equalInstallments = eligibility.payment_plan.every((p: any, idx: number, arr: Array) => {
- return p.purchase_amount + p.customer_fee === arr[0].purchase_amount + arr[0].customer_fee
- });
+function paymentPlanTemplate(
+ eligibility: EligibleEligibility,
+ config: WidgetConfig,
+ _: WidgetFactoryFunc
+): HTMLElement[] {
+ const installmentsCountLabel = document.createElement('span')
+ installmentsCountLabel.className = config.classes.paymentPlan.installmentsCount
+ installmentsCountLabel.innerHTML = `${eligibility.installments_count}×`
+
+ const installments = document.createElement('span')
+ installments.className = config.classes.paymentPlan.installmentsWrapper
+
+ const installmentsData = [...eligibility.payment_plan]
+ const equalInstallments = eligibility.payment_plan.every(
+ (p: IInstallment, idx: number, arr: IInstallment[]) => {
+ return p.purchase_amount + p.customer_fee === arr[0].purchase_amount + arr[0].customer_fee
+ }
+ )
if (!equalInstallments) {
- let amount = eligibility.payment_plan[0].purchase_amount + eligibility.payment_plan[0].customer_fee;
- installments.appendChild(_installmentTemplate(`${formatCents(amount)} €`, config.classes));
- installments.appendChild(document.createTextNode(" + "));
+ const amount =
+ eligibility.payment_plan[0].purchase_amount + eligibility.payment_plan[0].customer_fee
+ installments.appendChild(_installmentTemplate(`${formatCents(amount)} €`, config.classes))
+ installments.appendChild(document.createTextNode(' + '))
}
- let amount = installmentsData[1].purchase_amount + installmentsData[1].customer_fee;
- let installmentsCount = equalInstallments ? installmentsData.length : installmentsData.length - 1;
- installments.appendChild(_installmentTemplate(`${installmentsCount} × ${formatCents(amount)} €`, config.classes));
+ const amount = installmentsData[1].purchase_amount + installmentsData[1].customer_fee
+ const installmentsCount = equalInstallments
+ ? installmentsData.length
+ : installmentsData.length - 1
+ installments.appendChild(
+ _installmentTemplate(`${installmentsCount} × ${formatCents(amount)} €`, config.classes)
+ )
- return [installmentsCountLabel, installments];
+ return [installmentsCountLabel, installments]
}
-function notEligibleTemplate(min: number, max: number, installmentsCounts: integer[], config: PaymentPlanConfig, createWidget: WidgetFactoryFunc): HTMLElement {
- let titleWrapper = createRootElement(config.classes.title);
-
- let title = document.createElement("strong");
- title.innerHTML = `Payez en ${joinInstallmentsCounts(installmentsCounts)} fois entre ${formatCents(min)} € et ${formatCents(max)} €`;
-
- createWidget(
- HowItWorksWidget,
- {
- container: titleWrapper,
- displayLogo: false,
- templates: {
- cta: howItWorksCtaTemplate(title, config.classes)
- }
- } as HowItWorksSettings
- );
-
- return titleWrapper;
+function notEligibleTemplate(
+ min: number,
+ max: number,
+ installmentsCounts: integer[],
+ config: WidgetConfig,
+ createWidget: WidgetFactoryFunc
+): HTMLElement {
+ const titleWrapper = createRootElement(config.classes.title)
+
+ const title = document.createElement('strong')
+ title.innerHTML = `Payez en ${joinInstallmentsCounts(
+ installmentsCounts
+ )} fois entre ${formatCents(min)} € et ${formatCents(max)} €`
+
+ createWidget(HowItWorksWidget, {
+ container: titleWrapper,
+ displayLogo: false,
+ templates: {
+ cta: howItWorksCtaTemplate(title, config.classes),
+ },
+ // TODO: why does WidgetFactoryFunc "wrongly" resolves SettingsFor to WidgetSettings when
+ // widget is declared as `class Widget`, but correctly to
+ // HowItWorksSettings when it is declared as `class Widget`?
+ } as ResolvePreserve)
+
+ return titleWrapper
}
-const templates: PaymentPlanTemplatesOption = {
+export const defaultTemplates = {
title: titleTemplate,
paymentPlan: paymentPlanTemplate,
notEligible: notEligibleTemplate,
-};
-
-export default templates;
+}
diff --git a/src/widgets/payment_plan/index.ts b/src/widgets/payment_plan/index.ts
index de35241..8e8bf06 100644
--- a/src/widgets/payment_plan/index.ts
+++ b/src/widgets/payment_plan/index.ts
@@ -1,127 +1,136 @@
-import './styles.scss';
-
-import {EligibleEligibility} from "alma-js-client/dist/types/entities/eligibility";
-
-import {Widget} from "../base";
-import {DOMContent, integer} from "../../types";
-import {Client} from "alma-js-client";
-import {setDOMContent} from "../../utils";
-import defaultTemplates from './default_templates';
-import {PaymentPlanConfig, PaymentPlanSettings, PaymentPlanWidgetClasses} from "./types";
-import {WidgetFactoryFunc} from "../types";
-
-const defaultClasses: PaymentPlanWidgetClasses = {
- root: "alma-payment_plan",
- title: "alma-payment_plan--title",
- infoButton: "alma-payment_plan--info_btn",
- paymentPlan: {
- root: "alma-payment_plan--plan",
- installmentsCount: "alma-payment_plan--installments_count",
- installmentsWrapper: "alma-payment_plan--installments",
- installmentAmount: "alma-payment_plan--installment",
- },
- notEligible: "alma-payment_plan--not_eligible"
-};
-
-export class PaymentPlanWidget extends Widget {
- constructor(almaClient: Client, options: PaymentPlanSettings) {
- // Inject default templates into the given options
- options = {
- ...options,
- templates: {
- ...defaultTemplates,
- ...options.templates,
- },
+import './styles.scss'
+
+import { EligibleEligibility } from '@alma/client/dist/types/entities/eligibility'
+
+import { Widget } from '../base'
+import { DOMContent, integer } from '@/types'
+import { Client } from '@alma/client'
+import { setDOMContent } from '@/utils'
+import { defaultTemplates } from './default_templates'
+import { PaymentPlanSettings } from './types'
+import { WidgetFactoryFunc } from '../types'
+import { DefaultWidgetConfig } from '@/widgets/config'
+
+type PaymentPlanDefaultConfig = DefaultWidgetConfig
+
+export class PaymentPlanWidget extends Widget {
+ defaultConfig(): PaymentPlanDefaultConfig {
+ return {
+ purchaseAmount: 100,
+ installmentsCount: 3,
+ minPurchaseAmount: null,
+ maxPurchaseAmount: null,
+ templates: defaultTemplates,
classes: {
- ...defaultClasses,
- ...options.classes,
- }
- };
-
- super(almaClient, options);
- }
-
-
- get config(): PaymentPlanConfig {
- return {...this._config} as PaymentPlanConfig;
+ root: 'alma-payment_plan',
+ title: 'alma-payment_plan--title',
+ infoButton: 'alma-payment_plan--info_btn',
+ paymentPlan: {
+ root: 'alma-payment_plan--plan',
+ installmentsCount: 'alma-payment_plan--installments_count',
+ installmentsWrapper: 'alma-payment_plan--installments',
+ installmentAmount: 'alma-payment_plan--installment',
+ },
+ notEligible: 'alma-payment_plan--not_eligible',
+ },
+ }
}
-
get installmentsCounts(): integer[] {
- let installmentsCounts = this.config.installmentsCount;
+ let installmentsCounts = this.config.installmentsCount
- if (typeof this.config.installmentsCount === "number") {
- installmentsCounts = [this.config.installmentsCount];
+ if (typeof this.config.installmentsCount === 'number') {
+ installmentsCounts = [this.config.installmentsCount]
}
- return installmentsCounts as integer[];
+ return installmentsCounts as integer[]
}
protected async prepare(almaClient: Client): Promise {
- const options = this._config as PaymentPlanConfig;
- const installmentsCount = this.installmentsCounts;
+ const { purchaseAmount, minPurchaseAmount, maxPurchaseAmount } = this.config
if (
- options.purchaseAmount < options.minPurchaseAmount ||
- options.purchaseAmount > options.maxPurchaseAmount
+ (minPurchaseAmount && purchaseAmount < minPurchaseAmount) ||
+ (maxPurchaseAmount && purchaseAmount > maxPurchaseAmount)
) {
- return [{
- eligible: false,
- reasons: {
- purchase_amount: "invalid_value"
+ return [
+ {
+ eligible: false,
+ reasons: {
+ purchase_amount: 'invalid_value',
+ },
+ constraints: {
+ purchase_amount: {
+ minimum: minPurchaseAmount,
+ maximum: maxPurchaseAmount,
+ },
+ },
},
- constraints: {
- purchase_amount: {
- minimum: options.minPurchaseAmount,
- maximum: options.maxPurchaseAmount
- }
- }
- }];
+ ]
}
return almaClient.payments.eligibility({
payment: {
- purchase_amount: options.purchaseAmount,
- installments_count: installmentsCount
- }
- });
+ purchase_amount: purchaseAmount,
+ installments_count: this.installmentsCounts,
+ },
+ })
}
- protected async render(renderingContext: any, createWidget: WidgetFactoryFunc): Promise {
- let root = document.createElement("div");
- root.className = this.config.classes.root;
+ protected async render(
+ renderingContext: any,
+ createWidget: WidgetFactoryFunc
+ ): Promise {
+ const root = document.createElement('div')
+ root.className = this.config.classes.root
- let eligiblePlans: EligibleEligibility[] = [];
- let minEligible: integer = Number.MAX_VALUE;
- let maxEligible: integer = Number.MIN_VALUE;
+ const eligiblePlans: EligibleEligibility[] = []
+ let minEligible: integer = Number.MAX_VALUE
+ let maxEligible: integer = Number.MIN_VALUE
- for (let eligibility of renderingContext) {
+ for (const eligibility of renderingContext) {
if (eligibility.eligible) {
- eligiblePlans.push(eligibility);
- } else if (!eligibility.reasons.installments_count) {
- let min = Math.max(this.config.minPurchaseAmount || 0, eligibility.constraints.purchase_amount.minimum);
- let max = Math.min(this.config.maxPurchaseAmount || 0, eligibility.constraints.purchase_amount.maximum);
-
- minEligible = min < minEligible ? min : minEligible;
- maxEligible = max > maxEligible ? max : maxEligible;
+ eligiblePlans.push(eligibility)
+ } else {
+ if (eligibility.reasons.purchase_amount) {
+ const min = Math.max(
+ this.config.minPurchaseAmount || 0,
+ eligibility.constraints.purchase_amount.minimum
+ )
+ const max = Math.min(
+ this.config.maxPurchaseAmount || 0,
+ eligibility.constraints.purchase_amount.maximum
+ )
+
+ minEligible = min < minEligible ? min : minEligible
+ maxEligible = max > maxEligible ? max : maxEligible
+ } else if (eligibility.reasons.merchant) {
+ return ''
+ }
}
}
if (eligiblePlans.length > 0) {
- let titleRoot = document.createElement("div");
- titleRoot.className = this.config.classes.title;
- setDOMContent(titleRoot, this.config.templates.title(eligiblePlans, this.config, createWidget));
- setDOMContent(root, titleRoot);
-
- for (let eligibility of eligiblePlans) {
- let plan = document.createElement("div");
- plan.className = this.config.classes.paymentPlan.root;
- setDOMContent(plan, this.config.templates.paymentPlan(eligibility, this.config, createWidget));
+ const titleRoot = document.createElement('div')
+ titleRoot.className = this.config.classes.title
+ setDOMContent(
+ titleRoot,
+ this.config.templates.title(eligiblePlans, this.config, createWidget)
+ )
+ setDOMContent(root, titleRoot)
+
+ for (const eligibility of eligiblePlans) {
+ const plan = document.createElement('div')
+ plan.className = this.config.classes.paymentPlan.root
+ setDOMContent(
+ plan,
+ this.config.templates.paymentPlan(eligibility, this.config, createWidget)
+ )
- root.appendChild(plan);
+ root.appendChild(plan)
}
} else {
- let notEligibleRoot = document.createElement("div");
- notEligibleRoot.className = this.config.classes.notEligible;
+ const notEligibleRoot = document.createElement('div')
+ notEligibleRoot.className = this.config.classes.notEligible
setDOMContent(
notEligibleRoot,
this.config.templates.notEligible(
@@ -131,13 +140,11 @@ export class PaymentPlanWidget extends Widget {
this.config,
createWidget
)
- );
+ )
- setDOMContent(root, notEligibleRoot);
+ setDOMContent(root, notEligibleRoot)
}
- return root;
+ return root
}
}
-
-export default PaymentPlanWidget;
diff --git a/src/widgets/payment_plan/styles.scss b/src/widgets/payment_plan/styles.scss
index e516f5d..af93883 100644
--- a/src/widgets/payment_plan/styles.scss
+++ b/src/widgets/payment_plan/styles.scss
@@ -15,7 +15,7 @@
vertical-align: middle;
border-bottom: 3px solid #3651d2;
- font-family: "Helvetica Neue", Helvetica, Arial, "Segoe UI", sans-serif;
+ font-family: 'Helvetica Neue', Helvetica, Arial, 'Segoe UI', sans-serif;
font-weight: bold;
-webkit-font-smoothing: antialiased;
diff --git a/src/widgets/payment_plan/types.ts b/src/widgets/payment_plan/types.ts
index e01787c..f7e1178 100644
--- a/src/widgets/payment_plan/types.ts
+++ b/src/widgets/payment_plan/types.ts
@@ -1,37 +1,47 @@
-import {DeepRequired, DOMContent, integer} from "../../types";
-import {WidgetSettings} from "../base";
-import {EligibleEligibility} from "alma-js-client/dist/types/entities/eligibility";
-import {WidgetFactoryFunc} from "../types";
+import { DeepRequired, DOMContent, integer } from '@/types'
+import { BaseWidgetSettings, WidgetConfig } from '../config'
+import { EligibleEligibility } from '@alma/client/dist/types/entities/eligibility'
+import { WidgetFactoryFunc } from '../types'
+import { BaseClassesSettings, BaseTemplateSettings } from '@/widgets/config'
-export type PaymentPlanWidgetClassesOption = {
- root?: string;
- title?: string;
- infoButton?: string;
- paymentPlan?: {
- root?: string;
- installmentsCount?: string;
- installmentsWrapper?: string;
- installmentAmount?: string;
- };
- notEligible?: string;
+interface PaymentPlanTemplates extends BaseTemplateSettings {
+ title?: (
+ eligiblePlans: EligibleEligibility[],
+ config: WidgetConfig,
+ createWidget: WidgetFactoryFunc
+ ) => DOMContent
+ paymentPlan?: (
+ eligibility: EligibleEligibility,
+ config: WidgetConfig,
+ createWidget: WidgetFactoryFunc
+ ) => DOMContent
+ notEligible?: (
+ min: number,
+ max: number,
+ installmentsCounts: integer[],
+ config: WidgetConfig,
+ createWidget: WidgetFactoryFunc
+ ) => DOMContent
}
-export type PaymentPlanWidgetClasses = DeepRequired;
-
-export type PaymentPlanTemplatesOption = {
- title?: (eligiblePlans: EligibleEligibility[], config: PaymentPlanConfig, createWidget: WidgetFactoryFunc) => DOMContent;
- paymentPlan?: (eligibility: EligibleEligibility, config: PaymentPlanConfig, createWidget: WidgetFactoryFunc) => DOMContent;
- notEligible?: (min: number, max: number, installmentsCounts: integer[], config: PaymentPlanConfig, createWidget: WidgetFactoryFunc) => DOMContent;
+interface PaymentPlanClasses extends BaseClassesSettings {
+ title?: string
+ infoButton?: string
+ paymentPlan?: {
+ root?: string
+ installmentsCount?: string
+ installmentsWrapper?: string
+ installmentAmount?: string
+ }
+ notEligible?: string
}
-type PaymentPlanOptions = {
- purchaseAmount: integer;
- installmentsCount: integer | integer[];
- minPurchaseAmount?: integer;
- maxPurchaseAmount?: integer;
- templates?: PaymentPlanTemplatesOption,
- classes?: PaymentPlanWidgetClassesOption;
-}
+export type PaymentPlanClassesConfig = DeepRequired
-export type PaymentPlanSettings = PaymentPlanOptions & WidgetSettings;
-export type PaymentPlanConfig = DeepRequired & WidgetSettings;
+export interface PaymentPlanSettings
+ extends BaseWidgetSettings {
+ purchaseAmount: integer
+ installmentsCount: integer | integer[]
+ minPurchaseAmount?: integer | null
+ maxPurchaseAmount?: integer | null
+}
diff --git a/src/widgets/types.ts b/src/widgets/types.ts
index fe2fd70..d4f2b43 100644
--- a/src/widgets/types.ts
+++ b/src/widgets/types.ts
@@ -1,10 +1,10 @@
-import {Widget, WidgetConstructor, WidgetSettings} from "./base";
-import {DOMContent} from "../types";
+import { ConstructorFor, SettingsFor } from './base'
+import { DOMContent } from '@/types'
export interface WidgetFactoryFunc {
- (widgetCtor: WidgetConstructor, options: WidgetSettings): Widget;
+ (widgetCtor: ConstructorFor, settings: SettingsFor): T
}
export interface RenderingFunc {
- (renderingContext: any, createWidget: WidgetFactoryFunc): Promise;
+ (renderingContext: unknown, createWidget: WidgetFactoryFunc): Promise
}
diff --git a/src/widgets_controller.ts b/src/widgets_controller.ts
index 6b035a7..685453c 100644
--- a/src/widgets_controller.ts
+++ b/src/widgets_controller.ts
@@ -1,29 +1,25 @@
-import {ApiMode, Client} from "alma-js-client";
-import {Widget, WidgetConstructor, WidgetSettings} from "./widgets/base";
+import { Client } from '@alma/client'
+import { Widget, ConstructorFor, SettingsFor } from './widgets/base'
+import { WidgetSettings } from '@/widgets/config'
export class WidgetsController {
- private readonly almaClient: Client;
- private widgets: Widget[] = [];
+ private widgets: Widget[] = []
- constructor(almaClient: Client) {
- this.almaClient = almaClient;
- }
+ constructor(private readonly almaClient: Client) {}
- create(widgetCtor: WidgetConstructor, options: WidgetSettings): Widget {
- const widget = new widgetCtor(this.almaClient, options);
- this.widgets.push(widget);
- return widget;
+ create(widgetCtor: ConstructorFor, settings: SettingsFor): T {
+ const widget = new widgetCtor(this.almaClient, settings)
+ this.widgets.push(widget)
+ return widget
}
- async render() {
- let promises: Promise[] = [];
+ async render(): Promise {
+ const promises: Promise[] = []
- for (let widget of this.widgets) {
- promises.push(widget.refresh());
+ for (const widget of this.widgets) {
+ promises.push(widget.refresh())
}
- await Promise.all(promises);
+ await Promise.all(promises)
}
}
-
-export default WidgetsController;
diff --git a/test/alma-widgets.test.ts b/test/alma-widgets.test.ts
index 96bdb7d..09782f8 100644
--- a/test/alma-widgets.test.ts
+++ b/test/alma-widgets.test.ts
@@ -1,14 +1,29 @@
-import DummyClass from "../src/alma-widgets"
-
-/**
- * Dummy test
- */
-describe("Dummy test", () => {
- it("works if true is truthy", () => {
- expect(true).toBeTruthy()
+import { expectTypeOf } from 'expect-type'
+
+import { Widgets } from '../src'
+import { WidgetsController } from '@/widgets_controller'
+import { ApiMode } from '@alma/client'
+import { PaymentPlanWidget } from '../src/widgets/payment_plan'
+import { HowItWorksWidget } from '../src/widgets/how_it_works'
+
+describe('Widgets namespace', () => {
+ it('exports the initialize function', () => {
+ expect(Widgets.initialize).toBeDefined()
+
+ expectTypeOf(Widgets.initialize).toEqualTypeOf<
+ (merchantId: string, apiMode: ApiMode) => WidgetsController
+ >()
})
- it("DummyClass is instantiable", () => {
- expect(new DummyClass()).toBeInstanceOf(DummyClass)
+ it('exports the PaymentPlan widget', () => {
+ expect(Widgets.PaymentPlan).toBeDefined()
+
+ expectTypeOf(Widgets.PaymentPlan).toEqualTypeOf()
+ })
+
+ it('exports the HowItWorks widget', () => {
+ expect(Widgets.HowItWorks).toBeDefined()
+
+ expectTypeOf(Widgets.HowItWorks).toEqualTypeOf()
})
})
diff --git a/test/config.test.ts b/test/config.test.ts
new file mode 100644
index 0000000..31dd03e
--- /dev/null
+++ b/test/config.test.ts
@@ -0,0 +1,113 @@
+import { expectTypeOf } from 'expect-type'
+
+import {
+ BaseClassesSettings,
+ BaseTemplateSettings,
+ BaseWidgetSettings,
+ DefaultWidgetConfig,
+ makeConfig,
+ SettingsLiteral,
+ WidgetConfig,
+ WidgetSettings,
+} from '@/widgets/config'
+import { DOMContent } from '../src/types'
+import { RenderingFunc } from '../src/widgets/types'
+
+describe('makeConfig', () => {
+ it('has the correct signature', () => {
+ type expectedFuncSignature = (
+ defaults: DefaultWidgetConfig,
+ settings: SettingsLiteral
+ ) => WidgetConfig
+
+ expectTypeOf(makeConfig).toEqualTypeOf()
+ })
+
+ it('correctly merges a default config object with some related settings', () => {
+ interface TestTemplates extends BaseTemplateSettings {
+ testTemplate?: () => DOMContent
+ }
+
+ interface TestClasses extends BaseClassesSettings {
+ testClass?: string
+ }
+
+ interface TestSettings extends BaseWidgetSettings {
+ testSetting?: {
+ testNestedSetting?: {
+ anActualValue: boolean
+
+ testDoubleNestedSetting?: {
+ andAnOptionalOne?: string
+ }
+ }
+ }
+ }
+
+ const container = document.createElement('div')
+ const render: RenderingFunc = async (): Promise => 'Hello world'
+
+ const defaults: DefaultWidgetConfig = {
+ testSetting: {
+ testNestedSetting: {
+ anActualValue: true,
+ testDoubleNestedSetting: {
+ andAnOptionalOne: 'now that is deep',
+ },
+ },
+ },
+ classes: {
+ root: 'test--root',
+ testClass: 'test--testClass',
+ },
+ templates: {
+ testTemplate: () => 'Hello, World!',
+ },
+ }
+
+ const settings: SettingsLiteral = {
+ container,
+ render,
+ testSetting: {
+ testNestedSetting: {
+ anActualValue: false,
+ testDoubleNestedSetting: {},
+ },
+ },
+ classes: {
+ testClass: 'overridden-class',
+ },
+ }
+
+ const config = makeConfig(defaults, settings)
+ expectTypeOf(config).toEqualTypeOf>()
+
+ // Resulting config is a merge of default config & settings, with precedence to the latter
+ expect(config).toEqual({
+ ...defaults,
+ ...settings,
+
+ testSetting: {
+ testNestedSetting: {
+ ...defaults.testSetting.testNestedSetting,
+ ...settings.testSetting?.testNestedSetting,
+
+ testDoubleNestedSetting: {
+ ...defaults.testSetting.testNestedSetting.testDoubleNestedSetting,
+ ...settings.testSetting?.testNestedSetting?.testDoubleNestedSetting,
+ },
+ },
+ },
+
+ classes: {
+ ...defaults.classes,
+ ...settings.classes,
+ },
+
+ templates: {
+ ...defaults.templates,
+ ...settings.templates,
+ },
+ })
+ })
+})
diff --git a/tools/gh-pages-publish.ts b/tools/gh-pages-publish.ts
deleted file mode 100644
index 53869cd..0000000
--- a/tools/gh-pages-publish.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-const { cd, exec, echo, touch } = require("shelljs")
-const { readFileSync } = require("fs")
-const url = require("url")
-
-let repoUrl
-let pkg = JSON.parse(readFileSync("package.json") as any)
-if (typeof pkg.repository === "object") {
- if (!pkg.repository.hasOwnProperty("url")) {
- throw new Error("URL does not exist in repository section")
- }
- repoUrl = pkg.repository.url
-} else {
- repoUrl = pkg.repository
-}
-
-let parsedUrl = url.parse(repoUrl)
-let repository = (parsedUrl.host || "") + (parsedUrl.path || "")
-let ghToken = process.env.GH_TOKEN
-
-echo("Deploying docs!!!")
-cd("docs")
-touch(".nojekyll")
-exec("git init")
-exec("git add .")
-exec('git config user.name "Olivier Lance"')
-exec('git config user.email "olivier.lance@getalma.eu"')
-exec('git commit -m "docs(docs): update gh-pages"')
-exec(
- `git push --force --quiet "https://${ghToken}@${repository}" master:gh-pages`
-)
-echo("Docs deployed!!")
diff --git a/tools/semantic-release-prepare.ts b/tools/semantic-release-prepare.ts
deleted file mode 100644
index db14dd7..0000000
--- a/tools/semantic-release-prepare.ts
+++ /dev/null
@@ -1,54 +0,0 @@
-const path = require("path")
-const { fork } = require("child_process")
-const colors = require("colors")
-
-const { readFileSync, writeFileSync } = require("fs")
-const pkg = JSON.parse(
- readFileSync(path.resolve(__dirname, "..", "package.json"))
-)
-
-pkg.scripts.prepush = "npm run test:prod && npm run build"
-pkg.scripts.commitmsg = "commitlint -E HUSKY_GIT_PARAMS"
-
-writeFileSync(
- path.resolve(__dirname, "..", "package.json"),
- JSON.stringify(pkg, null, 2)
-)
-
-// Call husky to set up the hooks
-fork(path.resolve(__dirname, "..", "node_modules", "husky", "lib", "installer", 'bin'), ['install'])
-
-console.log()
-console.log(colors.green("Done!!"))
-console.log()
-
-if (pkg.repository.url.trim()) {
- console.log(colors.cyan("Now run:"))
- console.log(colors.cyan(" npm install -g semantic-release-cli"))
- console.log(colors.cyan(" semantic-release-cli setup"))
- console.log()
- console.log(
- colors.cyan('Important! Answer NO to "Generate travis.yml" question')
- )
- console.log()
- console.log(
- colors.gray(
- 'Note: Make sure "repository.url" in your package.json is correct before'
- )
- )
-} else {
- console.log(
- colors.red(
- 'First you need to set the "repository.url" property in package.json'
- )
- )
- console.log(colors.cyan("Then run:"))
- console.log(colors.cyan(" npm install -g semantic-release-cli"))
- console.log(colors.cyan(" semantic-release-cli setup"))
- console.log()
- console.log(
- colors.cyan('Important! Answer NO to "Generate travis.yml" question')
- )
-}
-
-console.log()
diff --git a/tsconfig.json b/tsconfig.json
index 215a7f1..10f7291 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,22 +1,23 @@
{
"compilerOptions": {
"moduleResolution": "node",
- "target": "es5",
- "module":"es2015",
+ "target": "ES2019",
+ "module": "commonjs",
"lib": ["es2015", "es2016", "es2017", "dom"],
"strict": true,
"sourceMap": true,
"declaration": true,
+ "esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"declarationDir": "dist/types",
"outDir": "dist/lib",
- "typeRoots": [
- "node_modules/@types"
- ]
+ "typeRoots": ["node_modules/@types"],
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["src/*"]
+ }
},
- "include": [
- "src"
- ]
+ "include": ["src"]
}
diff --git a/tslint.json b/tslint.json
deleted file mode 100644
index 398a416..0000000
--- a/tslint.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "extends": [
- "tslint-config-standard",
- "tslint-config-prettier"
- ]
-}
\ No newline at end of file