@tiquo/dom-package
v1.3.2
Published
Tiquo SDK for third-party websites - authentication, customer profiles, orders, bookings, and enquiries
Maintainers
Readme
@tiquo/dom-package
Tiquo SDK for third-party websites. Integrate authentication, customer profiles, orders, bookings, and enquiries into your website with a simple JavaScript API.
Features
- Email OTP Authentication - Secure passwordless login using email verification codes
- JWT-Based Tokens - Secure access and refresh tokens with automatic refresh
- Multi-Tab Sync - Auth state automatically syncs across all browser tabs
- Native App Integration - WebView token injection for iOS/Android hybrid apps
- Customer Flow Integration - Embed authenticated customer flows with one line of code
- Phone Number Utilities - Validation, formatting, and country code support for 200+ countries
- Framework Agnostic - Works with React, Vue, Svelte, or vanilla JavaScript
- TypeScript Support - Full type definitions included
Installation
npm install @tiquo/dom-package
# or
yarn add @tiquo/dom-package
# or
pnpm add @tiquo/dom-packageQuick Start
1. Get Your Public Key
Go to your Tiquo dashboard → Settings → Auth DOM to get your public key.
2. Initialize the SDK
import { TiquoAuth } from '@tiquo/dom-package';
const auth = new TiquoAuth({
publicKey: 'pk_dom_your_public_key'
});3. Authenticate Users
// Step 1: Send OTP to user's email
await auth.sendOTP('[email protected]');
// Step 2: Verify the OTP code
const result = await auth.verifyOTP('[email protected]', '123456');
// Step 3: Get user data
const session = await auth.getUser();
console.log(session?.user.email); // [email protected]
console.log(session?.customer?.firstName); // JohnAPI Reference
Constructor
const auth = new TiquoAuth({
publicKey: string; // Required: Your public key from Tiquo dashboard
apiEndpoint?: string; // Optional: API endpoint (defaults to production)
storagePrefix?: string; // Optional: localStorage key prefix
debug?: boolean; // Optional: Enable debug logging
enableTabSync?: boolean; // Optional: Multi-tab session sync (default: true)
});Methods
sendOTP(email: string): Promise<SendOTPResult>
Send a verification code to the user's email.
const result = await auth.sendOTP('[email protected]');
// { success: true, message: 'OTP sent' }verifyOTP(email: string, otp: string): Promise<VerifyOTPResult>
Verify the OTP code and authenticate the user.
const result = await auth.verifyOTP('[email protected]', '123456');
// {
// success: true,
// accessToken: 'eyJhbGciOiJSUzI1NiJ9...',
// refreshToken: 'rt_xxx...',
// expiresIn: 3600,
// expiresAt: 1234567890000,
// isNewUser: false,
// hasCustomer: true
// }getUser(): Promise<TiquoSession | null>
Get the current authenticated user and customer data.
const session = await auth.getUser();
if (session) {
console.log(session.user.id);
console.log(session.user.email);
console.log(session.customer?.firstName);
console.log(session.customer?.customerNumber);
}isAuthenticated(): boolean
Check if the user is currently authenticated.
if (auth.isAuthenticated()) {
// User is logged in
}updateProfile(updates): Promise<ProfileUpdateResult>
Update the authenticated customer's profile. Only allows updating the logged-in customer's own data.
Phone numbers are automatically normalized to E.164 format before being sent to the API. You can pass common formats and they'll be cleaned up, but for best results include the country code.
const result = await auth.updateProfile({
firstName: 'John',
lastName: 'Doe',
displayName: 'Johnny',
phone: '+1 (555) 456-7890', // Auto-normalized to +15554567890
});
console.log(result.customer.firstName); // 'John'Available fields:
firstName- Customer's first namelastName- Customer's last namedisplayName- Display name (nickname)phone- Primary phone number (E.164 format recommended:+[country code][number])profilePhoto- URL to profile photophones- Full phone list (array of{ number, isPrimary, order })
Important: Phone numbers should include a country code (e.g.,
+1for US,+44for UK). UseTiquoPhone.buildPhone()to construct properly formatted numbers from a country selector + national number input. See Phone Number Utilities below.
logout(): Promise<void>
Log out the current user.
await auth.logout();getOrders(options?): Promise<GetOrdersResult>
Get the authenticated customer's order history. Only returns orders for the logged-in customer.
// Get all orders (default limit 50)
const { orders, hasMore, nextCursor } = await auth.getOrders();
// With pagination
const page1 = await auth.getOrders({ limit: 10 });
const page2 = await auth.getOrders({ limit: 10, cursor: page1.nextCursor });
// Filter by status
const completedOrders = await auth.getOrders({ status: 'completed' });Options:
limit- Number of orders to return (max 100, default 50)cursor- Order ID to start after (for pagination)status- Filter by status:draft,pending,processing,completed,cancelled,refunded,open_tab
Returns:
orders- Array of order objects with items, totals, and statushasMore- Whether there are more orders to fetchnextCursor- Cursor for the next page (ifhasMoreis true)
getBookings(options?): Promise<GetBookingsResult>
Get the authenticated customer's booking history. Only returns bookings for the logged-in customer.
// Get all bookings (default limit 50)
const { bookings, hasMore, nextCursor } = await auth.getBookings();
// Get upcoming bookings only (sorted soonest first)
const upcomingBookings = await auth.getBookings({ upcoming: true });
// Filter by status
const confirmedBookings = await auth.getBookings({ status: 'confirmed' });
// With pagination
const page1 = await auth.getBookings({ limit: 10 });
const page2 = await auth.getBookings({ limit: 10, cursor: page1.nextCursor });Options:
limit- Number of bookings to return (max 100, default 50)cursor- Booking ID to start after (for pagination)status- Filter by status:draft,scheduled,confirmed,reminder_sent,waiting_room,waiting_list,checked_in,active,in_progress,completed,cancelled,no_show,rescheduledupcoming- If true, only return future bookings (sorted soonest first)
Returns:
bookings- Array of booking objects with service details, date/time, and statushasMore- Whether there are more bookings to fetchnextCursor- Cursor for the next page (ifhasMoreis true)
getEnquiries(options?): Promise<GetEnquiriesResult>
Get the authenticated customer's enquiry history. Only returns enquiries for the logged-in customer.
// Get all enquiries (default limit 50)
const { enquiries, hasMore, nextCursor } = await auth.getEnquiries();
// Filter by status
const openEnquiries = await auth.getEnquiries({ status: 'in_progress' });
// With pagination
const page1 = await auth.getEnquiries({ limit: 10 });
const page2 = await auth.getEnquiries({ limit: 10, cursor: page1.nextCursor });Options:
limit- Number of enquiries to return (max 100, default 50)cursor- Enquiry ID to start after (for pagination)status- Filter by status:new,in_progress,waiting_customer,waiting_internal,won,lost,closed,resolved,archived
Returns:
enquiries- Array of enquiry objects with subject, message, status, and priorityhasMore- Whether there are more enquiries to fetchnextCursor- Cursor for the next page (ifhasMoreis true)
destroy(): void
Clean up resources when destroying the auth instance. Call this when your component unmounts or when you no longer need the auth instance.
// In React
useEffect(() => {
const auth = new TiquoAuth({ publicKey: 'pk_dom_xxx' });
return () => {
auth.destroy(); // Cleanup on unmount
};
}, []);
// Or when done with auth
auth.destroy();onAuthStateChange(callback): () => void
Subscribe to authentication state changes. Returns an unsubscribe function.
const unsubscribe = auth.onAuthStateChange((session) => {
if (session) {
console.log('User logged in:', session.user.email);
} else {
console.log('User logged out');
}
});
// Later: unsubscribe()embedCustomerFlow(flowUrl, container, options?): Promise<HTMLIFrameElement>
Embed a Tiquo customer flow with automatic authentication.
await auth.embedCustomerFlow(
'https://book.tiquo.app/your-flow',
'#container',
{
width: '100%',
height: '600px',
onLoad: () => console.log('Flow loaded'),
onError: (err) => console.error('Flow error:', err)
}
);getIframeToken(customerFlowId?): Promise<IframeTokenResult>
Generate a short-lived token for manual iframe authentication.
const { token, expiresAt } = await auth.getIframeToken();
// Use token in iframe URL: ?_auth_token=xxxReact Integration
import { TiquoAuth } from '@tiquo/dom-package';
import { useState, useEffect } from 'react';
const auth = new TiquoAuth({ publicKey: 'pk_dom_xxx' });
function App() {
const [session, setSession] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Subscribe to auth changes
const unsubscribe = auth.onAuthStateChange((s) => {
setSession(s);
setLoading(false);
});
return unsubscribe;
}, []);
if (loading) return <div>Loading...</div>;
if (!session) {
return <LoginForm />;
}
return (
<div>
<p>Welcome, {session.user.email}!</p>
<button onClick={() => auth.logout()}>Logout</button>
</div>
);
}
function LoginForm() {
const [email, setEmail] = useState('');
const [otp, setOtp] = useState('');
const [step, setStep] = useState<'email' | 'otp'>('email');
const [error, setError] = useState('');
const handleSendOTP = async (e) => {
e.preventDefault();
try {
await auth.sendOTP(email);
setStep('otp');
} catch (err) {
setError(err.message);
}
};
const handleVerifyOTP = async (e) => {
e.preventDefault();
try {
await auth.verifyOTP(email, otp);
// Auth state change will update the UI
} catch (err) {
setError(err.message);
}
};
if (step === 'email') {
return (
<form onSubmit={handleSendOTP}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
required
/>
<button type="submit">Send Code</button>
{error && <p>{error}</p>}
</form>
);
}
return (
<form onSubmit={handleVerifyOTP}>
<p>Enter the code sent to {email}</p>
<input
type="text"
value={otp}
onChange={(e) => setOtp(e.target.value)}
placeholder="000000"
maxLength={6}
required
/>
<button type="submit">Verify</button>
{error && <p>{error}</p>}
</form>
);
}Vue Integration
<script setup>
import { TiquoAuth } from '@tiquo/dom-package';
import { ref, onMounted, onUnmounted } from 'vue';
const auth = new TiquoAuth({ publicKey: 'pk_dom_xxx' });
const session = ref(null);
const loading = ref(true);
let unsubscribe;
onMounted(() => {
unsubscribe = auth.onAuthStateChange((s) => {
session.value = s;
loading.value = false;
});
});
onUnmounted(() => {
unsubscribe?.();
auth.destroy(); // Clean up resources including tab sync
});
async function logout() {
await auth.logout();
}
</script>
<template>
<div v-if="loading">Loading...</div>
<div v-else-if="session">
<p>Welcome, {{ session.user.email }}!</p>
<button @click="logout">Logout</button>
</div>
<LoginForm v-else />
</template>Phone Number Utilities
The SDK includes TiquoPhone, a static utility class for working with international phone numbers. It supports 200+ countries and handles validation, normalization, formatting, and country detection — everything you need to build a phone input with a country selector.
Import
import { TiquoPhone } from '@tiquo/dom-package';Get Countries (for building a dropdown)
const countries = TiquoPhone.getCountries();
// Returns alphabetically sorted array:
// [
// { code: "AF", name: "Afghanistan", dialCode: "+93", format: "XX XXX XXXX", flag: "🇦🇫" },
// ...
// { code: "GB", name: "United Kingdom", dialCode: "+44", format: "XXXX XXXXXX", flag: "🇬🇧" },
// { code: "US", name: "United States", dialCode: "+1", format: "(XXX) XXX-XXXX", flag: "🇺🇸" },
// ...
// ]Build a Phone Number (country selector + input)
// Combine a country selector value with a local number input
TiquoPhone.buildPhone("GB", "07911 123456"); // → "+447911123456"
TiquoPhone.buildPhone("US", "(555) 456-7890"); // → "+15554567890"
TiquoPhone.buildPhone("FR", "06 65 08 50 44"); // → "+33665085044"Validate a Phone Number
TiquoPhone.validate("+44 7911 123456");
// → { valid: true, normalized: "+447911123456", countryCode: "GB" }
TiquoPhone.validate("555-1234");
// → { valid: false, reason: "Phone number must include a country code..." }
TiquoPhone.validate("+999 123");
// → { valid: false, reason: "Phone number is too short..." }Normalize to E.164
TiquoPhone.normalize("+1 (555) 456-7890"); // → "+15554567890"
TiquoPhone.normalize("+44 (0)20 3227 4972"); // → "+442032274972"
TiquoPhone.normalize("0033 6 65 08 50 44"); // → "+33665085044"
TiquoPhone.normalize("+49 0151 12345678"); // → "+4915112345678"Format for Display
TiquoPhone.format("+15554567890"); // → "+1 (555) 456-7890"
TiquoPhone.format("+447911123456"); // → "+44 7911 123456"
TiquoPhone.format("+33665085044"); // → "+33 6 65 08 50 44"Detect Country
TiquoPhone.detectCountry("+15554567890"); // → "US"
TiquoPhone.detectCountry("+447911123456"); // → "GB"
TiquoPhone.detectCountry("+33665085044"); // → "FR"Get Dial Code
TiquoPhone.getDialCode("US"); // → "+1"
TiquoPhone.getDialCode("GB"); // → "+44"
TiquoPhone.getDialCode("FR"); // → "+33"Example: Phone Input with Country Selector (React)
import { TiquoPhone } from '@tiquo/dom-package';
import { useState } from 'react';
function PhoneInput({ onChange }) {
const countries = TiquoPhone.getCountries();
const [country, setCountry] = useState('US');
const [number, setNumber] = useState('');
const [error, setError] = useState('');
const handleChange = (newCountry, newNumber) => {
setCountry(newCountry);
setNumber(newNumber);
if (!newNumber.trim()) {
setError('');
onChange('');
return;
}
const phone = TiquoPhone.buildPhone(newCountry, newNumber);
if (!phone) {
setError('Invalid phone number');
onChange('');
return;
}
const result = TiquoPhone.validate(phone);
if (result.valid) {
setError('');
onChange(result.normalized);
} else {
setError(result.reason);
onChange('');
}
};
return (
<div>
<select
value={country}
onChange={(e) => handleChange(e.target.value, number)}
>
{countries.map((c) => (
<option key={c.code} value={c.code}>
{c.flag} {c.name} ({c.dialCode})
</option>
))}
</select>
<input
type="tel"
value={number}
onChange={(e) => handleChange(country, e.target.value)}
placeholder={countries.find(c => c.code === country)?.format}
/>
{error && <p style={{ color: 'red' }}>{error}</p>}
</div>
);
}
// Usage with updateProfile:
function ProfilePage() {
const [phone, setPhone] = useState('');
const handleSave = async () => {
await auth.updateProfile({ phone }); // Already in E.164 format
};
return (
<div>
<PhoneInput onChange={setPhone} />
<button onClick={handleSave} disabled={!phone}>Save</button>
</div>
);
}Multi-Tab Session Sync
The SDK automatically synchronizes authentication state across all browser tabs using the BroadcastChannel API. This means:
- Login in one tab → All tabs are authenticated
- Logout in one tab → All tabs are logged out
- Profile updates sync across tabs
This feature is enabled by default and works automatically. No additional code is required.
Disabling Tab Sync
If you need to disable multi-tab sync (e.g., for isolated sessions):
const auth = new TiquoAuth({
publicKey: 'pk_dom_xxx',
enableTabSync: false, // Disable multi-tab sync
});Browser Support
Multi-tab sync requires BroadcastChannel support. It's available in all modern browsers:
- Chrome 54+
- Firefox 38+
- Safari 15.4+
- Edge 79+
For older browsers, the feature gracefully degrades - auth still works normally, just without cross-tab sync.
Error Handling
All SDK methods can throw TiquoAuthError with helpful error codes:
import { TiquoAuth, TiquoAuthError } from '@tiquo/dom-package';
try {
await auth.verifyOTP(email, otp);
} catch (error) {
if (error instanceof TiquoAuthError) {
switch (error.code) {
case 'OTP_VERIFY_FAILED':
console.log('Invalid or expired code');
break;
case 'NOT_AUTHENTICATED':
console.log('Please log in first');
break;
default:
console.log('Error:', error.message);
}
}
}Native App WebView Integration
When building hybrid apps with iOS/Android native + WebView, you can pass authentication tokens from the native app to the DOM package.
Method 1: Config Parameters
const auth = new TiquoAuth({
publicKey: 'pk_dom_xxx',
accessToken: 'eyJhbGciOiJSUzI1NiJ9...', // From OAuth flow
refreshToken: 'rt_xxx...'
});Method 2: Global Variable (Native App Injects JS)
iOS (Swift):
let script = """
window.__TIQUO_INIT_TOKEN__ = {
accessToken: '\(accessToken)',
refreshToken: '\(refreshToken)'
};
"""
let userScript = WKUserScript(source: script, injectionTime: .atDocumentStart, forMainFrameOnly: true)
webView.configuration.userContentController.addUserScript(userScript)Android (Kotlin):
webView.evaluateJavascript("""
window.__TIQUO_INIT_TOKEN__ = {
accessToken: '$accessToken',
refreshToken: '$refreshToken'
};
""".trimIndent(), null)Method 3: URL Fragment
https://yoursite.com/page#access_token=eyJ...&refresh_token=rt_...The DOM package automatically detects and consumes tokens from all three methods. The user will be immediately authenticated in the WebView without needing to log in again.
For complete integration examples, see the Client API documentation.
Token Management
The SDK uses JWT tokens for authentication:
- Access Token: Short-lived (1 hour), used for API requests
- Refresh Token: Long-lived (30 days), used to obtain new access tokens
- Automatic Refresh: Tokens are automatically refreshed before expiration
// Get tokens for manual use (advanced)
const accessToken = auth.getAccessToken();
const refreshToken = auth.getRefreshToken();
// Initialize with external tokens
auth.initWithTokens(accessToken, refreshToken);Security Considerations
- The public key is safe to expose in client-side code
- Tokens are stored in localStorage with automatic refresh
- Access tokens expire after 1 hour (automatically refreshed)
- Refresh tokens expire after 30 days
- OTP codes expire after 10 minutes
- Failed OTP attempts are rate limited (max 5 attempts per code)
Support
License
MIT
