Source: main.js

import { reactive, watch, watchEffect } from "vue";
import createUpsellApp from "/src/create-upsell-app.js";
import UpsellStore from "/src/upsell-store.js";
import AnalyticsObserver from "/src/analytics-observer.js";
import { captureException } from "@sentry/vue";

/**
 * A config object for the {@link main} function that is easy to override for local development.
 * Required to initialize the upsell system.
 * @typedef {Object} UpsellAppConfig
 * @property {number} orderTotal - Total price of the order. Used to calculate analytics event values.
 * @property {Object[]} cartItems - Array of cart items. 
 * @property {string} assetHostingURL - URL for the asset hosting service
 * @property {string} upsellEndpointURL - URL for the upsell endpoint
 * @property {string} rechargeUpsellServiceURL - URL for the Recharge Upsell Service
 */

/**
 * Main entry point for the app.
 * This will be loaded directly onto the "Thank You" page.
 * It fetches the upsell flow from the server and creates the UpsellStore and Vue app.
 * @public
 * @async
 * @function main
 * @param {UpsellAppConfig} config - Config object which may override the {@link defaultConfig} 
 * @returns {void}
 */
export default async function main(config = {}) {
    // If the store can be restored from snapshot, we don't need to fetch the API again
    const store = getSavedState() || await createUpsellStore(config);
    // A snapshot is saved every time the store updates.
    saveState(store);
    watch(store, () => saveState(store));

    // 600 seconds = 10 minutes
    // ReCharge Upsell Service automatically closes the upsell window after 10 minutes.
    store.exitFlowOnTimeout(600);

    createUpsellApp(store);

    /**
     * Analytics depend on the global dataLayer object from Google Tag Manager.
     */
    const analytics = new AnalyticsObserver(dataLayer, store);
    watch(store, (newStore) => analytics.update(newStore));
}

/**
 * Fetches the flow from the /upsell API and creates the {@link UpsellStore}.
 * The csrf token is also retrieved from the page, if it exists.
 * @param {UpsellAppConfig} config - Config object which may override the {@link defaultConfig}
 * @returns {Promise<UpsellStore>} - The upsell store
 */
async function createUpsellStore(config) {
    let csrfToken = "";
    let flow = null;
    const upsellEndpointURL = config.upsellEndpointURL || "https://cf.earthbreezedev.com/api/upsell";

    /**
     * Get the CSRF token first. Without it, there is no point loading the upsell flow.
    */
    try {
        csrfToken = getCSRFToken();
        flow = await getFlow(upsellEndpointURL, cartItems);
        console.log("DEBUG: flow", flow);
    } catch (error) {
        captureException(error);
    }

    const upsellStoreOptions = {
        assetHostingURL: "https://cf.earthbreezedev.com/offer-views",
        rechargeUpsellServiceURL: window.location.href,
        csrfToken,
        ...config
    };

    return reactive(new UpsellStore(flow, upsellStoreOptions));
}

/**
 * Get the CSRF token from the hidden input on the page.
 * The CSRF token is required for the ReCharge Upsell Service endpoint.
 * If it is not found, an error will be thrown.
 * @function getCSRFToken
 * @returns {string} - CSRF token
 * @throws {Error} - If the CSRF token is not found
 */
function getCSRFToken() {
    const csrfInput = document.getElementById("csrf_token");
    const csrfToken = csrfInput ? csrfInput.value : null;

    if (!csrfToken) throw new Error("CSRF token not found");

    return csrfToken;
}


/**
 * Fetch the upsell flow from the server.
 * This is what provides the data for the {@link UpsellStore}
 * @async
 * @function getFlow
 * @param {string} endpoint - URL for the /upsell API endpoint
 * @param {Object[]} cartItems - Array of cart items
 * @returns {Object} - Upsell flow
 * @throws {Error} - If the response is not successful
 */
async function getFlow(endpoint, cartItems) {
    const response = await fetch(endpoint, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ cartItems }),
    });
    const { flow } = await response.json();
    return flow;
}

/**
 * Creates a snapshot of the store and saves it to sessionStorage.
 * @param {UpsellStore} store - The store to save
 */
async function saveState(store) {
    const snapshot = await store.createSnapshot();
    sessionStorage.setItem('upsellStoreSnapshot', snapshot);
}

/**
 * Checks for a saved store in sessionStorage. If it exists, it will be restored. Otherwise, returns null.
 * @returns {UpsellStore|null} - The saved store, or null if there is no saved store
 */
function getSavedState() {
    const snapshot = sessionStorage.getItem('upsellStoreSnapshot');
    return snapshot
        ? reactive(UpsellStore.fromSnapshot(snapshot))
        : null;
}

/**
 * If this is a production build, run the main function.
 */
if (import.meta.env.PROD) {
    window.upsell = main;
}