bison-web-components 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/wio-payment.js ADDED
@@ -0,0 +1,579 @@
1
+ /**
2
+ * WioPayment Web Component
3
+ *
4
+ * A web component that integrates Moov payment methods with Plaid Link for
5
+ * Worker-Independent Operators (WIOs) to securely link their bank accounts.
6
+ *
7
+ * @author @kfajardo
8
+ * @version 1.0.0
9
+ *
10
+ * @requires BisonJibPayAPI - Must be loaded before this component (from component.js)
11
+ *
12
+ * @example
13
+ * ```html
14
+ * <script src="component.js"></script>
15
+ * <script src="wio-payment.js"></script>
16
+ *
17
+ * <wio-payment id="payment"></wio-payment>
18
+ * <script>
19
+ * const payment = document.getElementById('payment');
20
+ * payment.wioEmail = 'wio@example.com';
21
+ * payment.env = 'sandbox';
22
+ * payment.onSuccess = (data) => console.log('Success!', data);
23
+ * payment.onError = ({ errorType, error }) => console.error(errorType, error);
24
+ * payment.open = true;
25
+ * </script>
26
+ * ```
27
+ */
28
+
29
+ class WioPayment extends HTMLElement {
30
+ constructor() {
31
+ super();
32
+ this.attachShadow({ mode: "open" });
33
+
34
+ // API Configuration
35
+ this.apiBaseURL =
36
+ this.getAttribute("api-base-url") ||
37
+ "https://bison-jib-development.azurewebsites.net";
38
+ this.embeddableKey =
39
+ this.getAttribute("embeddable-key") ||
40
+ "R80WMkbNN8457RofiMYx03DL65P06IaVT30Q2emYJUBQwYCzRC";
41
+
42
+ // Check if BisonJibPayAPI is available
43
+ if (typeof BisonJibPayAPI === "undefined") {
44
+ console.error(
45
+ "WioPayment: BisonJibPayAPI is not available. Please ensure component.js is loaded before wio-payment.js"
46
+ );
47
+ this.api = null;
48
+ } else {
49
+ this.api = new BisonJibPayAPI(this.apiBaseURL, this.embeddableKey);
50
+ }
51
+
52
+ // Internal state
53
+ this._state = {
54
+ wioEmail: null,
55
+ env: "sandbox",
56
+ redirectURL: typeof window !== "undefined" ? window.location.origin : "",
57
+ moovToken: null,
58
+ plaidToken: null,
59
+ isInitialized: false,
60
+ isLoading: false,
61
+ error: null,
62
+ };
63
+
64
+ // Callback references
65
+ this._onSuccessCallback = null;
66
+ this._onErrorCallback = null;
67
+
68
+ // Moov drop reference
69
+ this._moovRef = null;
70
+
71
+ // Render the component
72
+ this.render();
73
+ }
74
+
75
+ // ==================== STATIC PROPERTIES ====================
76
+
77
+ static get observedAttributes() {
78
+ return [
79
+ "wio-email",
80
+ "env",
81
+ "redirect-url",
82
+ "on-success",
83
+ "on-error",
84
+ "open",
85
+ "api-base-url",
86
+ "embeddable-key",
87
+ ];
88
+ }
89
+
90
+ // ==================== PROPERTY GETTERS/SETTERS ====================
91
+
92
+ /**
93
+ * Get the WIO email
94
+ * @returns {string|null}
95
+ */
96
+ get wioEmail() {
97
+ return this._state.wioEmail;
98
+ }
99
+
100
+ /**
101
+ * Set the WIO email (triggers initialization)
102
+ * @param {string} value - WIO's email address
103
+ */
104
+ set wioEmail(value) {
105
+ if (this._state.wioEmail !== value) {
106
+ this._state.wioEmail = value;
107
+ this._state.isInitialized = false;
108
+
109
+ // Reinitialize if already connected and email is provided
110
+ if (this.isConnected && value && this._state.env) {
111
+ this.initializeMoovDrop();
112
+ }
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Get the Plaid environment
118
+ * @returns {string}
119
+ */
120
+ get env() {
121
+ return this._state.env;
122
+ }
123
+
124
+ /**
125
+ * Set the Plaid environment (required for Plaid integration)
126
+ * @param {string} value - Plaid environment ('sandbox', 'development', or 'production')
127
+ */
128
+ set env(value) {
129
+ if (this._state.env !== value) {
130
+ this._state.env = value;
131
+ this._state.isInitialized = false;
132
+
133
+ // Reinitialize if already connected and both email and env are provided
134
+ if (this.isConnected && this._state.wioEmail && value) {
135
+ this.initializeMoovDrop();
136
+ }
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Get the redirect URL
142
+ * @returns {string}
143
+ */
144
+ get redirectURL() {
145
+ return this._state.redirectURL;
146
+ }
147
+
148
+ /**
149
+ * Set the redirect URL for Plaid OAuth flow
150
+ * @param {string} value - URL to redirect to after Plaid OAuth
151
+ */
152
+ set redirectURL(value) {
153
+ this._state.redirectURL = value;
154
+ }
155
+
156
+ /**
157
+ * Get the onSuccess callback
158
+ * @returns {Function|null}
159
+ */
160
+ get onSuccess() {
161
+ return this._onSuccessCallback;
162
+ }
163
+
164
+ /**
165
+ * Set the onSuccess callback
166
+ * @param {Function} callback - Called when bank account is successfully linked
167
+ */
168
+ set onSuccess(callback) {
169
+ if (typeof callback === "function" || callback === null) {
170
+ this._onSuccessCallback = callback;
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Get the onError callback
176
+ * @returns {Function|null}
177
+ */
178
+ get onError() {
179
+ return this._onErrorCallback;
180
+ }
181
+
182
+ /**
183
+ * Set the onError callback
184
+ * @param {Function} callback - Called when an error occurs
185
+ */
186
+ set onError(callback) {
187
+ if (typeof callback === "function" || callback === null) {
188
+ this._onErrorCallback = callback;
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Get the open state of the payment drop
194
+ * @returns {boolean}
195
+ */
196
+ get open() {
197
+ return this._moovRef?.open || false;
198
+ }
199
+
200
+ /**
201
+ * Set the open state of the payment drop
202
+ * @param {boolean} value - Whether to show the drop
203
+ */
204
+ set open(value) {
205
+ const shouldOpen = Boolean(value);
206
+
207
+ // Initialize if not yet initialized and trying to open
208
+ if (!this._state.isInitialized && shouldOpen) {
209
+ this.initializeMoovDrop().then(() => {
210
+ if (this._moovRef) {
211
+ this._moovRef.open = shouldOpen;
212
+ }
213
+ });
214
+ } else if (this._moovRef) {
215
+ this._moovRef.open = shouldOpen;
216
+ }
217
+ }
218
+
219
+ // ==================== LIFECYCLE METHODS ====================
220
+
221
+ connectedCallback() {
222
+ // Load Moov SDK if not already loaded
223
+ this.ensureMoovSDK().then(() => {
224
+ // Initialize if both wioEmail and env are already set
225
+
226
+ if (
227
+ this._state.wioEmail &&
228
+ this._state.env &&
229
+ !this._state.isInitialized
230
+ ) {
231
+ this.initializeMoovDrop();
232
+ }
233
+ });
234
+ }
235
+
236
+ disconnectedCallback() {
237
+ // Cleanup if needed
238
+ this._moovRef = null;
239
+ }
240
+
241
+ attributeChangedCallback(name, oldValue, newValue) {
242
+ if (oldValue === newValue) return;
243
+
244
+ switch (name) {
245
+ case "wio-email":
246
+ this.wioEmail = newValue;
247
+ break;
248
+
249
+ case "env":
250
+ this.env = newValue;
251
+ break;
252
+
253
+ case "redirect-url":
254
+ this.redirectURL = newValue;
255
+ break;
256
+
257
+ case "on-success":
258
+ // Attribute-based callback (function name in global scope)
259
+ if (newValue && window[newValue]) {
260
+ this.onSuccess = window[newValue];
261
+ }
262
+ break;
263
+
264
+ case "on-error":
265
+ // Attribute-based callback (function name in global scope)
266
+ if (newValue && window[newValue]) {
267
+ this.onError = window[newValue];
268
+ }
269
+ break;
270
+
271
+ case "open":
272
+ this.open = newValue !== null;
273
+ break;
274
+
275
+ case "api-base-url":
276
+ this.apiBaseURL = newValue;
277
+ this.api = new BisonJibPayAPI(this.apiBaseURL, this.embeddableKey);
278
+ break;
279
+
280
+ case "embeddable-key":
281
+ this.embeddableKey = newValue;
282
+ this.api = new BisonJibPayAPI(this.apiBaseURL, this.embeddableKey);
283
+ break;
284
+ }
285
+ }
286
+
287
+ // ==================== CORE METHODS ====================
288
+
289
+ /**
290
+ * Ensure Moov SDK is loaded
291
+ *
292
+ * This method dynamically loads the Moov SDK from the CDN if not already present.
293
+ * This eliminates the need for consumers to manually include the script tag.
294
+ *
295
+ * @returns {Promise<void>} Resolves when SDK is ready
296
+ */
297
+ async ensureMoovSDK() {
298
+ // Check if Moov is already loaded
299
+ if (window.Moov) {
300
+ console.log("WioPayment: Moov SDK already loaded");
301
+ return Promise.resolve();
302
+ }
303
+
304
+ // Check if script is already being loaded
305
+ const existingScript = document.querySelector('script[src*="moov.js"]');
306
+ if (existingScript) {
307
+ console.log("WioPayment: Moov SDK script found, waiting for load...");
308
+ return new Promise((resolve, reject) => {
309
+ existingScript.addEventListener("load", () => resolve());
310
+ existingScript.addEventListener("error", () =>
311
+ reject(new Error("Failed to load Moov SDK"))
312
+ );
313
+ });
314
+ }
315
+
316
+ // Load the SDK
317
+ console.log("WioPayment: Loading Moov SDK from CDN...");
318
+ return new Promise((resolve, reject) => {
319
+ const script = document.createElement("script");
320
+ script.src = "https://js.moov.io/v1";
321
+ script.async = true;
322
+ script.defer = true;
323
+
324
+ script.onload = () => {
325
+ console.log("WioPayment: Moov SDK loaded successfully");
326
+ resolve();
327
+ };
328
+
329
+ script.onerror = () => {
330
+ const error = new Error("Failed to load Moov SDK from CDN");
331
+ console.error("WioPayment:", error);
332
+ this.handleError({
333
+ errorType: "sdk",
334
+ error: error.message,
335
+ });
336
+ reject(error);
337
+ };
338
+
339
+ // Append to document head
340
+ document.head.appendChild(script);
341
+ });
342
+ }
343
+
344
+ /**
345
+ * Initialize the Moov payment methods drop with Plaid integration
346
+ *
347
+ * This method:
348
+ * 1. Validates prerequisites (wioEmail, env, Moov SDK)
349
+ * 2. Generates both Plaid and Moov access tokens
350
+ * 3. Configures the moov-payment-methods element with Plaid settings
351
+ * 4. Sets up success/error callbacks
352
+ */
353
+ async initializeMoovDrop() {
354
+ // 1. Validate prerequisites
355
+ if (!this._state.wioEmail) {
356
+ console.warn("WioPayment: wioEmail is required");
357
+ return;
358
+ }
359
+
360
+ if (!this._state.env) {
361
+ console.warn("WioPayment: env is required for Plaid integration");
362
+ return;
363
+ }
364
+
365
+ if (!window.Moov) {
366
+ this.handleError({
367
+ errorType: "sdk",
368
+ error: "Moov SDK not loaded. Please include the Moov SDK script.",
369
+ });
370
+ return;
371
+ }
372
+
373
+ // 2. Validate API availability
374
+ if (!this.api) {
375
+ this.handleError({
376
+ errorType: "initialization",
377
+ error:
378
+ "BisonJibPayAPI is not available. Please ensure component.js is loaded first.",
379
+ });
380
+ return;
381
+ }
382
+ let accID = "";
383
+
384
+ try {
385
+ this._state.isLoading = true;
386
+
387
+ // 3. Generate Plaid token
388
+ console.log("WioPayment: Generating Plaid token...");
389
+ const plaidTokenResult = await this.api.generatePlaidToken(
390
+ this._state.wioEmail
391
+ );
392
+ this._state.plaidToken = plaidTokenResult.data.linkToken;
393
+ console.log("WioPayment: Plaid token generated successfully");
394
+
395
+ // 4. Generate Moov token
396
+ console.log("WioPayment: Generating Moov token...");
397
+ const moovTokenResult = await this.api.generateMoovToken(
398
+ this._state.wioEmail
399
+ );
400
+ this._state.moovToken = moovTokenResult.data.accessToken;
401
+ accID = moovTokenResult.data.moovAccountId;
402
+
403
+ console.log("WioPayment: Moov token generated successfully");
404
+ } catch (error) {
405
+ this.handleError({
406
+ errorType: "token",
407
+ error: error.message || "Failed to generate tokens",
408
+ });
409
+ return;
410
+ } finally {
411
+ this._state.isLoading = false;
412
+ }
413
+
414
+ // 5. Get reference to moov-payment-methods element
415
+ this._moovRef = this.shadowRoot.querySelector("moov-payment-methods");
416
+
417
+ if (!this._moovRef) {
418
+ console.error(
419
+ "WioPayment: moov-payment-methods element not found in shadow DOM"
420
+ );
421
+ return;
422
+ }
423
+
424
+ // 6. Configure Plaid integration
425
+ this._moovRef.plaid = {
426
+ env: this._state.env,
427
+ redirectURL: this._state.redirectURL,
428
+ token: this._state.plaidToken,
429
+ onSuccess: (moovBankAccount) => {
430
+ console.log(
431
+ "WioPayment: Plaid flow completed successfully",
432
+ moovBankAccount
433
+ );
434
+ },
435
+ onExit: (err, metadata) => {
436
+ console.log("PLAID ERROR", err, metadata);
437
+ if (err) {
438
+ console.error("WioPayment: Plaid flow exited with error", err);
439
+ this.handleError({
440
+ errorType: "plaid",
441
+ error: err,
442
+ });
443
+ } else {
444
+ console.log("WioPayment: Plaid flow exited by user", metadata);
445
+ }
446
+ },
447
+ onLoad: () => {
448
+ console.log("WioPayment: Plaid Link resource loaded");
449
+ },
450
+ onProcessorTokenRequest: async (public_token, bank_account_id) => {
451
+ console.log(
452
+ "WioPayment: Generating processor token for bank account",
453
+ bank_account_id
454
+ );
455
+
456
+ try {
457
+ const result = await this.api.createProcessorToken(
458
+ public_token,
459
+ bank_account_id
460
+ );
461
+ console.log(
462
+ "WioPayment: Processor token generated successfully",
463
+ result.data.processorToken
464
+ );
465
+ return result.data.processorToken;
466
+ } catch (error) {
467
+ console.error("WioPayment: Failed to create processor token", error);
468
+ this.handleError({
469
+ errorType: "plaid",
470
+ error: "Failed to create processor token",
471
+ });
472
+ throw error;
473
+ }
474
+ },
475
+ };
476
+ console.log("accountID", accID);
477
+ // 7. Configure the Moov drop
478
+ this._moovRef.token = this._state.moovToken;
479
+ this._moovRef.accountID = accID;
480
+ this._moovRef.paymentMethodTypes = ["plaid"];
481
+ this._moovRef.showLogo = true;
482
+
483
+ // 8. Set up callbacks
484
+ this._moovRef.onResourceCreated = (result) => {
485
+ console.log("WioPayment: Bank account successfully linked", result);
486
+
487
+ // Call user's success callback
488
+ if (this._onSuccessCallback) {
489
+ this._onSuccessCallback(result);
490
+ }
491
+
492
+ // Auto-close after success
493
+ this.open = false;
494
+ };
495
+
496
+ this._moovRef.onError = ({ errorType, error }) => {
497
+ console.error("WioPayment: Moov error", errorType, error);
498
+ this.handleError({ errorType, error });
499
+ };
500
+
501
+ // 9. Mark as initialized
502
+ this._state.isInitialized = true;
503
+ console.log(
504
+ "WioPayment: Initialized for",
505
+ this._state.wioEmail,
506
+ "with env",
507
+ this._state.env
508
+ );
509
+ }
510
+
511
+ /**
512
+ * Handle errors
513
+ * @param {Object} errorData - Error information
514
+ * @param {string} errorData.errorType - Type of error
515
+ * @param {string|Error} errorData.error - Error message or object
516
+ */
517
+ handleError({ errorType, error }) {
518
+ // Store error in state
519
+ this._state.error = { errorType, error };
520
+
521
+ // Log to console
522
+ console.error(`WioPayment Error (${errorType}):`, error);
523
+
524
+ // Call user's error callback
525
+ if (this._onErrorCallback) {
526
+ this._onErrorCallback({ errorType, error });
527
+ }
528
+
529
+ // Emit custom event
530
+ this.dispatchEvent(
531
+ new CustomEvent("payment-error", {
532
+ detail: { errorType, error },
533
+ bubbles: true,
534
+ composed: true,
535
+ })
536
+ );
537
+ }
538
+
539
+ // ==================== RENDERING ====================
540
+
541
+ /**
542
+ * Render the component (Shadow DOM)
543
+ */
544
+ render() {
545
+ this.shadowRoot.innerHTML = `
546
+ <style>
547
+ :host {
548
+ display: block;
549
+ font-family: var(--font-sans, 'Inter', system-ui, sans-serif);
550
+ color: var(--color-secondary, #5f6e78);
551
+ }
552
+
553
+ /* Ensure moov-payment-methods is properly contained */
554
+ moov-payment-methods {
555
+ display: block;
556
+ width: 100%;
557
+ }
558
+ </style>
559
+
560
+ <!-- Moov payment methods drop -->
561
+ <moov-payment-methods></moov-payment-methods>
562
+ `;
563
+ }
564
+ }
565
+
566
+ // Register the custom element only if it hasn't been registered yet
567
+ if (!customElements.get("wio-payment")) {
568
+ customElements.define("wio-payment", WioPayment);
569
+ }
570
+
571
+ // Export for module usage
572
+ if (typeof module !== "undefined" && module.exports) {
573
+ module.exports = { WioPayment };
574
+ }
575
+
576
+ // Make available globally for script tag usage
577
+ if (typeof window !== "undefined") {
578
+ window.WioPayment = WioPayment;
579
+ }