/* * * * * * * * * * * *
 * Stripe integration  *
 * * * * * * * * * * * */


var stripe = null;
var donation = 0;
var frequency = "single"
var altchaSolution = null;
var clientSecret = null;
var elements = null;
var paymentMethod = "payment";
var paymentElement = null;
var csrfToken = null;
var baseUrl = null;
var displayPaymentError = null;
var handleFormErrors = null;
const unknownServiceError = `An unexpected error occurred.
Please wait and try your donation again later.
If this error persists, please email giving@torproject.org.`;
const stripeNotLoadedError = `Stripe is not loaded. Probably this page
does not contain a Stripe API key.`;
const paymentElementOptions = {
  layout: "tabs",
  fields: {
    // Never collect email and address in the Stripe payment element, because
    // we collect them in our custom form and pass them to Stripe when
    // confirming the payment.
    billingDetails: {
      "address": "never",
      "email": "never"
    }
  }
};
const paymentElementAppearance = {
  theme: "stripe",
  variables: {
    gridRowSpacing: '32px',
  },
  rules: {
    '.Label': {
      marginBottom: '12px',
    },
  },
};


const apiKey = document.getElementById("stripe-api-key")?.dataset?.key;
if (apiKey) {
  stripe = Stripe(apiKey, { locale: 'en' });
}

/*
 * function: stripeInit
 * purpose:
 *    Instantiates several Stripe-wide variables,
 *    creates the stripe Elements and Payment Element objects,
 *    then adds listener to Altcha to only initialize
 *    the Stripe payment fields after the proof-of-work
 *    has been validated.
 *    https://docs.stripe.com/js/elements_object/create
 *    https://docs.stripe.com/js/elements_object/create_payment_element
 */
async function stripeInit(initialDonation, token, serverBase, errorPay, errorForm) {
  if (!stripe) {
    throw new Error(stripeNotLoadedError);
  }

  donation = initialDonation;
  csrfToken = token;
  baseUrl = serverBase;
  displayPaymentError = errorPay;
  handleFormErrors = errorForm;

  elements = stripe.elements({
    mode: paymentMethod,
    amount: parseInt(donation),
    currency: 'usd',
    appearance: paymentElementAppearance,
  });
  paymentElement = elements.create("payment", paymentElementOptions);

  document.addEventListener('DOMContentLoaded', () => {
    const widget = document.querySelector('altcha-widget');
    widget.addEventListener('statechange', function (ev) {
      if (ev.detail.state === 'verified') {
        altchaSolution = ev.detail.payload;
        stripePaymentElementReveal();
      }
    });
  });
}


/*
 * function: stripeUpdatePaymentElements
 * purpose:
 *    Update the Stripe Elements object with the current
 *    payment method and amount, update the Payment Element
 *    with that new amount, and (if Altcha is finished verifying)
 *    mount the Payment Element to the DOM.
 *    https://docs.stripe.com/js/elements_object/update
 *    https://docs.stripe.com/js/elements_object/update_payment_element
 */
async function stripeUpdatePaymentElements() {
  elements.update({
    mode: paymentMethod,
    amount: parseInt(donation)
  });

  if(altchaSolution) {
    const btnSubmit = document.getElementById("btn-donateSubmit");
    btnSubmit.disabled = false;
    paymentElement.mount("#payment-element");
  }
}


/*
 * function: stripePaymentElementReveal
 * purpose:
 *    Call stripeUpdatePaymentElements(), then
 *    hide the now-verified Altcha interface,
 *    revealing the Stripe Payment Element.
 */
async function stripePaymentElementReveal() {
  await stripeUpdatePaymentElements();
}


/*
 * function: stripeUpdate
 * purpose:
 *    Update the stored donation amount and frequency, as well
 *    as the payment method, then call stripeUpdatePaymentElements()
 *    to propogate those changes to the on-page payment element.
 */
async function stripeUpdate(donationAmount = donation, donationFrequency = frequency) {
  donation = donationAmount;
  frequency = donationFrequency;
  paymentMethod = (frequency === "single") ? "payment" : "subscription";

  await stripeUpdatePaymentElements();
}


/*
 * function: stripeSubmit
 * purpose:
 *    1. Collect form data and validate it
 *    2. Validate Stripe fields via the Elements element
 *    3. Treat and send data for processing, generating a client token
 *    4. Use client token and validated Elements to confirm payment
 *    5. After successful payment, redirect user to success page
 */
async function stripeSubmit() {
  if (!stripe) {
    throw new Error(stripeNotLoadedError);
  }

  const formData = $("#donateForm").serializeArray();
  const formParam = $.param(formData);

  const formValidate = await fetch("/validate/", {
    method: "POST",
    credentials: "include",
    headers: {
      "Content-Type": "application/json",
      "X-CSRFToken": csrfToken,
    },
    body: JSON.stringify({
      form: formParam,
    }),
  });

  const validateData = await formValidate.json();

  if(validateData.errors) {
    handleFormErrors(validateData.errors);
    return false;
  }

  /*  When deferring the collection of client_secret, Stripe requires that we manually
   *  run form validation via the elements object in preparation for instantiating the
   *  subscription. We do so here.
   *  https://docs.stripe.com/js/elements/submit
   */
  var elementsError = await elements.submit();
  if (elementsError.error) {
    displayPaymentError("Unable to submit because of a validation error. Please review your information and try again.");
    return;
  }

  let processObject = {};

  formData.forEach(function (el) {
    processObject[el.name] = el.value;
  });

  processObject.payload = altchaSolution;
  processObject.email = processObject.email_address;
  processObject.amount = donation;
  processObject.recurring = processObject.single_or_monthly == "monthly" ? true : false;

  // override country property to provide standard 2-letter ISO code instead of name
  let countryElement = $(`#donateForm select[name='country'] option[value='${processObject.country}']`)[0];
  processObject.country = countryElement.dataset.countrycode;

  const stripeResponse = await fetch("/stripe/process/", {
    method: "POST",
    credentials: "include",
    headers: {
      "Content-Type": "application/json",
      "X-CSRFToken": csrfToken,
    },
    body: JSON.stringify(processObject),
  });

  const processData = await stripeResponse.json();

  if(processData.error) {
    paymentElement.unmount()
    displayPaymentError(processData.error);
    return false;
  } else if(!processData.client_secret) {
    displayPaymentError(unknownServiceError);
    return false;
  } else {
    var confirmPaymentArgs = {
      elements,
      clientSecret: processData.client_secret,
      confirmParams: {
        return_url: `${baseUrl}/donate-thank-you/`,
        payment_method_data: {
          billing_details: {
            name: processObject.first_name + " " + processObject.last_name,
            email: processObject.email,
            address: {
              city: processObject.city,
              country: processObject.country,
              line1: processObject.street_address,
              line2: processObject.apartment_number,
              postal_code: processObject.postal_code,
              state: processObject.state || "",
            },
          },
        },
      },
    }

    const confirmError = await stripe.confirmPayment(confirmPaymentArgs);
    if(confirmError.error) {
      if (confirmError.error === "card_error" || confirmError.error === "validation_error") {
        displayPaymentError(confirmError.message);
      } else {
        displayPaymentError(unknownServiceError);
      }
    }
  }

  return;
}


/*
 * function: stripeResults
 * purpose:
 *    sniff URL for stripe transaction
 *    parameters and conditionally
 *    display content based on result
 *    of transaction
 */
async function stripeResults() {
  if (!stripe) {
    throw new Error(stripeNotLoadedError);
  }

  const params = new Proxy(new URLSearchParams(window.location.search), {
    get: (searchParams, prop) => searchParams.get(prop),
  });

  let intent = params.payment_intent || "";
  let secret = params.payment_intent_client_secret || "";

  if (!secret) return;

  const { paymentIntent } = await stripe.retrievePaymentIntent(secret);

  $("body").addClass(paymentIntent.status || "requires_payment_method");
}

function haveStripeClientSecret() {
  return clientSecret ? true : false;
}

export {
  stripe,
  stripeInit,
  stripeUpdate,
  stripeSubmit,
  stripeResults,
  haveStripeClientSecret,
};
