ODIN 支付@exerp/odin-dropin(中文(简体) )
这个软件包为将ODIN Pay Drop-in组件整合到网络应用程序提供了公共外观. 它简化了与 Stencil 网络基本组件和 OdinPay.js 图书馆.
示例:卡片支付表
|
示例:银行 支付表
|
安装
# Using pnpm
pnpm add @exerp/odin-dropin
# Using npm
npm install @exerp/odin-dropin
# Using yarn
yarn add @exerp/odin-dropin
API 文档
此套件的主要导出是 OdinDropin 课.
导入
// ES Module import
import {
OdinDropin,
type LogLevel,
type OdinDropinInitializationParams, // For typing params
type OdinFieldValidationEvent, // For the Validation callback
type OdinV2ThemeConfig, // For the theme config
type OdinSubmitPayload, // For onSubmit callback
type OdinPayErrorPayload // For onError callback
} from '@exerp/odin-dropin';
// For UMD builds (if included via <script> tag),
// it should be available as a global variable `OdinDropin`.
// const odinDropinInstance = new window.OdinDropin({...});
new OdinDropin(params)
初始化 ODIN 滴入控制器的新实例 .
参数:
params(单位:千美元)OdinDropinInitializationParams, (中文(简体) ), 需求: 包含配置参数的对象:odinPublicToken(单位:千美元)string, (中文(简体) ), 需求:从你的后端安全地获得的短命公共标志(它从ODIN中获取)/auth2/public-token结束点). 此符号用于验证和初始化基础OdinPay.js图书馆.countryCode(单位:千美元)'US' | 'CA', (中文(简体) ), 需求:指定应配置付款表的国家。 这影响到邮政编码域标注和验证规则OdinPay.js。 。 。 .paymentMethodType?(单位:千美元)'CARD' | 'BANK_ACCOUNT', (中文(简体) ), 可选,默认 :'CARD':指定要制作的付款表的类型.'CARD':提交信用卡支付表.'BANK_ACCOUNT': 提供银行账户支付表(美国ACH,加拿大EFT,由OdinPay支持).
billingFieldsConfig?(单位:千美元)BillingFieldsConfig, (中文(简体) ), 可选:一个用于配置收费地址字段和其他标准字段的能见度并自定义外观的对象.- 信用卡使用:
- 对于可选字段(例如,
name, (中文(简体) ),addressLine1, (中文(简体) ),city, (中文(简体) ),state, (中文(简体) ),country, (中文(简体) ),emailAddress, (中文(简体) ),phoneNumber:- 关闭字段密钥: 字段将不会显示 .
- 设定为
true: 字段将显示其默认标签和占位符. - 设定为
FieldCustomization对象(例如,{ label: 'Your Name', placeholder: 'Full Name Here' }:字段将显示指定的自定义标签和/或占位符。 标签/位置持有人的空字符串将有效使用默认值 .
- 对于总是构成结构的一部分但其标签/位置持有者可定制的字段(
postalCode, (中文(简体) ),cardInformation标签 :- 关闭字段密钥或提供空键
FieldCustomization对象:将使用默认标签/位置持有人. - 设定为
FieldCustomization对象:将使用指定的自定义标签和/或占位符。 (说明:cardInformation其内部占位符无法通过此来配置).
- 关闭字段密钥或提供空键
- 对于可选字段(例如,
- 示例
billingFieldsConfig: { name: true, // Enable 'Name on Card' with default texts addressLine1: { label: 'Street Address', placeholder: 'E.g., 123 Main St' }, city: { label: 'Town/City' }, // Custom label, default placeholder postalCode: { placeholder: 'ZIP / Postal Code' } // Custom placeholder for postal code // Other fields like addressLine2, state, country etc., will not be shown. } - 银行用法: 银行账户
name输入billingFieldsConfig(如果定制或启用为true)指"账户持有者名". 其他可选计费字段( E)addressLine1, (中文(简体) ),postalCode也可以配置,并将与银行账户细节一起收集。 田园accountNumber, (中文(简体) ),bankAccountType, (中文(简体) ),routingNumber(美国),transitNumber(加拿大),以及institutionNumber(CA)是银行账户表格的核心,其标签/位置持有者也可以使用其各自的密钥进行定制billingFieldsConfig(例如,accountNumber: { label: 'Bank Account No.' }) (中文(简体) ). - 示例( 银行账户字段定制):
billingFieldsConfig: { name: { label: 'Full Name on Account' }, // Custom label for Account Holder Name accountNumber: { placeholder: 'Enter your bank account number' }, // addressLine1: true, // Optionally collect address } - (参见导出)
BillingFieldsConfig财务报告和审定财务报表FieldCustomization详细类型。 )
- 信用卡使用:
logLevel?(单位:千美元)LogLevel, (中文(简体) ), 可选,默认 :'WARN': 控制通过 Drop- in 组件(外观和核心部分)登录到浏览器控制台上的信件的动词. 设置一个关卡可以显示该关卡和更严重的信息.- 可能的值 :
'NONE', (中文(简体) ),'ERROR', (中文(简体) ),'WARN', (中文(简体) ),'INFO', (中文(简体) ),'DEBUG'。 。 。 . 'NONE': 没有显示日志 .'ERROR':仅显示关键错误.'WARN': 显示错误和警告.'INFO': 显示错误、警告和信息信息(例如生命周期事件).'DEBUG':显示所有信件,包括详细的调试信息.
- 可能的值 :
onChangeValidation?(单位:千美元)(event: OdinFieldValidationEvent) => void, (中文(简体) ), 可选: OdinPay.js v2在用户与输入字段互动时引用的可选回调函数. 它为单个字段提供了实时验证状态,允许动态UI更新或自定义逻辑.- 有效载荷
OdinFieldValidationEvent:财产 类型 说明 typestring事件的类型,一般 "FIELD_VALIDATION"。 。 。 .fieldNamestring正在验证的字段名称( 例如 ), "cardNumber", (中文(简体) ),"postalCode") (中文(简体) ).selectorstring向OdinPay.js提供的外地集装箱CSS选择器. isValidboolean表示根据 OdinPay.js 验证规则该字段目前是否有效 . errorCode?string如果字段无效则错误代码( 例如, ), "INVALID", (中文(简体) ),"REQUIRED")可选. - 示例使用法:
new OdinDropin({ // ... other params onChangeValidation: (event) => { console.log('Field Validation:', event.fieldName, 'Is Valid:', event.isValid, 'Error Code:', event.errorCode); // Example: Add/remove a custom error class to the field's parent const fieldContainer = document.querySelector(event.selector)?.closest('.my-field-wrapper'); if (fieldContainer) { if (event.isValid) { fieldContainer.classList.remove('has-error'); } else { fieldContainer.classList.add('has-error'); } } } });
- 有效载荷
onSubmit(单位:千美元)(result: OdinSubmitPayload) => void, (中文(简体) ), 需求: 用户成功提交支付表时将引用的回调函数OdinPay.js返回付款方法符号.- 有效载荷
OdinSubmitPayload: 包含:paymentMethodId(单位:千美元)string:由ODIN所生成的象征性支付方法标识符. 将此ID发送到您的后端服务器以进行支付动作 .paymentMethodType(单位:千美元)'CARD' | 'BANK_ACCOUNT':一个字符串,表示已标出支付方法的类型. 这有助于解释details对象.billingInformation?(单位:千美元)OdinPayBillingInformation, (中文(简体) ), 可选:一个包含用户输入的计费细节的对象,如果设置并提交了任何计费字段. 其结构是:
如果配置了相应的字段, 将显示密钥 。 空字符串值表示字段被配置但被用户留下空白. 如果不配置一个字段,其密钥将被省略 。 这个interface OdinPayBillingInformation { name?: string; emailAddress?: string; phoneNumber?: string; address?: { addressLine1?: string; addressLine2?: string; city?: string; state?: string; postalCode?: string; country?: string; }; }address如果没有配置地址字段,对象本身将被省略.details?(单位:千美元)CardPaymentMethodDetails | AchPaymentMethodDetails | ..., (中文(简体) ), 可选: 包含特定细节的物体:paymentMethodType。 。 。 .- 若为
paymentMethodType实值'CARD'这将是一个CardPaymentMethodDetails对象 :interface CardPaymentMethodDetails { cardBrand?: string; // e.g., "VISA", "MASTERCARD" last4?: string; // Last four digits of the card number maskedAccountNumber?: string; // The masked card number (e.g., "************1111") expirationDate?: string; // e.g., "12/2025" binDetails?: any; // Object containing BIN (Bank Identification Number) details } - 若为
paymentMethodType实值'BANK_ACCOUNT',这将是一个AchPaymentMethodDetails对象 :interface AchPaymentMethodDetails { bankAccountType?: 'CHECKING' | 'SAVINGS'; // Type of the bank account accountNumberLast4?: string; // Last four digits of the bank account number routingNumber?: string; // The routing number (for US accounts) transitNumber?: string; // The transit number (for Canadian accounts) institutionNumber?: string; // The institution number (for Canadian accounts) country?: 'US' | 'CA'; // The country context for the Bank Account details }
- 若为
- 有效载荷
onError(单位:千美元)(error: OdinPayErrorPayload) => void, (中文(简体) ), 需求: 发生错误时引用的回调函数 。 可在下列期间发生:-
滴入或基础 OdinPay.js v2 库的初始化 .
-
如果 OdinPay.js v2 表示失败(例如验证错误,API错误),则支付表提交程序.
-
入站部分内部设置问题.
-
对于提交错误,信息输入
OdinPayErrorPayload主要来源于result.errorsodinPay.js v2 提供的数组. -
有效载荷
OdinPayErrorPayload:财产 类型 说明 codestring表示错误性质的错误代码字符串 。 参见下"错误代码"表,例和意义. messagestring对错误的一般,人可读的描述. 用于校验错误 fieldErrors这可能是一个总结。 对于API错误,它可能反映来自OdinPay.js的信件.fieldErrors?Array<{ field: string, message: string }>可选的 详细列出特定字段验证错误的一系列对象。 每个对象包含: field(单位:千美元)string:fieldName从OdinPay.js v2(例如,"cardInformation", (中文(简体) ),"postalCode")或通用密钥.message(单位:千美元)string: 通常为:errorCode字符串来自 OdinPay.js v2 用于该字段(例如,"REQUIRED", (中文(简体) ),"INVALID_CARD_INFORMATION") (中文(简体) ).
httpStatusCode?number可选的 如果错误来自API调用(例如, 401, (中文(简体) ),400, (中文(简体) ),500) (中文(简体) ).rawError?any可选的 OdinPay.js(例如: result.errorsv2 提交错误的数组,或result.message。用于调试. -
常见出错代码( E)
code属性 :代码 含义 页:1 VALIDATION_ERROR_FIELDS提交时一个或多个特定字段的输入验证失败 . 检查 fieldErrors每个字段的细节的阵列.VALIDATION_ERROR_GENERAL在提交过程中发生了一般输入验证失败( 与v2 不太常见) result.errors) (中文(简体) ).如果 result.errors结构不按预期.API_ERROROdinPay.js v2在提交文件时与其后端API通信时发生了一般错误. 经常对应 type: "BACKEND"从 OdinPay.js v2 调取result.errors。 。 。 .API_AUTH_ERRORODIN API的认证错误( 如 HTTP 401). 通常是因为过期或无效的公用标志. API_CLIENT_ERRORODIN API的客户端错误(例如其他HTTP 4xx错误). API_SERVER_ERROR使用 ODIN API 的服务器端错误( 如 HTTP 5xx 错误) . GENERAL_PAYMENT_ERRORV1 风格字符串消息的支付处理中出现一般错误 . INIT_NO_KEY_PROVIDED初始化失败: ODIN Public Token 丢失或为空 . INIT_BADLY_FORMATTED_KEY初始化失败: ODIN Public Token 格式化不良 . INIT_INVALID_KEY_STRUCTURE初始化失败: ODIN 公共托肯结构无效 . 从OdinPay.js内部验证. INIT_UNSUPPORTED_COUNTRY初始化失败: 已提供 countryCode由OdinPay.js不支持.INIT_BT_SDK_FAILURE初始化失败: OdinPay.js 使用的 Basic Theory SDK 相关的内部错误 . INIT_NO_COUNTRY_CODE初始化失败 : countryCode核心组件需要时缺少道具.内部为出入口设置. INIT_NO_COUNTRY_CODE_FACADE初始化失败 : countryCode失踪在OdinDropinInitializationParams。 。 。 .花脸级鉴定. SDK_LOAD_ERROROdinPay.js v2 SDK脚本未能从CDN装入. 检查网络连接和CDN URL. SDK_NOT_DEFINED_ERROROdinPay.js v2 SDK 脚本已装入, 但是 OdinPay对象没有在全球范围定义.INSTANCE_NULL内部错误: OdinPay 实例在试图创建窗体时无效 . CREATE_FORM_ERROR_CARD拨打 OdinPay.js 时出错 createCardForm()。 。 。 .检查 rawError详细内容来自OdinPay.js.CREATE_FORM_ERROR_BANK_ACCOUNT拨打 OdinPay.js 时出错 createBankAccountForm()。 。 。 .检查 rawError详细内容来自OdinPay.js.SUBMIT_WHILE_INIT_ERROR当组件处于初始化错误状态时尝试提交 . FORM_NOT_READY在OdinPay.js表格完全完成或准备就绪之前曾试图提交. UNKNOWN_PAYMENT_METHOD_TYPE在成功回话中从OdinPay.js收到了出乎意料的支付方法类型. UNEXPECTED_CALLBACK_STRUCTUREOdinPay.js的成功/错误回调结构不如预期. ODIN_CALLBACK_ERROR从OdinPay.js召回的通用错误无法进一步分类. (与v1 处理错误更为常见, 与 v2 较少) result.errors) (中文(简体) ).MOUNT_POINT_NOT_FOUND未找到用于挂起滴入的 DOM 元素 . 表面级错误 . COMPONENT_CREATION_FAILED内部的 Stencil 网络组件无法创建 . 表面级错误 . INTERNAL_ERROR滴入组件内出乎意料的内部出错(例如未找到隐藏的提交按钮). UNKNOWN_ERROR发生了其他解析规则无法具体识别的错误. 终相倒地. 检查 rawError。 。 。 .
-
theme?(单位:千美元)OdinV2ThemeConfig, (中文(简体) ), 可选: 自定义 OdinPay.js v2 提供的输入字段外观的可选主题配置对象. 此对象允许您控制字体,颜色,贴花等样式,以及伪元素/状态样式(如:,::placeholder, (中文(简体) ),:hover, (中文(简体) ),:focus, (中文(简体) ),invalid) (中文(简体) ).- 结构 : 这个
theme对象必须跟随 平面结构 指定由 OdinPay.js v2. CSS 属性直接应用在主题对象的顶级.// Example structure for OdinV2ThemeConfig interface OdinV2ThemeConfig { fontFamily?: string; fontSize?: string; color?: string; backgroundColor?: string; padding?: string; letterSpacing?: string; // ... other supported CSS properties ... // Pseudo-selectors as keys '::placeholder'?: { color?: string; fontStyle?: string; // ... other placeholder styles ... }; ':hover'?: { backgroundColor?: string; color?: string; // ... other hover styles ... }; ':focus'?: { backgroundColor?: string; color?: string; fontWeight?: string; // ... other focus styles ... }; // Style for invalid fields (syntax may depend on OdinPay.js v2 specific implementation) 'invalid'?: { // Or potentially ':invalid' if it's a pseudo-class color?: string; fontWeight?: string; // ... other invalid styles ... }; } - 关于样式范围的重要说明:
- 这个
theme对象 只样式 OdinPay.js 输入字段的内部部分 (例如,案文、背景) 在内部 输入,占位符). - 它不控制边框、框阴影、整体尺寸或实地容器的结构布局. 这些结构样式 必须通过外部 CSS 应用 目标
divodinPay.js 字段的主机元素。 我们的投放部件 使这些容器 具有一类.odin-input内置 a.odin-field-container,每个都有一个独特的ID.
- 这个
- 默认行为 : 若无
theme对象提供, OdinPay.js v2 将为字段使用自己的默认内部样式. - 示例
theme对象 :new OdinDropin({ // ... other params theme: { "fontFamily": "Verdana, sans-serif", "fontSize": "14px", "color": "#000080", // Navy text "backgroundColor": "#f0f8ff", // Aliceblue background for input area "padding": "12px 15px", "::placeholder": { "color": "#778899", "fontStyle": "italic" }, ":focus": { "backgroundColor": "#add8e6" } } }); - 示例外部 CSS( 用于边框/ 容器, 将放在应用程序的样式表中):
/* Target the containers rendered by @exerp/odin-dropin */ /* If your app's styles are scoped (e.g., Vue <style scoped>), you might need :deep() */ /* Styles the div our component creates around each OdinPay field div */ .odin-field-container { border: 1px solid #ced4da; border-radius: 0.25rem; background-color: #ffffff; padding: 8px 10px; /* Adjust padding around OdinPay's input area */ margin-bottom: 15px; } /* Styles the direct div OdinPay uses (inside .odin-field-container) */ /* Make it transparent so .odin-field-container's background shows */ .odin-field-container .odin-input { background-color: transparent; min-height: 38px; /* Ensure it has some height */ }
- 结构 : 这个
instance.mount(selectorOrElement)
将 ODIN Drop- in UI 组件挂入指定的 DOM 元素.
参数:
selectorOrElement(单位:千美元)string | HTMLElement, (中文(简体) ), 需求:- 如果字符串,应该是一个CSS选择器(例如,
'#my-odin-container'指出应交付的集装箱要素. - 如果一个
HTMLElement,这是直接参考 容器元素.
- 如果字符串,应该是一个CSS选择器(例如,
- 回返情况:
void
instance.unmount()
从 DOM 中删除 ODIN Drop- in UI 组件,并清理关联事件听众.
- 参数: 无
- 回返情况:
void
用法示例
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<title>Odin Drop-in Test</title>
<style>
/* Basic button styling for this example */
.odin-submit-button { /* Target class from internal component */
padding: 10px 15px; background-color: #007bff; color: white;
border: none; border-radius: 4px; cursor: pointer; margin-top: 10px;
}
.odin-submit-button:disabled { opacity: 0.6; cursor: not-allowed; }
#odin-container { max-width: 400px; padding: 20px; border: 1px solid #ccc; margin: 20px; min-height: 150px; }
.error-message { color: red; margin-top: 10px; font-size: 0.9em; }
/*
IMPORTANT for OdinPay.js v2:
Border styling and other structural styles for the input field containers
are NOT controlled by the 'theme' object. Apply them using external CSS
targeting the containers rendered by @exerp/odin-dropin.
Our drop-in renders <div class="odin-field-container"> which wraps
a <div class="odin-input"> where OdinPay.js mounts its field.
*/
.odin-field-container { /* Or use :deep() if styles are scoped in your framework */
border: 1px solid #ccc;
border-radius: 4px;
padding: 5px 8px; /* Example padding around OdinPay's input area */
margin-bottom: 10px;
background-color: #fff;
}
.odin-field-container .odin-input {
background-color: transparent; /* Let container background show through */
min-height: 36px; /* Ensure space for OdinPay field */
}
</style>
</head>
<body>
<h1>Odin Payment Form Example</h1>
<div id="odin-container">
<!-- Drop-in will mount here -->
</div>
<div id="result-display"></div>
<div id="error-display" class="error-message"></div>
<div id="validation-event-display" style="margin-top:10px; font-size:0.9em; color: blue;"></div>
<script type="module" src="app.js"></script>
</body>
</html>
// app.js
import {
OdinDropin,
type BillingFieldsConfig, // Keep if used in example logic
type OdinFieldValidationEvent,
type OdinV2ThemeConfig,
type OdinSubmitPayload,
type OdinPayErrorPayload,
type LogLevel
} from '@exerp/odin-dropin';
const odinContainer = document.getElementById('odin-container');
const resultDisplay = document.getElementById('result-display');
const errorDisplay = document.getElementById('error-display');
const validationEventDisplay = document.getElementById('validation-event-display');
let odinDropinInstance = null; // To hold the instance
// --- Fetch Public Token (Replace with your actual backend call) ---
async function getPublicToken() {
// Simulate fetching from backend
console.log("Fetching ODIN Public Token...");
// In a real app, replace this with:
// const response = await fetch('/api/get-odin-public-token');
// if (!response.ok) throw new Error('Failed to fetch token');
// const data = await response.json();
// return data.publicToken;
// For example purposes:
return prompt("Enter ODIN Public Token:"); // DO NOT use prompt in production
}
function handleFieldValidation(event: OdinFieldValidationEvent) {
console.log('[Example App] onChangeValidation:', event);
if (validationEventDisplay) {
validationEventDisplay.textContent = `Last Validation: ${event.fieldName} - Valid: ${event.isValid}, Code: ${event.errorCode || 'N/A'}`;
}
// Add custom logic here, e.g., dynamic styling based on event.isValid
}
// --- Initialize and Mount ---
async function initializeOdin() {
if (!odinContainer) {
console.error('Mount point not found');
return;
}
try {
const token = await getPublicToken();
if (!token) {
throw new Error('ODIN Public Token is required.');
}
const country = prompt("Enter Country Code (US or CA):"); // Example, get this from your app's context
if (country !== 'US' && country !== 'CA') {
throw new Error('Valid Country Code (US or CA) is required.');
}
const pmtType = confirm("Use BANK_ACCOUNT? (Cancel for Card)") ? 'BANK_ACCOUNT' : 'CARD'; // Example of selecting payment method type
const enableNameField = confirm("Enable 'Name on Card' field?"); // Example
const currentBillingFieldsConfig: BillingFieldsConfig = {
name: enableNameField,
// addressLine1: { label: "Street Address", placeholder: "123 Main St" },
// addressLine2: false,
};
const exampleTheme: OdinV2ThemeConfig = {
"fontFamily": "Arial, sans-serif",
"fontSize": "16px",
"color": "#333",
"::placeholder": {
"color": "#888"
}
};
// Clean previous state if re-initializing
if (odinDropinInstance) {
odinDropinInstance.unmount();
}
if (resultDisplay) resultDisplay.textContent = '';
if (errorDisplay) errorDisplay.textContent = '';
if (validationEventDisplay) validationEventDisplay.textContent = '';
odinDropinInstance = new OdinDropin({
odinPublicToken: token,
countryCode: country as 'US' | 'CA',
paymentMethodType: pmtType as 'CARD' | 'BANK_ACCOUNT',
billingFieldsConfig: currentBillingFieldsConfig,
logLevel: 'DEBUG', // Example: set a log level
onChangeValidation: handleFieldValidation,
onSubmit: (result) => {
console.log('onSubmit received:', result);
let message = `Success! PM ID: ${result.paymentMethodId}\nType: ${result.paymentMethodType}`;
if (result.paymentMethodType === 'CARD' && result.details) {
const cardDetails = result.details; // Type assertion might be needed in TS, not in JS example
message += `\nCard Brand: ${cardDetails.cardBrand || 'N/A'}`;
message += `\nLast 4: ${cardDetails.last4 || 'N/A'}`;
message += `\nExpires: ${cardDetails.expirationDate || 'N/A'}`;
// You might want to display maskedAccountNumber or binDetails too
// console.log('Masked Account Number:', cardDetails.maskedAccountNumber);
// console.log('BIN Details:', cardDetails.binDetails);
} else if (result.paymentMethodType === 'BANK_ACCOUNT' && result.details) {
const achDetails = result.details as AchPaymentMethodDetails; // Type assertion for clarity
message += `\nAccount Type: ${achDetails.bankAccountType || 'N/A'}`;
message += `\nAccount Last4: ${achDetails.accountNumberLast4 || 'N/A'}`;
if (achDetails.routingNumber) message += `\nRouting: ${achDetails.routingNumber}`;
if (achDetails.transitNumber) message += `\nTransit: ${achDetails.transitNumber}`;
if (achDetails.institutionNumber) message += `\nInstitution: ${achDetails.institutionNumber}`;
}
if (result.billingInformation) {
message += `\nBilling Info: ${JSON.stringify(result.billingInformation, null, 2)}`;
}
if (resultDisplay) resultDisplay.textContent = message;
if (errorDisplay) errorDisplay.textContent = '';
alert(`Payment Method ID: ${result.paymentMethodId} - Review console for all details. Send ID to backend.`);
// Optionally unmount after success:
// odinDropinInstance.unmount();
},
onError: (error) => {
console.error('onError received:', error);
if (errorDisplay) errorDisplay.textContent = `Error: ${error.message}`;
if (resultDisplay) resultDisplay.textContent = '';
},
});
odinDropinInstance.mount(odinContainer); // Mount into the div element
} catch (error) {
console.error('Initialization or mounting failed:', error);
if (errorDisplay) errorDisplay.textContent = `Error: ${error.message || error}`;
}
}
// --- Run Initialization ---
initializeOdin();
// Example of how to unmount if needed later (e.g., on component destroy in a framework)
// function cleanup() {
// if (odinDropinInstance) {
// odinDropinInstance.unmount();
// odinDropinInstance = null;
// console.log('Odin Drop-in unmounted.');
// }
// }
// window.addEventListener('beforeunload', cleanup); // Example cleanup trigger