bitovi/canjs

View on GitHub
docs/can-guides/commitment/recipes/credit-card-advanced/9-disable-payments.js

Summary

Maintainability
F
4 days
Test Coverage
import { kefir as Kefir, StacheElement } from "//unpkg.com/can@6/ecosystem.mjs";

class CCPayment extends StacheElement {
    static view = `
        <form on:submit="this.pay(scope.event)">
            {{# if(this.showCardError.value) }}
                <div class="message">{{ this.cardError.value }}</div>
            {{/ if }}

            {{# if(this.showExpiryError.value) }}
                <div class="message">{{ this.expiryError.value }}</div>
            {{/ if }}

            {{# if(this.showCVCError.value) }}
                <div class="message">{{ this.cvcError.value }}</div>
            {{/ if }}

            <input type="text" name="number" placeholder="Card Number"
                    on:input:value:to="this.userCardNumber.value"
                    on:blur="this.userCardNumberBlurred.emitter.value(true)"
                    {{# if(this.showCardError.value) }}class="is-error"{{/ if }}>

            <input type="text" name="expiry" placeholder="MM-YY"
                    on:input:value:to="this.userExpiry.value"
                    on:blur="this.userExpiryBlurred.emitter.value(true)"
                    {{# if(this.showExpiryError.value) }}class="is-error"{{/ if }}>
    
            <input type="text" name="cvc" placeholder="CVC"
                    on:input:value:to="this.userCVC.value"
                    on:blur="this.userCVCBlurred.emitter.value(true)"
                    {{# if(this.showCVCError.value) }}class="is-error"{{/ if }}>

            <button disabled:from="this.disablePaymentButton.value">
                {{# eq(this.paymentStatus.value.status, "pending") }}Paying{{ else }}Pay{{/ eq }} \${{ this.amount.value }}
            </button>
        </form>
  `;

    static props = {
        amount: {
            get default() {
                return Kefir.constant(1000);
            }
        },

        userCardNumber: {
            get default() {
                return Kefir.emitterProperty();
            }
        },

        userCardNumberBlurred: {
            get default() {
                return Kefir.emitterProperty();
            }
        },

        userExpiry: {
            get default() {
                return Kefir.emitterProperty();
            }
        },

        userExpiryBlurred: {
            get default() {
                return Kefir.emitterProperty();
            }
        },

        userCVC: {
            get default() {
                return Kefir.emitterProperty();
            }
        },

        userCVCBlurred: {
            get default() {
                return Kefir.emitterProperty();
            }
        },

        payClicked: {
            get default() {
                return Kefir.emitterProperty();
            }
        },

        get cardNumber() {
            return this.userCardNumber.map(card => {
                if (card) {
                    return card.replace(/[\s-]/g, "");
                }
            });
        },

        get cardError() {
            return this.cardNumber.map(this.validateCard);
        },

        get showCardError() {
            return this.showOnlyWhenBlurredOnce(
                this.cardError,
                this.userCardNumberBlurred
            );
        },

        // EXPIRY
        get expiry() {
            return this.userExpiry.map(expiry => {
                if (expiry) {
                    return expiry.split("-");
                }
            });
        },

        get expiryError() {
            return this.expiry.map(this.validateExpiry).toProperty();
        },

        get showExpiryError() {
            return this.showOnlyWhenBlurredOnce(
                this.expiryError,
                this.userExpiryBlurred
            );
        },

        // CVC
        get cvc() {
            return this.userCVC;
        },

        get cvcError() {
            return this.cvc.map(this.validateCVC).toProperty();
        },

        get showCVCError() {
            return this.showOnlyWhenBlurredOnce(this.cvcError, this.userCVCBlurred);
        },

        get isCardInvalid() {
            return Kefir.combine(
                [this.cardError, this.expiryError, this.cvcError],
                (cardError, expiryError, cvcError) => {
                    return !!(cardError || expiryError || cvcError);
                }
            );
        },

        get card() {
            return Kefir.combine(
                [this.cardNumber, this.expiry, this.cvc],
                (cardNumber, expiry, cvc) => {
                    return { cardNumber, expiry, cvc };
                }
            );
        },

        // STREAM< Promise<Number> | undefined >
        get paymentPromises() {
            return Kefir.combine(
                [this.payClicked],
                [this.card],
                (payClicked, card) => {
                    if (payClicked) {
                        console.log("Asking for token with", card);
                        return new Promise(resolve => {
                            setTimeout(() => {
                                resolve(1000);
                            }, 2000);
                        });
                    }
                }
            );
        },

        // STREAM< STREAM<STATUS> >
        // This is a stream of streams of status objects.
        get paymentStatusStream() {
            return this.paymentPromises.map(promise => {
                if (promise) {
                    // STREAM<STATUS>
                    return Kefir.concat([
                        Kefir.constant({
                            status: "pending"
                        }),
                        Kefir.fromPromise(promise).map(value => {
                            return {
                                status: "resolved",
                                value: value
                            };
                        })
                    ]);
                } else {
                    // STREAM
                    return Kefir.constant({
                        status: "waiting"
                    });
                }
            });
        },

        // STREAM<STATUS> //{status: "waiting"} | {status: "resolved"}
        get paymentStatus() {
            return this.paymentStatusStream.flatMap().toProperty();
        },

        get disablePaymentButton() {
            return Kefir.combine(
                [this.isCardInvalid, this.paymentStatus],
                (isCardInvalid, paymentStatus) => {
                    return (
                        isCardInvalid === true ||
                        !paymentStatus ||
                        paymentStatus.status === "pending"
                    );
                }
            ).toProperty(() => {
                return true;
            });
        }
    };

    pay(event) {
        event.preventDefault();
        this.payClicked.emitter.value(true);
    }

    validateCard(card) {
        if (!card) {
            return "There is no card";
        }
        if (card.length !== 16) {
            return "There should be 16 characters in a card";
        }
    }

    validateExpiry(expiry) {
        if (!expiry) {
            return "There is no expiry. Format  MM-YY";
        }
        if (
            expiry.length !== 2 ||
            expiry[0].length !== 2 ||
            expiry[1].length !== 2
        ) {
            return "Expiry must be formatted like MM-YY";
        }
    }

    validateCVC(cvc) {
        if (!cvc) {
            return "There is no CVC code";
        }
        if (cvc.length !== 3) {
            return "The CVC must be at least 3 numbers";
        }
        if (isNaN(parseInt(cvc))) {
            return "The CVC must be numbers";
        }
    }

    showOnlyWhenBlurredOnce(errorStream, blurredStream) {
        const errorEvent = errorStream.map(error => {
            if (!error) {
                return {
                    type: "valid"
                };
            } else {
                return {
                    type: "invalid",
                    message: error
                };
            }
        });

        const focusEvents = blurredStream.map(isBlurred => {
            if (isBlurred === undefined) {
                return {};
            }
            return isBlurred
                ? {
                        type: "blurred"
                  }
                : {
                        type: "focused"
                  };
        });

        return Kefir.merge([errorEvent, focusEvents])
            .scan(
                (previous, event) => {
                    switch (event.type) {
                        case "valid":
                            return Object.assign({}, previous, {
                                isValid: true,
                                showCardError: false
                            });
                        case "invalid":
                            return Object.assign({}, previous, {
                                isValid: false,
                                showCardError: previous.hasBeenBlurred
                            });
                        case "blurred":
                            return Object.assign({}, previous, {
                                hasBeenBlurred: true,
                                showCardError: !previous.isValid
                            });
                        default:
                            return previous;
                    }
                },
                {
                    hasBeenBlurred: false,
                    showCardError: false,
                    isValid: false
                }
            )
            .map(state => {
                return state.showCardError;
            });
    }
}

customElements.define("cc-payment", CCPayment);