Sudden issue with smart view triggering custom action

Feature(s) impacted

Smart view calling smart action.

Observed behavior

The custom action is failing to be called by the smart view.

Expected behavior

The custom action is called by the smart view.

Failure Logs

===== Created Stripe Card Token =====
 {paymentMethod: 'CARD', name: 'Jonathan Consumer', token: 'pm_1Qu***42S', last4: '4242', type: 'visa', …}

VM4000:390 ===== Sync to Payment Service =====
 {paymentMethod: 'CARD', name: 'Jonathan Consumer', token: 'pm_1Qu***42S', last4: '4242', type: 'visa', …}billingZip: "87505"expiry: "2026-03-01"last4: "4242"name: "Jonathan Consumer"paymentMethod: "CARD"subscriptionId: "sub_05bdfaa3-2ccc-5d25-9b51-1db94a000f36"token: "pm_1Qu***42S"type: "visa"vendor: "stripe"[[Prototype]]: Object

VM4000:399 ===== CustomAction is retreived =====

VM4000:405 ===== CustomAction Fields are set =====

chunk.565.cfafbf9dfe4e0386b102.js:2 Internal Error: Failed to sync to Payment Service:
 TypeError: Cannot read properties of undefined (reading 'triggerCustomAction')
    at _class2.onSyncToPaymentService (eval at registerComponent (client-19a93075c67121039116818f6b1d87e2.js:1:5419361), <anonymous>:411:27)
    at HTMLButtonElement.<anonymous> (vendor-30dd650ea1cf32022ca2d1864db3d536.js:22:336027)

Context

  • Project name: Scratch Payment Service
  • Team name: All
  • Environment name: All
  • Agent technology: nodejs
  • Agent (forest package) name & version: “forest-express-sequelize” “9.3.25”
  • Database type: msyql and psql
  • Recent changes made on your end if any: None

This just started happening today around 18:45 UTC. It was initially noticed in production but is reproducible in both staging and local environments. Nothing changed on our end.

Here is the directly related code block:

@action
  onSyncToPaymentService() {
    try {
      const subscriptionId = this.subscriptionId;
      if (!this.resultToken || !this.resultToken.token) {
        throw new Error("Missing Token data");
      }
      const tokenData = {
        ...this.resultToken,
        subscriptionId,
        vendor: this.vendor,
      };
      console.log("===== Sync to Payment Service =====\n", {
        ...tokenData,
        token: obfuscateToken(tokenData.token),
        vendor: this.vendor,
      });
      this.isClickedToSync = true;

      //Getting Add Token Custom Action
      const customActions = this.store
        .peekAll("custom-action")
        .filter((c) => c.name === "Add Token");

      console.log("===== CustomAction is retreived =====\n");

      //Setting the customerActionValues with the tokenData
      customActions[0].fields.forEach((f) => {
        f.customActionValue = tokenData[f.fieldName];
      });

      console.log("===== CustomAction Fields are set =====\n");

      if (!this.selectedRecord) {
        this.selectedRecord = {};
      }

      //Triggering CustomAction
//THIS IS THE SOURCE OF THE ERROR
      this.customAction.triggerCustomAction(
        customActions[0],
        [this.selectedRecord],
        tokenData
      );

      console.log("===== CustomAction is triggered =====\n");
    } catch (error) {
      console.error(
        "Internal Error: Failed to sync to Payment Service:\n",
        error
      );
    }
  }

@jeffladiray

Hello @Brett_Belka,

Thanks for the report, we have a suspected contribution in mind that could have caused this issue,
We are merging a revert ASAP, as this contribution is a few days old we sadly cannot rollback.

Sorry for the inconvenience.

The customAction service is a private service that should not be called directly.

Could you try to replace:

      //Triggering CustomAction
//THIS IS THE SOURCE OF THE ERROR
      this.customAction.triggerCustomAction(
        customActions[0],
        [this.selectedRecord],
        tokenData
      );

By:

import { triggerSmartAction } from "client/utils/smart-view-utils";

      //Triggering CustomAction
//THIS IS THE SOURCE OF THE ERROR
      triggerSmartAction(
        customActions[0].name,
        [this.selectedRecord],
        tokenData
      );

It looks like your smart view is quite outdated. Don’t hesitate to share it here completely so I can update it properly if you want a code up to date :wink:

@vince Sounds good.

// v1.6.0: Add ability to fetch loanNumber without subscriptionToken record

import Component from "@ember/component";
import { scheduleOnce } from "@ember/runloop";
import { inject as service } from "@ember/service";
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
import { loadExternalJavascript } from 'client/utils/smart-view-utils';

const CARD_STYLE = {
  base: {
    color: "#54BD7E",
    fontFamily: '"Helvetica Neue", Helvetica, sans-serif',
    fontSmoothing: "antialiased",
    fontSize: "14px",
    "::placeholder": {
      color: "#aab7c4", // --color-beta-on-surface_light
    },
  },
  invalid: {
    color: "#FB6669",
    iconColor: "#FB6669",
  },
};

const ELEMENT_IDS = {
  STRIPE_CARD_ELEMENT: "stripe-card-element",
  STRIPE_CARD_ERROR: "stripe-card-errors",
  STRIPE_CARD_HOLDER_NAME: "stripe-card-holder-name",
  STRIPE_CARD_HOLDER_NAME_ERROR: "stripe-card-holder-name-error",

  STRIPE_ACH_HOLDER_NAME: "stripe-ach-holder-name",
  STRIPE_ACH_ACCOUNT_NUMBER: "stripe-ach-account-number",
  STRIPE_ACH_ROUTING_NUMBER: "stripe-ach-routing-number",
  STRIPE_ACH_ACCOUNT_TYPE: "stripe-ach-account-type",
  STRIPE_ACH_HOLDER_NAME_ERROR: "stripe-ach-holder-name-error",
  STRIPE_ACH_ACCOUNT_NUMBER_ERROR: "stripe-ach-account-number-error",
  STRIPE_ACH_ROUTING_NUMBER_ERROR: "stripe-ach-routing-number-error",
};

const PAYMENT_METHOD = {
  CARD: "CARD",
  ACH: "ACH",
  PAD: "PAD",
};
const PAYMENT_METHOD_VENDOR = {
  STRIPE: "stripe",
  PAYIX: "payix",
};
const STRIPE_ACH_DEFAULT_VALUE = {
  BANK_COUNTRY_CODE: "US",
  CURRENCY: "USD",
};

const obfuscateToken = (token) =>
  token.includes("_")
    ? token.replace(/^(.*)_((.{3}).*(.{3}))$/, "$1_$3***$4")
    : token.replace(/^(.{3})(.*)(.{3})$/, "$1***$3");

export default class extends Component {
  @service() customAction;
  @service() store;

  @tracked selectedRecord = null;
  @tracked resultToken = null;

  @tracked stripe = null;
  @tracked isLoadedStripe = false;
  @tracked stripeCard = null;
  @tracked isCreatingStripeCard = false;
  @tracked isCreatingStripeACH = false;
  @tracked isClickedToSync = false;
  @tracked stripePublicKey = null;
  @tracked payixIframeCardAchURL = null;
  @tracked payixIframePadURL = null;
  @tracked subscriptionId = null;
  @tracked vendor = null;
  @tracked loanNumber = null;

  constructor(...args) {
    super(...args);

    console.log(`===== Smart view: v1.6.0: Add ability to fetch loanNumber without subscriptionToken record =====`);
  }

  async didInsertElement() {
    await this.setDefaultSelectedRecord();

    this.onCreatedPayixToken = this.onCreatedPayixToken.bind(this);
    window.addEventListener("message", this.onCreatedPayixToken);
  }

  willRemoveElement() {
    window.removeEventListener("message", this.onCreatedPayixToken);
  }

  @action
  onRecordsChange() {
    this.setDefaultSelectedRecord();
  }

  isSupportedStripe() {
    return this.vendor === PAYMENT_METHOD_VENDOR.STRIPE;
  }

  setURLs(data) {
    this.payixIframeCardAchURL = data["forest-payix_iframe_card_ach_url"];
    this.payixIframePadURL = data["forest-payix_iframe_pad_url"];
    this.stripePublicKey = data["forest-stripe_public_key"];
  }

  setSubscriptionIdAndVendor(record) {
    if (this.records) {
      this.subscriptionId = record["forest-subscription"]["id"];
      this.vendor = record["forest-subscription"]["forest-vendor"];
      this.loanNumber = record["forest-subscription"]["forest-loanNumber"];
    }
    if (!this.records) {
      this.subscriptionId = record["forest-id"];
      this.vendor = record["forest-vendor"];
      this.loanNumber = record["forest-loanNumber"];
    }
  }

  setDefaultSelectedRecord() {
    if (this.records && this.records.recordData) {
      this.subscriptionId = this.records.recordData.id;
      this.vendor = PAYMENT_METHOD_VENDOR.STRIPE;
      if (this.records.recordData.__data) {
        this.setURLs(this.records.recordData.__data);
        //Setting the vendor here again as if it's a Canadian User
        //The Vendor should be Payix not Stripe
        if (this.records.recordData.__data["forest-vendor"]) {
          this.vendor = this.records.recordData.__data["forest-vendor"];
        }
      }
      if (!this.stripe) this.loadPlugins();
    }

    if (!this.selectedRecord) {
      if (this.records && this.records.firstObject) {
        this.selectedRecord = this.records.firstObject;
        this.setURLs(this.records.firstObject);
        this.setSubscriptionIdAndVendor(this.records.firstObject);
        if (!this.stripe) this.loadPlugins();
      }

      if (!this.records) {
        const regex = /sub_[\w-]+/;
        const url = window.location.href
        const hasSubscriptionId = url.match(regex)
        if (hasSubscriptionId) {
          const subscriptionId = url.match(regex)[0]
          this.store.findRecord('forest-subscription', subscriptionId)
            .then((record) => {
              this.setURLs(record)
              this.setSubscriptionIdAndVendor(record)
              if (!this.stripe) this.loadPlugins();
            })
        }
      }
    }
  }

  loadPlugins() {
    scheduleOnce("afterRender", this, async () => {
      try {
        const stripePublicKey = this.stripePublicKey;
        await loadExternalJavascript('//js.stripe.com/v2/');
        await loadExternalJavascript('//js.stripe.com/v3/');

        Stripe.setPublishableKey(stripePublicKey); // v2
        const stripe = Stripe(stripePublicKey); // v3
        this.stripe = stripe;
        this.isLoadedStripe = true;

        if (this.isSupportedStripe()) {
          this.renderStripeElements();
        }
      } catch (error) {
        console.error("Internal Error: Failed to load Stripe:\n", error);
      }
    });
  }

  renderStripeCard() {
    const { stripe } = this;
    const elements = stripe.elements();
    const stripeCard = elements.create("card", { style: CARD_STYLE });
    stripeCard.mount(`#${ELEMENT_IDS.STRIPE_CARD_ELEMENT}`);

    stripeCard.on("change", function (event) {
      let errorElement = document.getElementById(ELEMENT_IDS.STRIPE_CARD_ERROR);
      if (event.error) {
        errorElement.textContent = event.error.message;
      } else {
        errorElement.textContent = "";
      }
    });
    this.stripeCard = stripeCard;
  }

  renderStripeElements() {
    if (!this.stripe) return;
    this.renderStripeCard();
  }

  onCreatedStripeCardToken(cardData, cardName) {
    try {
      const {
        id,
        billing_details: {
          address: { postal_code },
        },
        card: { brand, last4, exp_month, exp_year },
      } = cardData;
      const expiry = `${exp_year}-${("0" + exp_month).slice(-2)}-01`;
      const resultToken = {
        paymentMethod: PAYMENT_METHOD.CARD,
        name: cardName,
        token: id,
        last4,
        type: brand,
        expiry,
        billingZip: postal_code,
      };
      console.log("===== Created Stripe Card Token =====\n", {
        ...resultToken,
        token: obfuscateToken(resultToken.token),
      });
      this.resultToken = resultToken;
    } catch (error) {
      console.error(
        "Internal Error: Failed to add new Card with Stripe:\n",
        error
      );
    }
  }

  onCreatedStripeACHToken(achData, achValues) {
    try {
      const {
        id,
        bank_account: { last4, bank_name },
      } = achData;
      const resultToken = {
        paymentMethod: PAYMENT_METHOD.ACH,
        token: id,
        last4,
        bankName: bank_name,
        name: achValues.achHolderName,
        type: achValues.achAccountType,
      };
      console.log("===== Created Stripe ACH Token =====\n", {
        ...resultToken,
        token: obfuscateToken(resultToken.token),
      });
      this.resultToken = resultToken;
    } catch (error) {
      console.error(
        "Internal Error: Failed to add new ACH with Stripe:\n",
        error
      );
    }
  }

  onCreatedPayixToken(event) {
    try {
      const isAddedWithPayixSuccessfully =
        event.data &&
        event.data.token !== "" &&
        event.data.token !== null &&
        event.data.token !== undefined;
      if (isAddedWithPayixSuccessfully) {
        const {
          token,
          nameOnCard,
          nameOnAch,
          nameOnPad,
          cardBrand,
          expiryDate,
          lastFourDigits,
          billingZip,
          bankName,
        } = event.data;
        const isCard = !!nameOnCard;
        const isACH = !isCard && !!nameOnAch;
        const isPAD = !isCard && !isACH && !!nameOnPad;
        let resultToken;

        if (isCard) {
          const expiry = expiryDate.replace(/(\d{2})\/(\d{4})/, "$2-$1-01");
          resultToken = {
            paymentMethod: PAYMENT_METHOD.CARD,
            name: nameOnCard,
            token,
            last4: lastFourDigits,
            type: cardBrand,
            expiry,
            billingZip,
          };
        } else if (isACH) {
          resultToken = {
            paymentMethod: PAYMENT_METHOD.ACH,
            name: nameOnAch,
            token,
            last4: lastFourDigits,
            bankName,
          };
        } else if (isPAD) {
          resultToken = {
            paymentMethod: PAYMENT_METHOD.PAD,
            name: nameOnPad,
            token,
            last4: lastFourDigits,
            bankName,
          };
        }

        if (resultToken) {
          console.log(
            `===== Created Payix ${resultToken.paymentMethod} Token =====\n`,
            {
              ...resultToken,
              token: obfuscateToken(resultToken.token),
            }
          );
          this.resultToken = resultToken;
        }
      }
    } catch (error) {
      console.error(
        "Internal Error: Failed to add new payment method with Payix:\n",
        error
      );
    }
  }

  validateStripeACHForm() {
    const { value: achHolderName } = document.getElementById(
      ELEMENT_IDS.STRIPE_ACH_HOLDER_NAME
    );
    const { value: achAccountNumber } = document.getElementById(
      ELEMENT_IDS.STRIPE_ACH_ACCOUNT_NUMBER
    );
    const { value: achRoutingNumber } = document.getElementById(
      ELEMENT_IDS.STRIPE_ACH_ROUTING_NUMBER
    );
    const { value: achAccountType } = document.getElementById(
      ELEMENT_IDS.STRIPE_ACH_ACCOUNT_TYPE
    );
    const holderNameErrorElement = document.getElementById(
      ELEMENT_IDS.STRIPE_ACH_HOLDER_NAME_ERROR
    );
    const accountNumberErrorElement = document.getElementById(
      ELEMENT_IDS.STRIPE_ACH_ACCOUNT_NUMBER_ERROR
    );
    const routingNumberErrorElement = document.getElementById(
      ELEMENT_IDS.STRIPE_ACH_ROUTING_NUMBER_ERROR
    );

    let isValidHolderName = false;
    if (!achHolderName) {
      holderNameErrorElement.textContent =
        "Please input Account ownner's fullname";
    } else {
      holderNameErrorElement.textContent = "";
      isValidHolderName = true;
    }

    let isValidAccountNumber = false;
    if (!achAccountNumber) {
      accountNumberErrorElement.textContent = "Please input Account number";
    } else {
      const isValidAccountNumberByStripe = Stripe.bankAccount.validateAccountNumber(
        achAccountNumber,
        STRIPE_ACH_DEFAULT_VALUE.BANK_COUNTRY_CODE
      );
      if (!isValidAccountNumberByStripe) {
        accountNumberErrorElement.textContent =
          "Your Account number is invalid";
      } else {
        accountNumberErrorElement.textContent = "";
        isValidAccountNumber = true;
      }
    }

    let isValidRoutingNumber = false;
    if (!achRoutingNumber) {
      routingNumberErrorElement.textContent = "Please input Routing number";
    } else {
      const isValidRoutingNumberByStripe = Stripe.bankAccount.validateRoutingNumber(
        achRoutingNumber,
        STRIPE_ACH_DEFAULT_VALUE.BANK_COUNTRY_CODE
      );
      if (!isValidRoutingNumberByStripe) {
        routingNumberErrorElement.textContent =
          "Your Routing number is invalid";
      } else {
        routingNumberErrorElement.textContent = "";
        isValidRoutingNumber = true;
      }
    }

    const isValid = [
      isValidHolderName,
      isValidAccountNumber,
      isValidRoutingNumber,
    ].every((isValidValue) => isValidValue);

    if (!isValid) {
      return null;
    }

    return {
      achHolderName,
      achAccountNumber,
      achRoutingNumber,
      achAccountType,
    };
  }

  @action
  onSelectRecord(record) {
    this.selectedRecord = record;
    this.resultToken = null;
    this.setURLs(record);
    this.setSubscriptionIdAndVendor(record);

    if (this.isSupportedStripe()) {
      this.renderStripeElements();
    }
  }

  @action
  onSyncToPaymentService() {
    try {
      const subscriptionId = this.subscriptionId;
      if (!this.resultToken || !this.resultToken.token) {
        throw new Error("Missing Token data");
      }
      const tokenData = {
        ...this.resultToken,
        subscriptionId,
        vendor: this.vendor,
      };
      console.log("===== Sync to Payment Service =====\n", {
        ...tokenData,
        token: obfuscateToken(tokenData.token),
        vendor: this.vendor,
      });
      this.isClickedToSync = true;

      //Getting Add Token Custom Action
      const customActions = this.store
        .peekAll("custom-action")
        .filter((c) => c.name === "Add Token");

      console.log("===== CustomAction is retreived =====\n");

      //Setting the customerActionValues with the tokenData
      customActions[0].fields.forEach((f) => {
        f.customActionValue = tokenData[f.fieldName];
      });

      console.log("===== CustomAction Fields are set =====\n");

      if (!this.selectedRecord) {
        this.selectedRecord = {};
      }

      //Triggering CustomAction
      triggerCustomAction(
        customActions[0],
        [this.selectedRecord],
        tokenData
      );

      console.log("===== CustomAction is triggered =====\n");
    } catch (error) {
      console.error(
        "Internal Error: Failed to sync to Payment Service:\n",
        error
      );
    }
  }

  @action
  async onSubmitToCreateStripeCard(event) {
    event.preventDefault();
    const { value: cardHolderName } = document.getElementById(
      ELEMENT_IDS.STRIPE_CARD_HOLDER_NAME
    );
    let stripeCardNameErrorElement = document.getElementById(
      ELEMENT_IDS.STRIPE_CARD_HOLDER_NAME_ERROR
    );
    if (!cardHolderName) {
      stripeCardNameErrorElement.textContent =
        "Please input card's owner name";
    } else {
      stripeCardNameErrorElement.textContent = "";
      try {
        this.isCreatingStripeCard = true;
        const { value: cardName } = document.getElementById(
          ELEMENT_IDS.STRIPE_CARD_HOLDER_NAME
        );
        const { stripe, stripeCard } = this;
        const { paymentMethod: cardData } = await stripe.createPaymentMethod({
          type: "card",
          card: stripeCard,
          billing_details: { name: cardName },
        });
        this.isCreatingStripeCard = false;
        this.onCreatedStripeCardToken(cardData, cardName);
      } catch (error) {
        let stripeCardErrorElement = document.getElementById(
          ELEMENT_IDS.STRIPE_CARD_ERROR
        );
        stripeCardErrorElement.textContent = error.message;
      }
    }
  }

  @action
  onSubmitToCreateStripeACH(event) {
    event.preventDefault();
    const achValues = this.validateStripeACHForm();
    if (!achValues) {
      return;
    }

    try {
      this.isCreatingStripeACH = true;
      Stripe.bankAccount.createToken(
        {
          country: STRIPE_ACH_DEFAULT_VALUE.BANK_COUNTRY_CODE,
          currency: STRIPE_ACH_DEFAULT_VALUE.CURRENCY,
          account_holder_name: achValues.achHolderName,
          account_number: achValues.achAccountNumber,
          routing_number: achValues.achRoutingNumber,
          account_holder_type: achValues.achAccountType,
        },
        (status, response) => {
          if (response.error) {
            throw error;
          }
          this.isCreatingStripeACH = false;
          this.onCreatedStripeACHToken(response, achValues);
        }
      );
    } catch (error) {
      let stripeCardErrorElement = document.getElementById(
        ELEMENT_IDS.STRIPE_CARD_ERROR
      );
      stripeCardErrorElement.textContent = error.message;
    }
  }
}

@vince

import { triggerSmartAction } from "client/utils/smart-view-utils";

 //Triggering CustomAction
//THIS IS THE SOURCE OF THE ERROR
      triggerSmartAction(
        customActions[0].name,
        [this.selectedRecord],
        tokenData
     );

In the meantime, I tried this. I get a new error.

Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'findBy')
   at e.triggerSmartAction (client-cf1950b09e515…0e0b1478.js:8:23735)
e.triggerSmartAction
await in e.triggerSmartAction		
onSyncToPaymentService (anonymous)

Oh sorry I made a mistake

import { triggerSmartAction } from "client/utils/smart-view-utils";

      //Triggering CustomAction
//THIS IS THE SOURCE OF THE ERROR
      triggerSmartAction(
        this,
        customActions[0].name,
        [this.selectedRecord],
        tokenData
      );

I forgot the first parameter

I will need your template too :wink:
And I see that you use the following too this.records.recordData.id. records in your case is the list of records to display correct ? The records that are passed in arguments?
If that’s the case you are using private API and relying on them is quite dangerous. I will fix it too if you confirm

@vince

I tried adding this as first parameter.
Still got this error:

Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'findBy')
   at e.triggerSmartAction (client-cf1950b09e515…0e0b1478.js:8:23735)
e.triggerSmartAction
await in e.triggerSmartAction		
onSyncToPaymentService (anonymous)

Yes, that’s my understanding. If you have a better way, that’s great. Just don’t want to break anything. It’s been working as is for quite some time…


duh… Both are below.
template:

{{!-- v1.6.0: Add ability to fetch loanNumber without subscriptionToken record --}}

<div class="wrapper" id="smv_add_token" {{did-update this.onRecordsChange @records}}>
	<div class="wrapper-list">
		<div class="sub-tok-list-table">
			<div class="sub-tok-list-heading">
				<div class="sub-tok-list-heading-row">
					<div class="column sub-tok-ids">Subscription ID/Token ID</div>
					<div class="column loan-number">Loan number</div>
					<div class="column pm-info">Payment method info<br>(Vendor/Type/Last 4)</div>
				</div>
			</div>
			<div class="sub-tok-list-body">
				{{#each @records as |record|}}
					<div
						class="sub-tok-list-row {{if (eq this.selectedRecord record) 'selected'}}"
						{{on 'click' (fn this.onSelectRecord record)}}
					>
						<div class="column sub-tok-ids">
							<span class="value">
								{{record.forest-subscription.id}}
								<br>
								{{record.forest-token.id}}
								<br>
								{{#if record.forest-isPrimary}}
									<div class="primary-token">primary</div>
								{{/if}}
							</span>
						</div>
						<div class="column loan-number">
							<span class="value">{{record.forest-subscription.forest-loanNumber}}</span>
						</div>
						<div class="column pm-info">
							<span class="value">{{record.forest-subscription.forest-vendor}}</span>
							<span class="value">&nbsp;/&nbsp;{{record.forest-token.forest-paymentMethod}}&nbsp;/&nbsp;</span>
							<span class="value">{{record.forest-token.forest-last4}}</span>
						</div>
					</div>
				{{/each}}
			</div>
		</div>

		<DataDisplay::Table::TableFooter
			@collection={{@collection}}
			@viewList={{@viewList}}
			@records={{@records}}
			@currentPage={{@currentPage}}
			@numberOfPages={{@numberOfPages}}
			@recordsCount={{@recordsCount}}
			@isLoading={{@isLoading}}
			@fetchRecords={{@fetchRecords}}
		/>
	</div>

	<div class="wrapper-forms">
		{{#if this.stripe}}
			<h2 class="forms-title">New payment method for Loan: <b>{{this.loanNumber}}</b></h2>
		{{else}}
			<h2 class="forms-title">Getting ready ...</h2>
		{{/if}}

		{{#if this.resultToken}}
			{{#if (eq this.resultToken.paymentMethod 'CARD')}}
				<div class="wrapper-result">
					<div class="result-row">
						<div class="label">Name</div>
						<div class="value">{{this.resultToken.name}}</div>
					</div>
					<div class="result-row">
						<div class="label">Card brand</div>
						<div class="value">{{this.resultToken.type}}</div>
					</div>
					<div class="result-row">
						<div class="label">Last 4 digits</div>
						<div class="value">{{this.resultToken.last4}}</div>
					</div>
					<div class="result-row">
						<div class="label">Expiry date</div>
						<div class="value">{{this.resultToken.expiry}}</div>
					</div>
					{{#if this.resultToken.billingZip}}
					<div class="result-row">
						<div class="label">Billing zip</div>
						<div class="value">{{this.resultToken.billingZip}}</div>
					</div>
					{{/if}}
				</div>
			{{else}}
				<div class="wrapper-result">
					<div class="result-row">
						<div class="label">Bank name</div>
						<div class="value">{{this.resultToken.bankName}}</div>
					</div>
					<div class="result-row">
						<div class="label">Account owner's full name</div>
						<div class="value">{{this.resultToken.name}}</div>
					</div>
					<div class="result-row">
						<div class="label">Last 4 digits</div>
						<div class="value">{{this.resultToken.last4}}</div>
					</div>
					{{#if this.resultToken.type}}
					<div class="result-row">
						<div class="label">Account type</div>
						<div class="value">{{this.resultToken.type}}</div>
					</div>
					{{/if}}
				</div>
			{{/if}}
			{{#if this.isClickedToSync}}
				<h2 class="warning">Please refresh your browser to see new update after finishing the synchronization successfully.</h2>
			{{else}}
				<button class="btn btn-sync" {{on 'click' (fn this.onSyncToPaymentService)}}>
					Upload new Payment method to Payment service
				</button>
			{{/if}}
		{{else}}
			<div data-loaded="{{this.isLoadedStripe}}"
				data-visibility="{{if (eq this.vendor 'stripe') true false}}"
				class="wrapper-stripe-forms">
				{{!-- STRIPE CARD --}}
				<form id="stripe-form" data-visibility="{{if this.resultToken false true}}"
					onsubmit={{fn this.onSubmitToCreateStripeCard}}>
					<div class="stripe-form-row">
						<input id="stripe-card-holder-name" class="form-input" placeholder="Name" />
						<div id="stripe-card-holder-name-error" class="stripe-error" role="alert" />
					</div>
					<div class="stripe-form-row">
						<div id="stripe-card-element" />
						<div id="stripe-card-errors" class="stripe-error" role="alert" />
					</div>
					<div class="form-actions">
						{{#if this.isCreatingStripeCard}}
						<div class="wrapper-dots">
							<div class="dot"></div>
							<div class="dot"></div>
							<div class="dot"></div>
						</div>
						{{else}}
						<button type="submit" class="btn btn-stripe-add">Add Card</button>
						{{/if}}
					</div>
				</form>

				{{!-- STRIPE ACH --}}
				<form class="stripe-ach-form" onsubmit={{fn this.onSubmitToCreateStripeACH }}>
					<div class="stripe-ach-form-cols-wrapper">
						<div class="stripe-ach-form-col col-inputs">
							<div class="ach-form-input">
								<input type="text" id="stripe-ach-holder-name" placeholder="Account owner full name">
								<div id="stripe-ach-holder-name-error" class="stripe-error" role="alert" />
							</div>
							<div class="ach-form-input">
								<input type="text" id="stripe-ach-account-number" placeholder="Account number">
								<div id="stripe-ach-account-number-error" class="stripe-error" role="alert" />
							</div>
							<div class="ach-form-input">
								<input type="text" id="stripe-ach-routing-number" placeholder="Routing number">
								<div id="stripe-ach-routing-number-error" class="stripe-error" role="alert" />
							</div>
					</div>

					<div class="stripe-ach-form-col col-selectors">
						<div class="ach-form-selector">
							<label for="bank_country">Bank country</label>
							<select id="stripe-ach-bank-country" name="bank_country" disabled>
								<option value="US" selected>United States</option>
							</select>
						</div>
						<div class="ach-form-selector">
							<label for="currency">Currency</label>
							<select id="stripe-ach-currency" name="currency" disabled>
								<option value="USD" selected>USD</option>
							</select>
						</div>
						<div class="ach-form-selector">
							<label for="account_type">Account type</label>
							<select id="stripe-ach-account-type" name="account_type">
								<option value="individual">Individual</option>
								<option value="company">Company</option>
							</select>
						</div>
					</div>
					</div>
					<div class="form-actions">
						{{#if this.isCreatingStripeACH}}
						<div class="wrapper-dots">
							<div class="dot"></div>
							<div class="dot"></div>
							<div class="dot"></div>
						</div>
						{{else}}
						<button disabled={{this.isCreatingStripeACH}} type="submit" class="btn btn-stripe-add">Add ACH</button>
						{{/if}}
					</div>
				</form>

			</div>


			{{#if (eq this.vendor 'payix')}}
				<div class="wrapper-payix-iframes">
					<div class="warning-pad">PAD only supports for Canadian</div>
					<div class="wrapper-iframes">
						<iframe src="{{this.payixIframeCardAchURL}}" />
						<iframe src="{{this.payixIframePadURL}}" />
					</div>
				</div>
			{{/if}}
		{{/if}}
	</div>
</div>

component (current testing state)

// v1.6.1: Update to new syntax
import Component from "@ember/component";
import { scheduleOnce } from "@ember/runloop";
import { inject as service } from "@ember/service";
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
import { loadExternalJavascript, triggerSmartAction } from 'client/utils/smart-view-utils';

const CARD_STYLE = {
  base: {
    color: "#54BD7E",
    fontFamily: '"Helvetica Neue", Helvetica, sans-serif',
    fontSmoothing: "antialiased",
    fontSize: "14px",
    "::placeholder": {
      color: "#aab7c4", // --color-beta-on-surface_light
    },
  },
  invalid: {
    color: "#FB6669",
    iconColor: "#FB6669",
  },
};

const ELEMENT_IDS = {
  STRIPE_CARD_ELEMENT: "stripe-card-element",
  STRIPE_CARD_ERROR: "stripe-card-errors",
  STRIPE_CARD_HOLDER_NAME: "stripe-card-holder-name",
  STRIPE_CARD_HOLDER_NAME_ERROR: "stripe-card-holder-name-error",

  STRIPE_ACH_HOLDER_NAME: "stripe-ach-holder-name",
  STRIPE_ACH_ACCOUNT_NUMBER: "stripe-ach-account-number",
  STRIPE_ACH_ROUTING_NUMBER: "stripe-ach-routing-number",
  STRIPE_ACH_ACCOUNT_TYPE: "stripe-ach-account-type",
  STRIPE_ACH_HOLDER_NAME_ERROR: "stripe-ach-holder-name-error",
  STRIPE_ACH_ACCOUNT_NUMBER_ERROR: "stripe-ach-account-number-error",
  STRIPE_ACH_ROUTING_NUMBER_ERROR: "stripe-ach-routing-number-error",
};

const PAYMENT_METHOD = {
  CARD: "CARD",
  ACH: "ACH",
  PAD: "PAD",
};
const PAYMENT_METHOD_VENDOR = {
  STRIPE: "stripe",
  PAYIX: "payix",
};
const STRIPE_ACH_DEFAULT_VALUE = {
  BANK_COUNTRY_CODE: "US",
  CURRENCY: "USD",
};

const obfuscateToken = (token) =>
  token.includes("_")
    ? token.replace(/^(.*)_((.{3}).*(.{3}))$/, "$1_$3***$4")
    : token.replace(/^(.{3})(.*)(.{3})$/, "$1***$3");

export default class extends Component {
  @service() customAction;
  @service() store;

  @tracked selectedRecord = null;
  @tracked resultToken = null;

  @tracked stripe = null;
  @tracked isLoadedStripe = false;
  @tracked stripeCard = null;
  @tracked isCreatingStripeCard = false;
  @tracked isCreatingStripeACH = false;
  @tracked isClickedToSync = false;
  @tracked stripePublicKey = null;
  @tracked payixIframeCardAchURL = null;
  @tracked payixIframePadURL = null;
  @tracked subscriptionId = null;
  @tracked vendor = null;
  @tracked loanNumber = null;

  constructor(...args) {
    super(...args);

    console.log(`===== Smart view: v1.6.1: Update to new syntax =====`);
  }

  async didInsertElement() {
    await this.setDefaultSelectedRecord();

    this.onCreatedPayixToken = this.onCreatedPayixToken.bind(this);
    window.addEventListener("message", this.onCreatedPayixToken);
  }

  willRemoveElement() {
    window.removeEventListener("message", this.onCreatedPayixToken);
  }

  @action
  onRecordsChange() {
    this.setDefaultSelectedRecord();
  }

  isSupportedStripe() {
    return this.vendor === PAYMENT_METHOD_VENDOR.STRIPE;
  }

  setURLs(data) {
    this.payixIframeCardAchURL = data["forest-payix_iframe_card_ach_url"];
    this.payixIframePadURL = data["forest-payix_iframe_pad_url"];
    this.stripePublicKey = data["forest-stripe_public_key"];
  }

  setSubscriptionIdAndVendor(record) {
    if (this.records) {
      this.subscriptionId = record["forest-subscription"]["id"];
      this.vendor = record["forest-subscription"]["forest-vendor"];
      this.loanNumber = record["forest-subscription"]["forest-loanNumber"];
    }
    if (!this.records) {
      this.subscriptionId = record["forest-id"];
      this.vendor = record["forest-vendor"];
      this.loanNumber = record["forest-loanNumber"];
    }
  }

  setDefaultSelectedRecord() {
    if (this.records && this.records.recordData) {
      this.subscriptionId = this.records.recordData.id;
      this.vendor = PAYMENT_METHOD_VENDOR.STRIPE;
      if (this.records.recordData.__data) {
        this.setURLs(this.records.recordData.__data);
        //Setting the vendor here again as if it's a Canadian User
        //The Vendor should be Payix not Stripe
        if (this.records.recordData.__data["forest-vendor"]) {
          this.vendor = this.records.recordData.__data["forest-vendor"];
        }
      }
      if (!this.stripe) this.loadPlugins();
    }

    if (!this.selectedRecord) {
      if (this.records && this.records.firstObject) {
        this.selectedRecord = this.records.firstObject;
        this.setURLs(this.records.firstObject);
        this.setSubscriptionIdAndVendor(this.records.firstObject);
        if (!this.stripe) this.loadPlugins();
      }

      if (!this.records) {
        const regex = /sub_[\w-]+/;
        const url = window.location.href
        const hasSubscriptionId = url.match(regex)
        if (hasSubscriptionId) {
          const subscriptionId = url.match(regex)[0]
          this.store.findRecord('forest-subscription', subscriptionId)
            .then((record) => {
              this.setURLs(record)
              this.setSubscriptionIdAndVendor(record)
              if (!this.stripe) this.loadPlugins();
            })
        }
      }
    }
  }

  loadPlugins() {
    scheduleOnce("afterRender", this, async () => {
      try {
        const stripePublicKey = this.stripePublicKey;
        await loadExternalJavascript('//js.stripe.com/v2/');
        await loadExternalJavascript('//js.stripe.com/v3/');

        Stripe.setPublishableKey(stripePublicKey); // v2
        const stripe = Stripe(stripePublicKey); // v3
        this.stripe = stripe;
        this.isLoadedStripe = true;

        if (this.isSupportedStripe()) {
          this.renderStripeElements();
        }
      } catch (error) {
        console.error("Internal Error: Failed to load Stripe:\n", error);
      }
    });
  }

  renderStripeCard() {
    const { stripe } = this;
    const elements = stripe.elements();
    const stripeCard = elements.create("card", { style: CARD_STYLE });
    stripeCard.mount(`#${ELEMENT_IDS.STRIPE_CARD_ELEMENT}`);

    stripeCard.on("change", function (event) {
      let errorElement = document.getElementById(ELEMENT_IDS.STRIPE_CARD_ERROR);
      if (event.error) {
        errorElement.textContent = event.error.message;
      } else {
        errorElement.textContent = "";
      }
    });
    this.stripeCard = stripeCard;
  }

  renderStripeElements() {
    if (!this.stripe) return;
    this.renderStripeCard();
  }

  onCreatedStripeCardToken(cardData, cardName) {
    try {
      const {
        id,
        billing_details: {
          address: { postal_code },
        },
        card: { brand, last4, exp_month, exp_year },
      } = cardData;
      const expiry = `${exp_year}-${("0" + exp_month).slice(-2)}-01`;
      const resultToken = {
        paymentMethod: PAYMENT_METHOD.CARD,
        name: cardName,
        token: id,
        last4,
        type: brand,
        expiry,
        billingZip: postal_code,
      };
      console.log("===== Created Stripe Card Token =====\n", {
        ...resultToken,
        token: obfuscateToken(resultToken.token),
      });
      this.resultToken = resultToken;
    } catch (error) {
      console.error(
        "Internal Error: Failed to add new Card with Stripe:\n",
        error
      );
    }
  }

  onCreatedStripeACHToken(achData, achValues) {
    try {
      const {
        id,
        bank_account: { last4, bank_name },
      } = achData;
      const resultToken = {
        paymentMethod: PAYMENT_METHOD.ACH,
        token: id,
        last4,
        bankName: bank_name,
        name: achValues.achHolderName,
        type: achValues.achAccountType,
      };
      console.log("===== Created Stripe ACH Token =====\n", {
        ...resultToken,
        token: obfuscateToken(resultToken.token),
      });
      this.resultToken = resultToken;
    } catch (error) {
      console.error(
        "Internal Error: Failed to add new ACH with Stripe:\n",
        error
      );
    }
  }

  onCreatedPayixToken(event) {
    try {
      const isAddedWithPayixSuccessfully =
        event.data &&
        event.data.token !== "" &&
        event.data.token !== null &&
        event.data.token !== undefined;
      if (isAddedWithPayixSuccessfully) {
        const {
          token,
          nameOnCard,
          nameOnAch,
          nameOnPad,
          cardBrand,
          expiryDate,
          lastFourDigits,
          billingZip,
          bankName,
        } = event.data;
        const isCard = !!nameOnCard;
        const isACH = !isCard && !!nameOnAch;
        const isPAD = !isCard && !isACH && !!nameOnPad;
        let resultToken;

        if (isCard) {
          const expiry = expiryDate.replace(/(\d{2})\/(\d{4})/, "$2-$1-01");
          resultToken = {
            paymentMethod: PAYMENT_METHOD.CARD,
            name: nameOnCard,
            token,
            last4: lastFourDigits,
            type: cardBrand,
            expiry,
            billingZip,
          };
        } else if (isACH) {
          resultToken = {
            paymentMethod: PAYMENT_METHOD.ACH,
            name: nameOnAch,
            token,
            last4: lastFourDigits,
            bankName,
          };
        } else if (isPAD) {
          resultToken = {
            paymentMethod: PAYMENT_METHOD.PAD,
            name: nameOnPad,
            token,
            last4: lastFourDigits,
            bankName,
          };
        }

        if (resultToken) {
          console.log(
            `===== Created Payix ${resultToken.paymentMethod} Token =====\n`,
            {
              ...resultToken,
              token: obfuscateToken(resultToken.token),
            }
          );
          this.resultToken = resultToken;
        }
      }
    } catch (error) {
      console.error(
        "Internal Error: Failed to add new payment method with Payix:\n",
        error
      );
    }
  }

  validateStripeACHForm() {
    const { value: achHolderName } = document.getElementById(
      ELEMENT_IDS.STRIPE_ACH_HOLDER_NAME
    );
    const { value: achAccountNumber } = document.getElementById(
      ELEMENT_IDS.STRIPE_ACH_ACCOUNT_NUMBER
    );
    const { value: achRoutingNumber } = document.getElementById(
      ELEMENT_IDS.STRIPE_ACH_ROUTING_NUMBER
    );
    const { value: achAccountType } = document.getElementById(
      ELEMENT_IDS.STRIPE_ACH_ACCOUNT_TYPE
    );
    const holderNameErrorElement = document.getElementById(
      ELEMENT_IDS.STRIPE_ACH_HOLDER_NAME_ERROR
    );
    const accountNumberErrorElement = document.getElementById(
      ELEMENT_IDS.STRIPE_ACH_ACCOUNT_NUMBER_ERROR
    );
    const routingNumberErrorElement = document.getElementById(
      ELEMENT_IDS.STRIPE_ACH_ROUTING_NUMBER_ERROR
    );

    let isValidHolderName = false;
    if (!achHolderName) {
      holderNameErrorElement.textContent =
        "Please input Account ownner's fullname";
    } else {
      holderNameErrorElement.textContent = "";
      isValidHolderName = true;
    }

    let isValidAccountNumber = false;
    if (!achAccountNumber) {
      accountNumberErrorElement.textContent = "Please input Account number";
    } else {
      const isValidAccountNumberByStripe = Stripe.bankAccount.validateAccountNumber(
        achAccountNumber,
        STRIPE_ACH_DEFAULT_VALUE.BANK_COUNTRY_CODE
      );
      if (!isValidAccountNumberByStripe) {
        accountNumberErrorElement.textContent =
          "Your Account number is invalid";
      } else {
        accountNumberErrorElement.textContent = "";
        isValidAccountNumber = true;
      }
    }

    let isValidRoutingNumber = false;
    if (!achRoutingNumber) {
      routingNumberErrorElement.textContent = "Please input Routing number";
    } else {
      const isValidRoutingNumberByStripe = Stripe.bankAccount.validateRoutingNumber(
        achRoutingNumber,
        STRIPE_ACH_DEFAULT_VALUE.BANK_COUNTRY_CODE
      );
      if (!isValidRoutingNumberByStripe) {
        routingNumberErrorElement.textContent =
          "Your Routing number is invalid";
      } else {
        routingNumberErrorElement.textContent = "";
        isValidRoutingNumber = true;
      }
    }

    const isValid = [
      isValidHolderName,
      isValidAccountNumber,
      isValidRoutingNumber,
    ].every((isValidValue) => isValidValue);

    if (!isValid) {
      return null;
    }

    return {
      achHolderName,
      achAccountNumber,
      achRoutingNumber,
      achAccountType,
    };
  }

  @action
  onSelectRecord(record) {
    this.selectedRecord = record;
    this.resultToken = null;
    this.setURLs(record);
    this.setSubscriptionIdAndVendor(record);

    if (this.isSupportedStripe()) {
      this.renderStripeElements();
    }
  }

  @action
  onSyncToPaymentService() {
    try {
      const subscriptionId = this.subscriptionId;
      if (!this.resultToken || !this.resultToken.token) {
        throw new Error("Missing Token data");
      }
      const tokenData = {
        ...this.resultToken,
        subscriptionId,
        vendor: this.vendor,
      };
      console.log("===== Sync to Payment Service =====\n", {
        ...tokenData,
        token: obfuscateToken(tokenData.token),
        vendor: this.vendor,
      });
      this.isClickedToSync = true;

      //Getting Add Token Custom Action
      const customActions = this.store
        .peekAll("custom-action")
        .filter((c) => c.name === "Add Token");

      console.log("===== CustomAction is retreived =====\n");

      //Setting the customerActionValues with the tokenData
      customActions[0].fields.forEach((f) => {
        f.customActionValue = tokenData[f.fieldName];
      });

      console.log("===== CustomAction Fields are set =====\n");

      if (!this.selectedRecord) {
        this.selectedRecord = {};
      }

      //Triggering CustomAction
      triggerSmartAction(
        this,
        customActions[0].name,
        [this.selectedRecord],
        tokenData
      );

      console.log("===== CustomAction is triggered =====\n");
    } catch (error) {
      console.error(
        "Internal Error: Failed to sync to Payment Service:\n",
        error
      );
    }
  }

  @action
  async onSubmitToCreateStripeCard(event) {
    event.preventDefault();
    const { value: cardHolderName } = document.getElementById(
      ELEMENT_IDS.STRIPE_CARD_HOLDER_NAME
    );
    let stripeCardNameErrorElement = document.getElementById(
      ELEMENT_IDS.STRIPE_CARD_HOLDER_NAME_ERROR
    );
    if (!cardHolderName) {
      stripeCardNameErrorElement.textContent =
        "Please input card's owner name";
    } else {
      stripeCardNameErrorElement.textContent = "";
      try {
        this.isCreatingStripeCard = true;
        const { value: cardName } = document.getElementById(
          ELEMENT_IDS.STRIPE_CARD_HOLDER_NAME
        );
        const { stripe, stripeCard } = this;
        const { paymentMethod: cardData } = await stripe.createPaymentMethod({
          type: "card",
          card: stripeCard,
          billing_details: { name: cardName },
        });
        this.isCreatingStripeCard = false;
        this.onCreatedStripeCardToken(cardData, cardName);
      } catch (error) {
        let stripeCardErrorElement = document.getElementById(
          ELEMENT_IDS.STRIPE_CARD_ERROR
        );
        stripeCardErrorElement.textContent = error.message;
      }
    }
  }

  @action
  onSubmitToCreateStripeACH(event) {
    event.preventDefault();
    const achValues = this.validateStripeACHForm();
    if (!achValues) {
      return;
    }

    try {
      this.isCreatingStripeACH = true;
      Stripe.bankAccount.createToken(
        {
          country: STRIPE_ACH_DEFAULT_VALUE.BANK_COUNTRY_CODE,
          currency: STRIPE_ACH_DEFAULT_VALUE.CURRENCY,
          account_holder_name: achValues.achHolderName,
          account_number: achValues.achAccountNumber,
          routing_number: achValues.achRoutingNumber,
          account_holder_type: achValues.achAccountType,
        },
        (status, response) => {
          if (response.error) {
            throw error;
          }
          this.isCreatingStripeACH = false;
          this.onCreatedStripeACHToken(response, achValues);
        }
      );
    } catch (error) {
      let stripeCardErrorElement = document.getElementById(
        ELEMENT_IDS.STRIPE_CARD_ERROR
      );
      stripeCardErrorElement.textContent = error.message;
    }
  }
}

Here it is

// v1.6.1: Update to new syntax
import Component from "@glimmer/component";
import { scheduleOnce } from "@ember/runloop";
import { inject as service } from "@ember/service";
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
import { loadExternalJavascript, triggerSmartAction } from 'client/utils/smart-view-utils';

const CARD_STYLE = {
  base: {
    color: "#54BD7E",
    fontFamily: '"Helvetica Neue", Helvetica, sans-serif',
    fontSmoothing: "antialiased",
    fontSize: "14px",
    "::placeholder": {
      color: "#aab7c4", // --color-beta-on-surface_light
    },
  },
  invalid: {
    color: "#FB6669",
    iconColor: "#FB6669",
  },
};

const ELEMENT_IDS = {
  STRIPE_CARD_ELEMENT: "stripe-card-element",
  STRIPE_CARD_ERROR: "stripe-card-errors",
  STRIPE_CARD_HOLDER_NAME: "stripe-card-holder-name",
  STRIPE_CARD_HOLDER_NAME_ERROR: "stripe-card-holder-name-error",

  STRIPE_ACH_HOLDER_NAME: "stripe-ach-holder-name",
  STRIPE_ACH_ACCOUNT_NUMBER: "stripe-ach-account-number",
  STRIPE_ACH_ROUTING_NUMBER: "stripe-ach-routing-number",
  STRIPE_ACH_ACCOUNT_TYPE: "stripe-ach-account-type",
  STRIPE_ACH_HOLDER_NAME_ERROR: "stripe-ach-holder-name-error",
  STRIPE_ACH_ACCOUNT_NUMBER_ERROR: "stripe-ach-account-number-error",
  STRIPE_ACH_ROUTING_NUMBER_ERROR: "stripe-ach-routing-number-error",
};

const PAYMENT_METHOD = {
  CARD: "CARD",
  ACH: "ACH",
  PAD: "PAD",
};
const PAYMENT_METHOD_VENDOR = {
  STRIPE: "stripe",
  PAYIX: "payix",
};
const STRIPE_ACH_DEFAULT_VALUE = {
  BANK_COUNTRY_CODE: "US",
  CURRENCY: "USD",
};

const obfuscateToken = (token) =>
  token.includes("_")
    ? token.replace(/^(.*)_((.{3}).*(.{3}))$/, "$1_$3***$4")
    : token.replace(/^(.{3})(.*)(.{3})$/, "$1***$3");

export default class extends Component {
  @service() store;

  @tracked selectedRecord = null;
  @tracked resultToken = null;

  @tracked stripe = null;
  @tracked isLoadedStripe = false;
  @tracked stripeCard = null;
  @tracked isCreatingStripeCard = false;
  @tracked isCreatingStripeACH = false;
  @tracked isClickedToSync = false;
  @tracked stripePublicKey = null;
  @tracked payixIframeCardAchURL = null;
  @tracked payixIframePadURL = null;
  @tracked subscriptionId = null;
  @tracked vendor = null;
  @tracked loanNumber = null;

  constructor(...args) {
    super(...args);

    console.log(`===== Smart view: v1.6.1: Update to new syntax =====`);
  }

  async didInsertElement() {
    await this.setDefaultSelectedRecord();

    this.onCreatedPayixToken = this.onCreatedPayixToken.bind(this);
    window.addEventListener("message", this.onCreatedPayixToken);
  }

  willRemoveElement() {
    window.removeEventListener("message", this.onCreatedPayixToken);
  }

  @action
  onRecordsChange() {
    this.setDefaultSelectedRecord();
  }

  isSupportedStripe() {
    return this.vendor === PAYMENT_METHOD_VENDOR.STRIPE;
  }

  setURLs(data) {
    this.payixIframeCardAchURL = data["forest-payix_iframe_card_ach_url"];
    this.payixIframePadURL = data["forest-payix_iframe_pad_url"];
    this.stripePublicKey = data["forest-stripe_public_key"];
  }

  setSubscriptionIdAndVendor(record) {
    if (this.args.records) {
      this.subscriptionId = record["forest-subscription"]["id"];
      this.vendor = record["forest-subscription"]["forest-vendor"];
      this.loanNumber = record["forest-subscription"]["forest-loanNumber"];
    }
    if (!this.args.records) {
      this.subscriptionId = record["forest-id"];
      this.vendor = record["forest-vendor"];
      this.loanNumber = record["forest-loanNumber"];
    }
  }

  setDefaultSelectedRecord() {
    // I'm pretty sure this if is useless because `this.args.records.recordData` is always undefined 🤔
    if (this.args.records && this.args.records.recordData) {
      this.subscriptionId = this.args.records.recordData.id;
      this.vendor = PAYMENT_METHOD_VENDOR.STRIPE;
      if (this.args.records.recordData.__data) {
        this.setURLs(this.args.records.recordData.__data);
        //Setting the vendor here again as if it's a Canadian User
        //The Vendor should be Payix not Stripe
        if (this.args.records.recordData.__data["forest-vendor"]) {
          this.vendor = this.args.records.recordData.__data["forest-vendor"];
        }
      }
      if (!this.stripe) this.loadPlugins();
    }

    if (!this.selectedRecord) {
      if (this.args.records && this.args.records.firstObject) {
        this.selectedRecord = this.args.records.firstObject;
        this.setURLs(this.args.records.firstObject);
        this.setSubscriptionIdAndVendor(this.args.records.firstObject);
        if (!this.stripe) this.loadPlugins();
      }

      if (!this.args.records) {
        const regex = /sub_[\w-]+/;
        const url = window.location.href
        const hasSubscriptionId = url.match(regex)
        if (hasSubscriptionId) {
          const subscriptionId = url.match(regex)[0]
          this.store.findRecord('forest-subscription', subscriptionId)
            .then((record) => {
              this.setURLs(record)
              this.setSubscriptionIdAndVendor(record)
              if (!this.stripe) this.loadPlugins();
            })
        }
      }
    }
  }

  loadPlugins() {
    scheduleOnce("afterRender", this, async () => {
      try {
        const stripePublicKey = this.stripePublicKey;
        await loadExternalJavascript('//js.stripe.com/v2/');
        await loadExternalJavascript('//js.stripe.com/v3/');

        Stripe.setPublishableKey(stripePublicKey); // v2
        const stripe = Stripe(stripePublicKey); // v3
        this.stripe = stripe;
        this.isLoadedStripe = true;

        if (this.isSupportedStripe()) {
          this.renderStripeElements();
        }
      } catch (error) {
        console.error("Internal Error: Failed to load Stripe:\n", error);
      }
    });
  }

  renderStripeCard() {
    const { stripe } = this;
    const elements = stripe.elements();
    const stripeCard = elements.create("card", { style: CARD_STYLE });
    stripeCard.mount(`#${ELEMENT_IDS.STRIPE_CARD_ELEMENT}`);

    stripeCard.on("change", function (event) {
      let errorElement = document.getElementById(ELEMENT_IDS.STRIPE_CARD_ERROR);
      if (event.error) {
        errorElement.textContent = event.error.message;
      } else {
        errorElement.textContent = "";
      }
    });
    this.stripeCard = stripeCard;
  }

  renderStripeElements() {
    if (!this.stripe) return;
    this.renderStripeCard();
  }

  onCreatedStripeCardToken(cardData, cardName) {
    try {
      const {
        id,
        billing_details: {
          address: { postal_code },
        },
        card: { brand, last4, exp_month, exp_year },
      } = cardData;
      const expiry = `${exp_year}-${("0" + exp_month).slice(-2)}-01`;
      const resultToken = {
        paymentMethod: PAYMENT_METHOD.CARD,
        name: cardName,
        token: id,
        last4,
        type: brand,
        expiry,
        billingZip: postal_code,
      };
      console.log("===== Created Stripe Card Token =====\n", {
        ...resultToken,
        token: obfuscateToken(resultToken.token),
      });
      this.resultToken = resultToken;
    } catch (error) {
      console.error(
        "Internal Error: Failed to add new Card with Stripe:\n",
        error
      );
    }
  }

  onCreatedStripeACHToken(achData, achValues) {
    try {
      const {
        id,
        bank_account: { last4, bank_name },
      } = achData;
      const resultToken = {
        paymentMethod: PAYMENT_METHOD.ACH,
        token: id,
        last4,
        bankName: bank_name,
        name: achValues.achHolderName,
        type: achValues.achAccountType,
      };
      console.log("===== Created Stripe ACH Token =====\n", {
        ...resultToken,
        token: obfuscateToken(resultToken.token),
      });
      this.resultToken = resultToken;
    } catch (error) {
      console.error(
        "Internal Error: Failed to add new ACH with Stripe:\n",
        error
      );
    }
  }

  onCreatedPayixToken(event) {
    try {
      const isAddedWithPayixSuccessfully =
        event.data &&
        event.data.token !== "" &&
        event.data.token !== null &&
        event.data.token !== undefined;
      if (isAddedWithPayixSuccessfully) {
        const {
          token,
          nameOnCard,
          nameOnAch,
          nameOnPad,
          cardBrand,
          expiryDate,
          lastFourDigits,
          billingZip,
          bankName,
        } = event.data;
        const isCard = !!nameOnCard;
        const isACH = !isCard && !!nameOnAch;
        const isPAD = !isCard && !isACH && !!nameOnPad;
        let resultToken;

        if (isCard) {
          const expiry = expiryDate.replace(/(\d{2})\/(\d{4})/, "$2-$1-01");
          resultToken = {
            paymentMethod: PAYMENT_METHOD.CARD,
            name: nameOnCard,
            token,
            last4: lastFourDigits,
            type: cardBrand,
            expiry,
            billingZip,
          };
        } else if (isACH) {
          resultToken = {
            paymentMethod: PAYMENT_METHOD.ACH,
            name: nameOnAch,
            token,
            last4: lastFourDigits,
            bankName,
          };
        } else if (isPAD) {
          resultToken = {
            paymentMethod: PAYMENT_METHOD.PAD,
            name: nameOnPad,
            token,
            last4: lastFourDigits,
            bankName,
          };
        }

        if (resultToken) {
          console.log(
            `===== Created Payix ${resultToken.paymentMethod} Token =====\n`,
            {
              ...resultToken,
              token: obfuscateToken(resultToken.token),
            }
          );
          this.resultToken = resultToken;
        }
      }
    } catch (error) {
      console.error(
        "Internal Error: Failed to add new payment method with Payix:\n",
        error
      );
    }
  }

  validateStripeACHForm() {
    const { value: achHolderName } = document.getElementById(
      ELEMENT_IDS.STRIPE_ACH_HOLDER_NAME
    );
    const { value: achAccountNumber } = document.getElementById(
      ELEMENT_IDS.STRIPE_ACH_ACCOUNT_NUMBER
    );
    const { value: achRoutingNumber } = document.getElementById(
      ELEMENT_IDS.STRIPE_ACH_ROUTING_NUMBER
    );
    const { value: achAccountType } = document.getElementById(
      ELEMENT_IDS.STRIPE_ACH_ACCOUNT_TYPE
    );
    const holderNameErrorElement = document.getElementById(
      ELEMENT_IDS.STRIPE_ACH_HOLDER_NAME_ERROR
    );
    const accountNumberErrorElement = document.getElementById(
      ELEMENT_IDS.STRIPE_ACH_ACCOUNT_NUMBER_ERROR
    );
    const routingNumberErrorElement = document.getElementById(
      ELEMENT_IDS.STRIPE_ACH_ROUTING_NUMBER_ERROR
    );

    let isValidHolderName = false;
    if (!achHolderName) {
      holderNameErrorElement.textContent =
        "Please input Account ownner's fullname";
    } else {
      holderNameErrorElement.textContent = "";
      isValidHolderName = true;
    }

    let isValidAccountNumber = false;
    if (!achAccountNumber) {
      accountNumberErrorElement.textContent = "Please input Account number";
    } else {
      const isValidAccountNumberByStripe = Stripe.bankAccount.validateAccountNumber(
        achAccountNumber,
        STRIPE_ACH_DEFAULT_VALUE.BANK_COUNTRY_CODE
      );
      if (!isValidAccountNumberByStripe) {
        accountNumberErrorElement.textContent =
          "Your Account number is invalid";
      } else {
        accountNumberErrorElement.textContent = "";
        isValidAccountNumber = true;
      }
    }

    let isValidRoutingNumber = false;
    if (!achRoutingNumber) {
      routingNumberErrorElement.textContent = "Please input Routing number";
    } else {
      const isValidRoutingNumberByStripe = Stripe.bankAccount.validateRoutingNumber(
        achRoutingNumber,
        STRIPE_ACH_DEFAULT_VALUE.BANK_COUNTRY_CODE
      );
      if (!isValidRoutingNumberByStripe) {
        routingNumberErrorElement.textContent =
          "Your Routing number is invalid";
      } else {
        routingNumberErrorElement.textContent = "";
        isValidRoutingNumber = true;
      }
    }

    const isValid = [
      isValidHolderName,
      isValidAccountNumber,
      isValidRoutingNumber,
    ].every((isValidValue) => isValidValue);

    if (!isValid) {
      return null;
    }

    return {
      achHolderName,
      achAccountNumber,
      achRoutingNumber,
      achAccountType,
    };
  }

  @action
  onSelectRecord(record) {
    this.selectedRecord = record;
    this.resultToken = null;
    this.setURLs(record);
    this.setSubscriptionIdAndVendor(record);

    if (this.isSupportedStripe()) {
      this.renderStripeElements();
    }
  }

  @action
  onSyncToPaymentService() {
    try {
      const subscriptionId = this.subscriptionId;
      if (!this.resultToken || !this.resultToken.token) {
        throw new Error("Missing Token data");
      }
      const tokenData = {
        ...this.resultToken,
        subscriptionId,
        vendor: this.vendor,
      };
      console.log("===== Sync to Payment Service =====\n", {
        ...tokenData,
        token: obfuscateToken(tokenData.token),
        vendor: this.vendor,
      });
      this.isClickedToSync = true;

      //Getting Add Token Custom Action
      const customAction = this.store
        .peekAll("custom-action")
        .find((c) => c.name === "Add Token");

      console.log("===== CustomAction is retreived =====\n");

      //Setting the customerActionValues with the tokenData
      customAction.fields.forEach((f) => {
        f.customActionValue = tokenData[f.fieldName];
      });

      console.log("===== CustomAction Fields are set =====\n");

      if (!this.selectedRecord) {
        this.selectedRecord = {};
      }

      //Triggering CustomAction
      triggerSmartAction(
        this,
        customAction.collection,
        customAction.name,
        [this.selectedRecord],
        () => {},
        tokenData,
      );

      console.log("===== CustomAction is triggered =====\n");
    } catch (error) {
      console.error(
        "Internal Error: Failed to sync to Payment Service:\n",
        error
      );
    }
  }

  @action
  async onSubmitToCreateStripeCard(event) {
    event.preventDefault();
    const { value: cardHolderName } = document.getElementById(
      ELEMENT_IDS.STRIPE_CARD_HOLDER_NAME
    );
    let stripeCardNameErrorElement = document.getElementById(
      ELEMENT_IDS.STRIPE_CARD_HOLDER_NAME_ERROR
    );
    if (!cardHolderName) {
      stripeCardNameErrorElement.textContent =
        "Please input card's owner name";
    } else {
      stripeCardNameErrorElement.textContent = "";
      try {
        this.isCreatingStripeCard = true;
        const { value: cardName } = document.getElementById(
          ELEMENT_IDS.STRIPE_CARD_HOLDER_NAME
        );
        const { stripe, stripeCard } = this;
        const { paymentMethod: cardData } = await stripe.createPaymentMethod({
          type: "card",
          card: stripeCard,
          billing_details: { name: cardName },
        });
        this.isCreatingStripeCard = false;
        this.onCreatedStripeCardToken(cardData, cardName);
      } catch (error) {
        let stripeCardErrorElement = document.getElementById(
          ELEMENT_IDS.STRIPE_CARD_ERROR
        );
        stripeCardErrorElement.textContent = error.message;
      }
    }
  }

  @action
  onSubmitToCreateStripeACH(event) {
    event.preventDefault();
    const achValues = this.validateStripeACHForm();
    if (!achValues) {
      return;
    }

    try {
      this.isCreatingStripeACH = true;
      Stripe.bankAccount.createToken(
        {
          country: STRIPE_ACH_DEFAULT_VALUE.BANK_COUNTRY_CODE,
          currency: STRIPE_ACH_DEFAULT_VALUE.CURRENCY,
          account_holder_name: achValues.achHolderName,
          account_number: achValues.achAccountNumber,
          routing_number: achValues.achRoutingNumber,
          account_holder_type: achValues.achAccountType,
        },
        (status, response) => {
          if (response.error) {
            throw error;
          }
          this.isCreatingStripeACH = false;
          this.onCreatedStripeACHToken(response, achValues);
        }
      );
    } catch (error) {
      let stripeCardErrorElement = document.getElementById(
        ELEMENT_IDS.STRIPE_CARD_ERROR
      );
      stripeCardErrorElement.textContent = error.message;
    }
  }
}
{{!-- v1.6.0: Add ability to fetch loanNumber without subscriptionToken record --}}

<div class="wrapper" id="smv_add_token" {{did-update this.onRecordsChange @records}} ...attributes>
	<div class="wrapper-list">
		<div class="sub-tok-list-table">
			<div class="sub-tok-list-heading">
				<div class="sub-tok-list-heading-row">
					<div class="column sub-tok-ids">Subscription ID/Token ID</div>
					<div class="column loan-number">Loan number</div>
					<div class="column pm-info">Payment method info<br>(Vendor/Type/Last 4)</div>
				</div>
			</div>
			<div class="sub-tok-list-body">
				{{#each @records as |record|}}
					<div
						class="sub-tok-list-row {{if (eq this.selectedRecord record) 'selected'}}"
						{{on 'click' (fn this.onSelectRecord record)}}
					>
						<div class="column sub-tok-ids">
							<span class="value">
								{{record.forest-subscription.id}}
								<br>
								{{record.forest-token.id}}
								<br>
								{{#if record.forest-isPrimary}}
									<div class="primary-token">primary</div>
								{{/if}}
							</span>
						</div>
						<div class="column loan-number">
							<span class="value">{{record.forest-subscription.forest-loanNumber}}</span>
						</div>
						<div class="column pm-info">
							<span class="value">{{record.forest-subscription.forest-vendor}}</span>
							<span class="value">&nbsp;/&nbsp;{{record.forest-token.forest-paymentMethod}}&nbsp;/&nbsp;</span>
							<span class="value">{{record.forest-token.forest-last4}}</span>
						</div>
					</div>
				{{/each}}
			</div>
		</div>

		<DataDisplay::Table::TableFooter
			@collection={{@collection}}
			@viewList={{@viewList}}
			@records={{@records}}
			@currentPage={{@currentPage}}
			@numberOfPages={{@numberOfPages}}
			@recordsCount={{@recordsCount}}
			@isLoading={{@isLoading}}
			@fetchRecords={{@fetchRecords}}
		/>
	</div>

	<div class="wrapper-forms">
		{{#if this.stripe}}
			<h2 class="forms-title">New payment method for Loan: <b>{{this.loanNumber}}</b></h2>
		{{else}}
			<h2 class="forms-title">Getting ready ...</h2>
		{{/if}}

		{{#if this.resultToken}}
			{{#if (eq this.resultToken.paymentMethod 'CARD')}}
				<div class="wrapper-result">
					<div class="result-row">
						<div class="label">Name</div>
						<div class="value">{{this.resultToken.name}}</div>
					</div>
					<div class="result-row">
						<div class="label">Card brand</div>
						<div class="value">{{this.resultToken.type}}</div>
					</div>
					<div class="result-row">
						<div class="label">Last 4 digits</div>
						<div class="value">{{this.resultToken.last4}}</div>
					</div>
					<div class="result-row">
						<div class="label">Expiry date</div>
						<div class="value">{{this.resultToken.expiry}}</div>
					</div>
					{{#if this.resultToken.billingZip}}
					<div class="result-row">
						<div class="label">Billing zip</div>
						<div class="value">{{this.resultToken.billingZip}}</div>
					</div>
					{{/if}}
				</div>
			{{else}}
				<div class="wrapper-result">
					<div class="result-row">
						<div class="label">Bank name</div>
						<div class="value">{{this.resultToken.bankName}}</div>
					</div>
					<div class="result-row">
						<div class="label">Account owner's full name</div>
						<div class="value">{{this.resultToken.name}}</div>
					</div>
					<div class="result-row">
						<div class="label">Last 4 digits</div>
						<div class="value">{{this.resultToken.last4}}</div>
					</div>
					{{#if this.resultToken.type}}
					<div class="result-row">
						<div class="label">Account type</div>
						<div class="value">{{this.resultToken.type}}</div>
					</div>
					{{/if}}
				</div>
			{{/if}}
			{{#if this.isClickedToSync}}
				<h2 class="warning">Please refresh your browser to see new update after finishing the synchronization successfully.</h2>
			{{else}}
				<button class="btn btn-sync" {{on 'click' (fn this.onSyncToPaymentService)}}>
					Upload new Payment method to Payment service
				</button>
			{{/if}}
		{{else}}
			<div data-loaded="{{this.isLoadedStripe}}"
				data-visibility="{{if (eq this.vendor 'stripe') true false}}"
				class="wrapper-stripe-forms">
				{{!-- STRIPE CARD --}}
				<form id="stripe-form" data-visibility="{{if this.resultToken false true}}"
					onsubmit={{fn this.onSubmitToCreateStripeCard}}>
					<div class="stripe-form-row">
						<input id="stripe-card-holder-name" class="form-input" placeholder="Name" />
						<div id="stripe-card-holder-name-error" class="stripe-error" role="alert" />
					</div>
					<div class="stripe-form-row">
						<div id="stripe-card-element" />
						<div id="stripe-card-errors" class="stripe-error" role="alert" />
					</div>
					<div class="form-actions">
						{{#if this.isCreatingStripeCard}}
						<div class="wrapper-dots">
							<div class="dot"></div>
							<div class="dot"></div>
							<div class="dot"></div>
						</div>
						{{else}}
						<button type="submit" class="btn btn-stripe-add">Add Card</button>
						{{/if}}
					</div>
				</form>

				{{!-- STRIPE ACH --}}
				<form class="stripe-ach-form" onsubmit={{fn this.onSubmitToCreateStripeACH }}>
					<div class="stripe-ach-form-cols-wrapper">
						<div class="stripe-ach-form-col col-inputs">
							<div class="ach-form-input">
								<input type="text" id="stripe-ach-holder-name" placeholder="Account owner full name">
								<div id="stripe-ach-holder-name-error" class="stripe-error" role="alert" />
							</div>
							<div class="ach-form-input">
								<input type="text" id="stripe-ach-account-number" placeholder="Account number">
								<div id="stripe-ach-account-number-error" class="stripe-error" role="alert" />
							</div>
							<div class="ach-form-input">
								<input type="text" id="stripe-ach-routing-number" placeholder="Routing number">
								<div id="stripe-ach-routing-number-error" class="stripe-error" role="alert" />
							</div>
					</div>

					<div class="stripe-ach-form-col col-selectors">
						<div class="ach-form-selector">
							<label for="bank_country">Bank country</label>
							<select id="stripe-ach-bank-country" name="bank_country" disabled>
								<option value="US" selected>United States</option>
							</select>
						</div>
						<div class="ach-form-selector">
							<label for="currency">Currency</label>
							<select id="stripe-ach-currency" name="currency" disabled>
								<option value="USD" selected>USD</option>
							</select>
						</div>
						<div class="ach-form-selector">
							<label for="account_type">Account type</label>
							<select id="stripe-ach-account-type" name="account_type">
								<option value="individual">Individual</option>
								<option value="company">Company</option>
							</select>
						</div>
					</div>
					</div>
					<div class="form-actions">
						{{#if this.isCreatingStripeACH}}
						<div class="wrapper-dots">
							<div class="dot"></div>
							<div class="dot"></div>
							<div class="dot"></div>
						</div>
						{{else}}
						<button disabled={{this.isCreatingStripeACH}} type="submit" class="btn btn-stripe-add">Add ACH</button>
						{{/if}}
					</div>
				</form>

			</div>


			{{#if (eq this.vendor 'payix')}}
				<div class="wrapper-payix-iframes">
					<div class="warning-pad">PAD only supports for Canadian</div>
					<div class="wrapper-iframes">
						<iframe src="{{this.payixIframeCardAchURL}}" />
						<iframe src="{{this.payixIframePadURL}}" />
					</div>
				</div>
			{{/if}}
		{{/if}}
	</div>
</div>

@vince
Still seeing this error:
Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'findBy') at e.triggerSmartAction
So, when we run the following in the component

triggerSmartAction(
        this,
        customAction.collection,
        customAction.name,
        [this.selectedRecord],
        () => {},
        tokenData,
      );

it triggers

e.triggerSmartAction = async function(e,i,r,a,l = () => {},s = null,c = !1) {
  const u = (0, t.isArray)(a) ? a : [a];
  const d = (await Promise.resolve(i.customActions)).findBy("name", r);
  o(d, s),
    d
      ? l(n(e).triggerCustomAction(d, u, s, c))
      : (console.error(`Could not find the Smart Action: ${r}`), l());
};

But Promise.resolve(customAction.collection.customActions) resolves to undefined.

Thoughts?

It might be due to the missing await in front of collection :thinking:

await triggerSmartAction(
        this,
        await customAction.collection,
        customAction.name,
        [this.selectedRecord],
        () => {},
        tokenData,
      );

The editor won’t even save that…

unknown: Unexpected reserved word 'await'. (474:8) 472 | triggerSmartAction( 473 | this, > 474 | await customAction.collection(), | ^ 475 | customAction.name, 476 | [this.selectedRecord], 477 | () => {},

Any other ideas?

it’s because you need to make your function async :wink:

sorry, its my first day :roll_eyes:

@vince there are still issues here, though. Now that we’ve got the smart action triggering with the new syntax, the values are not in the request to the smart action as before (at all, as far as I can see)

The smart view calls this smart action:

router.post(uris.addToken, ensureAuthenticated, async (req, res) => {
  
  console.log(req.body.data.attributes.values); //logs '{}'

  const { subscriptionId, ...tokenData } = req.body.data.attributes.values;
  logger.info(
    `Request to add new payment method for subscription: id='${subscriptionId}'`,
    { requestID: req.id }
  );

  try {
    await subscription.addToken(subscriptionId, tokenData);
  } catch (e) {
    const message = `Add new payment method failed: ${e.message}`;
    logger.error(message, { requestID: req.id });
    return res.status(400).send({ error: message });
  }

  logger.info("Add new payment method successfully", { requestID: req.id });
  return res.send({
    success:
      "Add new payment method successfully. Please refresh to see new update."
  });
});

Which then calls

async addToken(subscriptionId, tokenData) {
    
    console.log(subscriptionId, tokenData); //logs 'undefined, {}'

    const subscription = await this.subscriptionService.findByID(
      subscriptionId
    );
    if (_.isNil(subscription)) {
      throw new Error("Cannot find subscription");
    }

    const customer = await subscription.getCustomer();
    return this.paymentApiService.addToken({
      ...tokenData,
      scratchBorrowerId: customer.scratchBorrowerId
    });
  }

Yet the call to the smart action from the smart view should have all of the data as expected

      console.log({resultToken:this.resultToken, subscriptionId, vendor:this.vendor})
      console.log(tokenData)
      //both of these log the values as expected
      
      //Triggering CustomAction
      await triggerSmartAction(
        this,
        await customAction.collection,
        customAction.name,
        [this.selectedRecord],
        () => {},
        tokenData,
      );

That certainly means you are sending unknown values.
Values should have a field attached to it.
So normally you can’t sent a vendor attribute in the payload if you don’t have a vendor field in your smart action.
Could you share the declaration of your smart action on your backend ?

@vince ok, here it is.

The last time that any of these related method or declarations were touched was 2021/08/03 which was a small tweak, most of it has been live since 2020/12/08. This smart view is used numerous times per day. It all worked fine until it just stopped a couple of weeks ago.

The values were never unknown before. So what did you change on your end that is causing this issue?

{
      name: "Add Token",
      // Type "single" need to have a record to be selected meanwhile we only trigger this action via a Smart view.
      // Type "global" will not prevent the trigger in Smart view
      // For more information, please read at https://docs.forestadmin.com/documentation/reference-guide/actions#triggering-different-types-of-actions
      type: "global",
      endpoint: "/forest/actions/subscription-token/add-token",
      fields: [
        { field: "billingZip" },
        { field: "expiry" },
        { field: "last4" },
        { field: "name" },
        { field: "paymentMethod" },
        { field: "subscriptionId" },
        { field: "token" },
        { field: "type" },
        { field: "vendor" },
        { field: "bankName" }
      ]
    }

Like I said you were using a private service that should not have been used.
But this service was deprecated.
We removed that service and clean the code on our side