1import { Controller } from "@hotwired/stimulus" 2 3const CONNECT_JS_SRC = "https://connect-js.stripe.com/v1.0/connect.js" 4 5function ensureConnectJsLoaded() { 6 if (window.StripeConnect?.init) return Promise.resolve() 7 if (window.__chConnectJsPromise) return window.__chConnectJsPromise 8 9 window.__chConnectJsPromise = new Promise((resolve, reject) => { 10 const existing = document.querySelector(`script[src="${CONNECT_JS_SRC}"]`) 11 const script = existing || document.createElement("script") 12 let settled = false 13 let timeout 14 15 const finish = (callback) => { 16 if (settled) return 17 18 settled = true 19 window.clearTimeout(timeout) 20 callback() 21 } 22 timeout = window.setTimeout(() => { 23 finish(() => reject(new Error("Payment management did not load"))) 24 }, 10_000) 25 26 script.addEventListener("load", () => { 27 if (window.StripeConnect?.init) { 28 finish(resolve) 29 } else { 30 finish(() => reject(new Error("Payment management did not load"))) 31 } 32 }, { once: true }) 33 script.addEventListener("error", () => { 34 finish(() => reject(new Error("Payment management did not load"))) 35 }, { once: true }) 36 37 if (!existing) { 38 script.src = CONNECT_JS_SRC 39 script.async = true 40 document.head.appendChild(script) 41 } 42 }) 43 44 return window.__chConnectJsPromise 45} 46 47export default class extends Controller { 48 static values = { 49 publishableKey: String, 50 sessionUrl: String, 51 refreshUrl: String, 52 } 53 54 static targets = ["error", "panel", "tab"] 55 56 async connect() { 57 this.components = [] 58 this.handleDocumentSubmit = this.handleDocumentSubmit.bind(this) 59 document.addEventListener("submit", this.handleDocumentSubmit, true) 60 this.clearError() 61 62 try { 63 await ensureConnectJsLoaded() 64 if (!this.element.isConnected) return 65 66 this.connectInstance = window.StripeConnect.init({ 67 publishableKey: this.publishableKeyValue, 68 fetchClientSecret: () => this.fetchClientSecret(), 69 locale: "en-US", 70 appearance: { 71 overlays: "dialog", 72 variables: { 73 colorPrimary: "#18181b", 74 borderRadius: "8px", 75 fontFamily: "ui-sans-serif, system-ui, sans-serif", 76 }, 77 }, 78 displayOptions: { showObjectIds: false }, 79 }) 80 81 this.element.querySelectorAll("[data-embedded-payment-component]").forEach((container) => { 82 this.mountComponent(container) 83 }) 84 } catch (_error) { 85 this.showError("Payment management failed to load. Refresh the page and try again.") 86 } 87 } 88 89 disconnect() { 90 document.removeEventListener("submit", this.handleDocumentSubmit, true) 91 this.components?.forEach((component) => component.remove()) 92 this.components = [] 93 } 94 95 async handleDocumentSubmit(event) { 96 const form = event.target 97 if (!(form instanceof HTMLFormElement)) return 98 if (new URL(form.action, window.location.href).pathname !== "/logout") return 99 if (!this.connectInstance?.logout || form.dataset.paymentCenterLogoutComplete === "true") return 100 101 event.preventDefault() 102 event.stopImmediatePropagation() 103 form.dataset.paymentCenterLogoutComplete = "true" 104 105 try { 106 await this.connectInstance.logout() 107 } finally { 108 HTMLFormElement.prototype.submit.call(form) 109 } 110 } 111 112 showPanel(event) { 113 const selected = event.currentTarget.dataset.panel 114 115 this.tabTargets.forEach((tab) => { 116 const active = tab.dataset.panel === selected 117 tab.setAttribute("aria-selected", active.toString()) 118 tab.classList.toggle("border-zinc-900", active) 119 tab.classList.toggle("text-zinc-900", active) 120 tab.classList.toggle("border-transparent", !active) 121 tab.classList.toggle("text-zinc-500", !active) 122 }) 123 124 this.panelTargets.forEach((panel) => { 125 panel.classList.toggle("hidden", panel.dataset.panel !== selected) 126 }) 127 } 128 129 async fetchClientSecret() { 130 const response = await fetch(this.sessionUrlValue, { 131 method: "POST", 132 credentials: "same-origin", 133 headers: { 134 "Accept": "application/json", 135 "Content-Type": "application/json", 136 "X-CSRF-Token": document.querySelector("meta[name='csrf-token']")?.content || "", 137 }, 138 }) 139 const payload = await response.json() 140 141 if (!response.ok || !payload.client_secret) { 142 throw new Error("Payment management is unavailable") 143 } 144 145 return payload.client_secret 146 } 147 148 mountComponent(container) { 149 const component = this.connectInstance.create(container.dataset.embeddedPaymentComponent) 150 const loading = container.querySelector("[data-component-loading]") 151 152 component.setOnLoaderStart?.(() => loading?.remove()) 153 component.setOnLoadError?.(() => { 154 loading?.remove() 155 this.showError("Part of the payment center could not load. Refresh the page and try again.") 156 }) 157 158 if (container.dataset.embeddedPaymentComponent === "account-onboarding") { 159 component.setOnExit?.(() => this.refreshAfterOnboarding()) 160 } 161 162 container.appendChild(component) 163 this.components.push(component) 164 } 165 166 async refreshAfterOnboarding() { 167 try { 168 await fetch(this.refreshUrlValue, { 169 method: "POST", 170 credentials: "same-origin", 171 headers: { 172 "Accept": "application/json", 173 "X-CSRF-Token": document.querySelector("meta[name='csrf-token']")?.content || "", 174 }, 175 }) 176 } finally { 177 window.location.reload() 178 } 179 } 180 181 showError(message) { 182 if (!this.hasErrorTarget) return 183 184 this.errorTarget.textContent = message 185 this.errorTarget.classList.remove("hidden") 186 } 187 188 clearError() { 189 if (!this.hasErrorTarget) return 190 191 this.errorTarget.textContent = "" 192 this.errorTarget.classList.add("hidden") 193 } 194}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.