@exerp/odin-dropin
Version:
ODIN Payment Drop-in Facade
530 lines (476 loc) • 35 kB
Markdown
<p align="left">
<a href="../../README.md"> 📃 Workspace README</a> -
<a href="./CHANGELOG.md"> 📄 Changelog</a>
</p>
---
# ODIN Payment Drop-in Facade (`/odin-dropin`)
This package provides the public facade for integrating the ODIN Payment Drop-in component into web applications. It simplifies the interaction with the underlying Stencil web components and the `OdinPay.js` library.
<table>
<tr>
<td align="center" valign="top">
<strong>Example: Card Payment Form</strong><br>
<img src="../../docs/assets/images/dropin-card-example-fields.png" alt="ODIN Drop-in Card Example" width="350">
</td>
<td align="center" valign="top">
<strong>Example: BANK_ACCOUNT Payment Form</strong><br>
<img src="../../docs/assets/images/dropin-ach-example-fields.png" alt="ODIN Drop-in BANK_ACCOUNT Example" width="350">
</td>
</tr>
</table>
## Installation
```bash
# Using pnpm
pnpm add /odin-dropin
# Using npm
npm install /odin-dropin
# Using yarn
yarn add /odin-dropin
```
## API Documentation
The primary export of this package is the `OdinDropin` class.
### Importing
```javascript
// 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 '/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)`
Initializes a new instance of the ODIN Drop-in controller.
**Parameters:**
* `params` (`OdinDropinInitializationParams`, **Required**): An object containing configuration parameters:
* `odinPublicToken` (`string`, **Required**): The short-lived public token obtained securely from your backend (which fetches it from the ODIN `/auth2/public-token` endpoint). This token is used to authenticate and initialize the underlying `OdinPay.js` library.
* `countryCode` (`'US' | 'CA'`, **Required**): Specifies the country for which the payment form should be configured. This influences aspects like postal code field labeling and validation rules within `OdinPay.js`.
* `paymentMethodType?` (`'CARD' | 'BANK_ACCOUNT'`, *Optional*, Default: `'CARD'`): Specifies the type of payment form to render.
* `'CARD'`: Renders the credit card payment form.
* `'BANK_ACCOUNT'`: Renders the bank account payment form (for US ACH, Canadian EFT, as supported by OdinPay).
* `billingFieldsConfig?` (`BillingFieldsConfig`, *Optional*): An object to configure the visibility and customize the appearance of billing address fields and other standard fields.
* **Usage for Credit Card:**
* For optional fields (e.g., `name`, `addressLine1`, `city`, `state`, `country`, `emailAddress`, `phoneNumber`):
* Omit the field key: The field will not be displayed.
* Set to `true`: The field will be displayed with its default label and placeholder.
* Set to a `FieldCustomization` object (e.g., `{ label: 'Your Name', placeholder: 'Full Name Here' }`): The field will be displayed with the specified custom label and/or placeholder. Empty strings for label/placeholder will effectively use the default.
* For fields that are always part of the form structure but whose labels/placeholders can be customized (`postalCode`, `cardInformation` label):
* Omit the field key or provide an empty `FieldCustomization` object: Default label/placeholder will be used.
* Set to a `FieldCustomization` object: The specified custom label and/or placeholder will be used. (Note: `cardInformation`'s internal placeholder is not configurable via this).
* **Example:**
```javascript
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.
}
```
* **Usage for BANK_ACCOUNT:** For Bank Account, the `name` field in `billingFieldsConfig` (if customized or enabled as `true`) refers to the "Account Holder Name". Other optional billing fields (`addressLine1`, `postalCode`, etc.) can also be configured and will be collected alongside the Bank Account details. The fields `accountNumber`, `bankAccountType`, `routingNumber` (US), `transitNumber` (CA), and `institutionNumber` (CA) are core to the Bank Account form and their labels/placeholders can also be customized using their respective keys in `billingFieldsConfig` (e.g., `accountNumber: { label: 'Bank Account No.' }`).
* **Example (Bank Account field customization):**
```javascript
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
}
```
* *(Refer to the exported `BillingFieldsConfig` and `FieldCustomization` types for full details.)*
* `logLevel?` (`LogLevel`, *Optional*, Default: `'WARN'`):
Controls the verbosity of messages logged to the browser console by the Drop-in component (both facade and core parts). Setting a level allows messages of that level and higher severity to be shown.
* Possible values: `'NONE'`, `'ERROR'`, `'WARN'`, `'INFO'`, `'DEBUG'`.
* `'NONE'`: No logs are shown.
* `'ERROR'`: Only critical errors are shown.
* `'WARN'`: Errors and warnings are shown.
* `'INFO'`: Errors, warnings, and informational messages (e.g., lifecycle events) are shown.
* `'DEBUG'`: All messages, including detailed debugging information, are shown.
* `onChangeValidation?` (`(event: OdinFieldValidationEvent) => void`, *Optional*): An optional callback function invoked by OdinPay.js v2 during user interaction with input fields. It provides real-time validation status for individual fields, allowing for dynamic UI updates or custom logic.
* **Event Payload (`OdinFieldValidationEvent`):**
| Property | Type | Description |
|-------------|----------|--------------------------------------------------------------------------------------|
| `type` | `string` | The type of event, typically `"FIELD_VALIDATION"`. |
| `fieldName` | `string` | The name of the field being validated (e.g., `"cardNumber"`, `"postalCode"`). |
| `selector` | `string` | The CSS selector for the field's container, as provided to OdinPay.js. |
| `isValid` | `boolean`| Indicates if the field is currently valid according to OdinPay.js validation rules. |
| `errorCode?`| `string` | An error code if the field is invalid (e.g., `"INVALID"`, `"REQUIRED"`). Optional. |
* **Example Usage:**
```javascript
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`, **Required**): A callback function that will be invoked when the user successfully submits the payment form and `OdinPay.js` returns a payment method token.
* **Payload (`OdinSubmitPayload`):** An object containing:
* `paymentMethodId` (`string`): The tokenized payment method identifier generated by ODIN. Send this ID to your backend server to perform payment actions.
* `paymentMethodType` (`'CARD' | 'BANK_ACCOUNT'`): A string indicating the type of payment method tokenized. This helps in interpreting the `details` object.
* `billingInformation?` (`OdinPayBillingInformation`, *Optional*): An object containing the billing details entered by the user, if any billing fields were configured and submitted. Its structure is:
```typescript
interface OdinPayBillingInformation {
name?: string;
emailAddress?: string;
phoneNumber?: string;
address?: {
addressLine1?: string;
addressLine2?: string;
city?: string;
state?: string;
postalCode?: string;
country?: string;
};
}
```
Keys will be present if the corresponding field was configured. An empty string value means the field was configured but left blank by the user. If a field was not configured, its key will be omitted. The `address` object itself will be omitted if no address fields were configured.
* `details?` (`CardPaymentMethodDetails | AchPaymentMethodDetails | ...`, *Optional*): An object containing details specific to the `paymentMethodType`.
* If `paymentMethodType` is `'CARD'`, this will be a `CardPaymentMethodDetails` object:
```typescript
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
}
```
* If `paymentMethodType` is `'BANK_ACCOUNT'`, this will be an `AchPaymentMethodDetails` object:
```typescript
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`, **Required**): A callback function invoked if an error occurs. This can happen during:
* Initialization of the drop-in or the underlying OdinPay.js v2 library.
* The payment form submission process if OdinPay.js v2 indicates a failure (e.g., validation errors, API errors).
* Internal setup issues within the drop-in component.
* For submission errors, the information in `OdinPayErrorPayload` is primarily derived from the `result.errors` array provided by OdinPay.js v2.
* **Payload (`OdinPayErrorPayload`):**
| Property | Type | Description |
|-----------------|-------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `code` | `string` | An error code string indicating the nature of the error. See the "Error Codes" table below for examples and meanings. |
| `message` | `string` | A general, human-readable description of the error. For validation errors with `fieldErrors`, this might be a summary. For API errors, it may reflect messages from OdinPay.js. |
| `fieldErrors?` | `Array<{ field: string, message: string }>` | Optional. An array of objects detailing field-specific validation errors. Each object contains:<ul><li>`field` (`string`): The `fieldName` from OdinPay.js v2 (e.g., `"cardInformation"`, `"postalCode"`) or a generic key.</li><li>`message` (`string`): Typically the `errorCode` string from OdinPay.js v2 for that field (e.g., `"REQUIRED"`, `"INVALID_CARD_INFORMATION"`).</li></ul> |
| `httpStatusCode?`| `number` | Optional. The HTTP status code if the error originated from an API call (e.g., `401`, `400`, `500`). |
| `rawError?` | `any` | Optional. The original error information received from OdinPay.js (e.g., the `result.errors` array for v2 submission errors, or `result.message`). Useful for debugging. |
* **Common Error Codes (`code` property):**
| Code | Meaning | Notes |
|----------------------------------|--------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------|
| `VALIDATION_ERROR_FIELDS` | Input validation failed for one or more specific fields during submission. | Check the `fieldErrors` array for details on each field. |
| `VALIDATION_ERROR_GENERAL` | A general input validation failure occurred during submission (less common with v2 `result.errors`). | May still occur if `result.errors` is not structured as expected. |
| `API_ERROR` | A general error occurred while OdinPay.js v2 communicated with its backend API during submission. | Often corresponds to `type: "BACKEND"` from OdinPay.js v2 `result.errors`. |
| `API_AUTH_ERROR` | An authentication error with the ODIN API (e.g., HTTP 401). | Often due to an expired or invalid public token. |
| `API_CLIENT_ERROR` | A client-side error with the ODIN API (e.g., other HTTP 4xx errors). | |
| `API_SERVER_ERROR` | A server-side error with the ODIN API (e.g., HTTP 5xx errors). | |
| `GENERAL_PAYMENT_ERROR` | Generic error during payment processing from v1-style string message. | |
| `INIT_NO_KEY_PROVIDED` | Initialization failed: ODIN Public Token was missing or empty. | |
| `INIT_BADLY_FORMATTED_KEY` | Initialization failed: ODIN Public Token was badly formatted. | |
| `INIT_INVALID_KEY_STRUCTURE` | Initialization failed: ODIN Public Token structure was invalid. | From OdinPay.js internal validation. |
| `INIT_UNSUPPORTED_COUNTRY` | Initialization failed: The provided `countryCode` is not supported by OdinPay.js. | |
| `INIT_BT_SDK_FAILURE` | Initialization failed: Internal error related to the Basis Theory SDK used by OdinPay.js. | |
| `INIT_NO_COUNTRY_CODE` | Initialization failed: `countryCode` prop was missing when required by the core component. | Internal to drop-in setup. |
| `INIT_NO_COUNTRY_CODE_FACADE` | Initialization failed: `countryCode` was missing in `OdinDropinInitializationParams`. | Facade-level validation. |
| `SDK_LOAD_ERROR` | The OdinPay.js v2 SDK script failed to load from the CDN. | Check network connectivity and CDN URL. |
| `SDK_NOT_DEFINED_ERROR` | OdinPay.js v2 SDK script loaded, but `OdinPay` object was not defined globally. | |
| `INSTANCE_NULL` | Internal error: OdinPay instance was null when trying to create a form. | |
| `CREATE_FORM_ERROR_CARD` | An error occurred when calling OdinPay.js `createCardForm()`. | Check `rawError` for details from OdinPay.js. |
| `CREATE_FORM_ERROR_BANK_ACCOUNT` | An error occurred when calling OdinPay.js `createBankAccountForm()`. | Check `rawError` for details from OdinPay.js. |
| `SUBMIT_WHILE_INIT_ERROR` | Submission attempted while the component was in an initialization error state. | |
| `FORM_NOT_READY` | Submission attempted before the OdinPay.js form was fully rendered or ready. | |
| `UNKNOWN_PAYMENT_METHOD_TYPE` | Received an unexpected payment method type from OdinPay.js in the success callback. | |
| `UNEXPECTED_CALLBACK_STRUCTURE` | The success/error callback structure from OdinPay.js was not as expected. | |
| `ODIN_CALLBACK_ERROR` | A generic error from an OdinPay.js callback that couldn't be further categorized. | (More common with v1 error handling, less with v2 `result.errors`). |
| `MOUNT_POINT_NOT_FOUND` | The DOM element specified for mounting the drop-in was not found. | Facade-level error. |
| `COMPONENT_CREATION_FAILED` | The internal Stencil web component could not be created. | Facade-level error. |
| `INTERNAL_ERROR` | An unexpected internal error within the drop-in component (e.g. hidden submit button not found). | |
| `UNKNOWN_ERROR` | An error occurred that could not be specifically identified by other parsing rules. | Ultimate fallback. Check `rawError`. |
* `theme?` (`OdinV2ThemeConfig`, *Optional*): An optional theme configuration object for customizing the appearance of the input fields rendered by OdinPay.js v2. This object allows you to control styles like fonts, colors, padding, and pseudo-element/state styles (e.g., `::placeholder`, `:hover`, `:focus`, `invalid`).
* **Structure:** The `theme` object must follow the **flat structure** specified by OdinPay.js v2. CSS properties are applied directly at the top level of the theme object.
```typescript
// 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 ...
};
}
```
* **Important Note on Styling Scope:**
* The `theme` object **only styles the internal parts of the OdinPay.js input fields** (e.g., the text, background *within* the input, placeholder).
* **It does NOT control borders, box-shadow, overall dimensions, or structural layout of the field containers.** These structural styles **must be applied via external CSS** targeting the `div` elements that host the OdinPay.js fields. Our drop-in component renders these containers with a class like `.odin-input` inside a `.odin-field-container`, and each has a unique ID.
* **Default Behavior:** If no `theme` object is provided, OdinPay.js v2 will use its own default internal styling for the fields.
* **Example `theme` Object:**
```javascript
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"
}
}
});
```
* **Example External CSS (for borders/containers, to be placed in your application's stylesheet):**
```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)`
Mounts the ODIN Drop-in UI component into the specified DOM element.
**Parameters:**
* `selectorOrElement` (`string | HTMLElement`, **Required**):
* If a string, it should be a CSS selector (e.g., `'#my-odin-container'`) pointing to the container element where the drop-in should be rendered.
* If an `HTMLElement`, it's the direct reference to the container element.
* **Returns:** `void`
### `instance.unmount()`
Removes the ODIN Drop-in UI component from the DOM and cleans up associated event listeners.
* **Parameters:** None
* **Returns:** `void`
## Usage Example
```html
<!-- 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>
```
```javascript
// app.js
import {
OdinDropin,
type BillingFieldsConfig, // Keep if used in example logic
type OdinFieldValidationEvent,
type OdinV2ThemeConfig,
type OdinSubmitPayload,
type OdinPayErrorPayload,
type LogLevel
} from '/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