You've already forked AstralRinth
forked from didirus/AstralRinth
feat: temporary tax compliance impl (#4393)
* feat: temporary tax compliance impl * fix: lint & intl * Update banner, reload page on submit, and fix withdraw button disabled state --------- Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
This commit is contained in:
187
apps/frontend/src/composables/avalara1099.ts
Normal file
187
apps/frontend/src/composables/avalara1099.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
import type { Ref } from 'vue'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
export interface FormRequestAttributes {
|
||||
form_type: 'W-9' | 'W-8BEN' | 'W-8BEN-E' | string
|
||||
company_id: number
|
||||
company_name: string
|
||||
company_email: string
|
||||
reference_id: string | null
|
||||
form_id: string | null
|
||||
signed_at: string | null
|
||||
tin_match_status: string | null
|
||||
expires_at: string | null
|
||||
}
|
||||
|
||||
export interface FormRequestLinks {
|
||||
action_validate?: string
|
||||
action_complete?: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export interface FormRequestData {
|
||||
id: string
|
||||
type: 'form_request' | string
|
||||
attributes: FormRequestAttributes
|
||||
links?: FormRequestLinks
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export interface FormRequestResponse {
|
||||
data: FormRequestData
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export interface UseAvalara1099Options {
|
||||
prefill?: Record<string, unknown>
|
||||
// Optional override for the origin (defaults to vendor CDN domain)
|
||||
origin?: string
|
||||
// Optional hook to further style the injected dialog/iframe
|
||||
styleDialog?: (dialog: HTMLDialogElement, iframe: HTMLIFrameElement | null) => void
|
||||
// Poll interval while waiting for global to appear
|
||||
pollIntervalMs?: number
|
||||
// Max time to wait for script before rejecting (ms); 0/undefined => no timeout
|
||||
timeoutMs?: number
|
||||
}
|
||||
|
||||
interface AvalaraGlobal {
|
||||
requestW9: (
|
||||
formRequest: FormRequestResponse | FormRequestData,
|
||||
opts?: { prefill?: Record<string, unknown> },
|
||||
) => Promise<FormRequestResponse> | any
|
||||
requestW8BEN: (
|
||||
formRequest: FormRequestResponse | FormRequestData,
|
||||
opts?: { prefill?: Record<string, unknown> },
|
||||
) => Promise<FormRequestResponse> | any
|
||||
requestW8BENE: (
|
||||
formRequest: FormRequestResponse | FormRequestData,
|
||||
opts?: { prefill?: Record<string, unknown> },
|
||||
) => Promise<FormRequestResponse> | any
|
||||
origin?: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
Avalara1099?: AvalaraGlobal
|
||||
}
|
||||
}
|
||||
|
||||
const injectedKey = '__avalara1099_script_injected__'
|
||||
|
||||
function ensureScriptInjected(origin: string) {
|
||||
if (import.meta.server) return
|
||||
const w = window as any
|
||||
if (w[injectedKey]) return
|
||||
w[injectedKey] = true
|
||||
useHead({
|
||||
script: [
|
||||
{
|
||||
src: `${origin.replace(/\/$/, '')}/api/request_form.js`,
|
||||
crossorigin: 'anonymous',
|
||||
type: 'module',
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
async function waitForAvalara(opts: {
|
||||
pollIntervalMs: number
|
||||
timeoutMs?: number
|
||||
origin: string
|
||||
}): Promise<AvalaraGlobal> {
|
||||
if (import.meta.server) throw new Error('Avalara 1099 is client-side only')
|
||||
ensureScriptInjected(opts.origin)
|
||||
const start = Date.now()
|
||||
return await new Promise((resolve, reject) => {
|
||||
const poll = () => {
|
||||
const g = window.Avalara1099
|
||||
if (g) return resolve(g)
|
||||
if (opts.timeoutMs && opts.timeoutMs > 0 && Date.now() - start > opts.timeoutMs) {
|
||||
return reject(new Error('Timed out waiting for Avalara1099 script to load'))
|
||||
}
|
||||
setTimeout(poll, opts.pollIntervalMs)
|
||||
}
|
||||
poll()
|
||||
})
|
||||
}
|
||||
|
||||
export function useAvalara1099(
|
||||
initial: FormRequestResponse | FormRequestData,
|
||||
options: UseAvalara1099Options = {},
|
||||
) {
|
||||
const origin = options.origin || 'https://www.track1099.com'
|
||||
const pollIntervalMs = options.pollIntervalMs ?? 250
|
||||
const timeoutMs = options.timeoutMs
|
||||
|
||||
const request: Ref<FormRequestResponse | FormRequestData> = ref(initial)
|
||||
const loading = ref(false)
|
||||
const error: Ref<unknown> = ref(null)
|
||||
|
||||
const signedAt = computed(() => {
|
||||
const data = (request.value as FormRequestResponse).data || request.value
|
||||
return data.attributes?.signed_at ? new Date(data.attributes.signed_at) : null
|
||||
})
|
||||
|
||||
const status = computed(() => (signedAt.value ? 'signed' : 'incomplete'))
|
||||
|
||||
async function start(): Promise<FormRequestResponse | FormRequestData> {
|
||||
loading.value = true
|
||||
error.value = null
|
||||
try {
|
||||
const g = await waitForAvalara({ pollIntervalMs, timeoutMs, origin })
|
||||
const data = (request.value as FormRequestResponse).data || request.value
|
||||
const formType = data.attributes?.form_type
|
||||
|
||||
// Defensive deep clone to strip proxies / non-cloneable refs before postMessage
|
||||
// (DataCloneError guard)
|
||||
let safeRequest: any
|
||||
try {
|
||||
safeRequest = JSON.parse(JSON.stringify(request.value))
|
||||
} catch {
|
||||
// Fallback shallow copy
|
||||
safeRequest = Array.isArray(request.value)
|
||||
? [...(request.value as any)]
|
||||
: { ...(request.value as any) }
|
||||
}
|
||||
let safePrefill: any = undefined
|
||||
if (options.prefill) {
|
||||
try {
|
||||
safePrefill = JSON.parse(JSON.stringify(options.prefill))
|
||||
} catch {
|
||||
safePrefill = { ...options.prefill }
|
||||
}
|
||||
}
|
||||
|
||||
let promise: any
|
||||
if (formType === 'W-8BEN') {
|
||||
promise = g.requestW8BEN(safeRequest, { prefill: safePrefill })
|
||||
} else if (formType === 'W-9') {
|
||||
promise = g.requestW9(safeRequest, { prefill: safePrefill })
|
||||
} else if (formType === 'W-8BEN-E' || formType === 'W-8BEN E') {
|
||||
promise = g.requestW8BENE(safeRequest, { prefill: safePrefill })
|
||||
} else {
|
||||
throw new Error(`Unsupported form_type: ${formType}`)
|
||||
}
|
||||
|
||||
// The vendor promise resolves with an updated form request (signed state)
|
||||
const newReq = await promise
|
||||
request.value = newReq
|
||||
return newReq
|
||||
} catch (e) {
|
||||
error.value = e
|
||||
throw e
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
start,
|
||||
request,
|
||||
signedAt,
|
||||
status, // 'signed' | 'incomplete'
|
||||
loading,
|
||||
error,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user