@tiquo/dom-package 1.4.1 → 1.5.1

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/README.md CHANGED
@@ -137,12 +137,24 @@ const result = await auth.updateProfile({
137
137
  console.log(result.customer.firstName); // 'John'
138
138
  ```
139
139
 
140
+ Profile photos can be passed as a URL, a browser `File`/`Blob`, or a `data:image/...`/`blob:...` URI:
141
+
142
+ ```typescript
143
+ const file = fileInput.files?.[0];
144
+ if (file) {
145
+ await auth.updateProfile({ profilePhoto: file });
146
+ }
147
+
148
+ // Or upload just the photo:
149
+ await auth.uploadProfilePhoto(file);
150
+ ```
151
+
140
152
  **Available fields:**
141
153
  - `firstName` - Customer's first name
142
154
  - `lastName` - Customer's last name
143
155
  - `displayName` - Display name (nickname)
144
156
  - `phone` - Primary phone number (E.164 format recommended: `+[country code][number]`)
145
- - `profilePhoto` - URL to profile photo
157
+ - `profilePhoto` - URL, File/Blob, or `data:image/...`/`blob:...` URI for profile photo
146
158
  - `phones` - Full phone list (array of `{ number, isPrimary, order }`)
147
159
 
148
160
  > **Important:** Phone numbers should include a country code (e.g., `+1` for US, `+44` for UK). Use `TiquoPhone.buildPhone()` to construct properly formatted numbers from a country selector + national number input. See [Phone Number Utilities](#phone-number-utilities) below.
package/dist/index.d.mts CHANGED
@@ -209,6 +209,18 @@ declare function buildPhone(countryCode: string, nationalNumber: string): string
209
209
  * ```
210
210
  */
211
211
 
212
+ declare class TiquoCMS {
213
+ private config;
214
+ constructor(config: TiquoCMSConfig);
215
+ /**
216
+ * Fetch a published CMS page for the website attached to this public key.
217
+ *
218
+ * This intentionally goes through the Tiquo edge API rather than exposing
219
+ * Convex connection details to the consuming website.
220
+ */
221
+ getPage(request: TiquoCMSPageRequest): Promise<TiquoCMSPage | null>;
222
+ private log;
223
+ }
212
224
  interface TiquoAuthConfig {
213
225
  /** Public key from your Tiquo Auth DOM settings */
214
226
  publicKey: string;
@@ -225,6 +237,53 @@ interface TiquoAuthConfig {
225
237
  /** Pre-authenticated refresh token (for WebView injection from native apps) */
226
238
  refreshToken?: string;
227
239
  }
240
+ interface TiquoCMSConfig {
241
+ /** Public key from your Tiquo Auth DOM settings */
242
+ publicKey: string;
243
+ /** API endpoint (defaults to production) */
244
+ apiEndpoint?: string;
245
+ /** Enable debug logging */
246
+ debug?: boolean;
247
+ }
248
+ interface TiquoCMSPageRequest {
249
+ /** CMS website slug */
250
+ siteSlug: string;
251
+ /** CMS page slug, defaults to "home" */
252
+ pageSlug?: string;
253
+ /** Optional template contract guard */
254
+ templateId?: string;
255
+ }
256
+ interface TiquoCMSBlock {
257
+ id: string;
258
+ type: string;
259
+ props: Record<string, unknown>;
260
+ locked?: boolean;
261
+ lockedBy?: string;
262
+ required?: boolean;
263
+ }
264
+ interface TiquoCMSPage {
265
+ id: string;
266
+ site: {
267
+ id: string;
268
+ name: string;
269
+ slug?: string;
270
+ templateId?: string;
271
+ templateVersion?: number;
272
+ globalConfig?: unknown;
273
+ };
274
+ page: {
275
+ id: string;
276
+ title: string;
277
+ slug: string;
278
+ path: string;
279
+ blocks: TiquoCMSBlock[];
280
+ settings?: unknown;
281
+ metaTitle?: string;
282
+ metaDescription?: string;
283
+ publishedAt?: number;
284
+ updatedAt: number;
285
+ };
286
+ }
228
287
  declare global {
229
288
  interface Window {
230
289
  __TIQUO_INIT_TOKEN__?: {
@@ -305,7 +364,13 @@ interface ProfileUpdateData {
305
364
  * for country+national input, or TiquoPhone.validate() to pre-check.
306
365
  */
307
366
  phone?: string;
308
- profilePhoto?: string;
367
+ /**
368
+ * URL to a profile photo, a data/blob URI, or a browser File/Blob.
369
+ *
370
+ * File/Blob and data/blob URI values are uploaded to Tiquo storage before the
371
+ * profile is updated. Remote URLs are stored as-is.
372
+ */
373
+ profilePhoto?: string | Blob;
309
374
  emails?: TiquoCustomerEmail[];
310
375
  /**
311
376
  * Full phone list. Each number should be in E.164 format: +[country code][number].
@@ -317,6 +382,10 @@ interface ProfileUpdateResult {
317
382
  success: boolean;
318
383
  customer: TiquoCustomer;
319
384
  }
385
+ interface ProfilePhotoUploadResult extends ProfileUpdateResult {
386
+ profilePhoto: string;
387
+ storageId: string;
388
+ }
320
389
  interface TiquoOrderItem {
321
390
  id: string;
322
391
  name: string;
@@ -492,6 +561,80 @@ interface GetEnquiriesResult {
492
561
  hasMore: boolean;
493
562
  nextCursor?: string;
494
563
  }
564
+ type TiquoCompanyRelationship = 'employee' | 'contractor' | 'manager' | 'executive' | 'owner' | 'contact' | 'other';
565
+ /**
566
+ * The authenticated customer's membership in a company. Each company the
567
+ * customer belongs to has its own membership record — a customer can be
568
+ * an admin of one company and a regular employee of another.
569
+ */
570
+ interface TiquoCompanyMembership {
571
+ relationship: TiquoCompanyRelationship;
572
+ title?: string;
573
+ department?: string;
574
+ isPrimaryContact: boolean;
575
+ /** True when this customer can call `getCompanyColleagues(company.id)`. */
576
+ isCompanyAdmin: boolean;
577
+ startDate?: number;
578
+ }
579
+ /**
580
+ * A company the authenticated customer is a member of. Bundles the
581
+ * company's public-ish details with the caller's own role inside it
582
+ * (`membership`). CRM/financial fields are intentionally omitted.
583
+ */
584
+ interface TiquoCompany {
585
+ id: string;
586
+ name: string;
587
+ displayName?: string;
588
+ companyNumber: string;
589
+ description?: string;
590
+ email?: string;
591
+ phone?: string;
592
+ website?: string;
593
+ websites?: string[];
594
+ logo?: string;
595
+ addressLine1?: string;
596
+ addressLine2?: string;
597
+ city?: string;
598
+ state?: string;
599
+ postalCode?: string;
600
+ country?: string;
601
+ industry?: string;
602
+ companySize?: '1-10' | '11-50' | '51-200' | '201-500' | '501-1000' | '1000+';
603
+ status: 'active' | 'inactive' | 'archived';
604
+ type?: 'client' | 'prospect' | 'vendor' | 'partner' | 'other';
605
+ membership: TiquoCompanyMembership;
606
+ }
607
+ interface GetCompaniesResult {
608
+ companies: TiquoCompany[];
609
+ }
610
+ /**
611
+ * A colleague — another customer in the same company. Returned only to
612
+ * Company Admins via `getCompanyColleagues()`. Limited to display-card
613
+ * info; CRM/financial data is never exposed.
614
+ */
615
+ interface TiquoCompanyColleague {
616
+ id: string;
617
+ firstName?: string;
618
+ lastName?: string;
619
+ displayName?: string;
620
+ profilePhoto?: string;
621
+ email?: string;
622
+ phone?: string;
623
+ relationship: TiquoCompanyRelationship;
624
+ title?: string;
625
+ department?: string;
626
+ isPrimaryContact: boolean;
627
+ isCompanyAdmin: boolean;
628
+ /** True when this colleague is the authenticated customer themselves. */
629
+ isSelf: boolean;
630
+ }
631
+ interface GetCompanyColleaguesResult {
632
+ company: {
633
+ id: string;
634
+ name: string;
635
+ };
636
+ colleagues: TiquoCompanyColleague[];
637
+ }
495
638
  type AuthStateChangeCallback = (session: TiquoSession | null) => void;
496
639
  declare class TiquoAuthError extends Error {
497
640
  code: string;
@@ -541,6 +684,14 @@ declare class TiquoAuth {
541
684
  * to construct from a country selector and national number.
542
685
  */
543
686
  updateProfile(updates: ProfileUpdateData): Promise<ProfileUpdateResult>;
687
+ /**
688
+ * Upload and save the authenticated customer's profile photo.
689
+ *
690
+ * Accepts a browser File/Blob, `data:image/...` URI, or `blob:...` URI. The
691
+ * image is uploaded to Tiquo storage first, and the customer profile is
692
+ * updated with the URL.
693
+ */
694
+ uploadProfilePhoto(photo: Blob | string): Promise<ProfilePhotoUploadResult>;
544
695
  /**
545
696
  * Log out the current user
546
697
  */
@@ -603,6 +754,32 @@ declare class TiquoAuth {
603
754
  * Only returns enquiries for the logged-in customer
604
755
  */
605
756
  getEnquiries(options?: GetEnquiriesOptions): Promise<GetEnquiriesResult>;
757
+ /**
758
+ * Get the companies the authenticated customer belongs to.
759
+ *
760
+ * Each entry includes the customer's role inside that company via
761
+ * `membership` — branch on `membership.isCompanyAdmin` to decide whether
762
+ * to render admin features (e.g. a colleagues list).
763
+ *
764
+ * Returns an empty array (not an error) when the customer isn't a
765
+ * member of any company.
766
+ */
767
+ getCompanies(): Promise<GetCompaniesResult>;
768
+ /**
769
+ * Get the colleagues (other customers) inside a company the authenticated
770
+ * customer is a member of.
771
+ *
772
+ * Restricted to Company Admins — if the caller isn't flagged as
773
+ * `isCompanyAdmin` on that company, this throws `TiquoAuthError` with
774
+ * code `NOT_COMPANY_ADMIN` (status 403). Check `membership.isCompanyAdmin`
775
+ * from `getCompanies()` before calling this if you want to render the
776
+ * admin UI conditionally.
777
+ *
778
+ * Each colleague includes basic contact info and their relationship to
779
+ * the company. The authenticated customer is included in the list with
780
+ * `isSelf: true` so the UI can highlight or skip them.
781
+ */
782
+ getCompanyColleagues(companyId: string): Promise<GetCompanyColleaguesResult>;
606
783
  /**
607
784
  * Generate a short-lived token for customer flow iframe authentication
608
785
  */
@@ -647,6 +824,9 @@ declare class TiquoAuth {
647
824
  * Check for tokens injected by native apps (WebView integration)
648
825
  */
649
826
  private checkForInjectedTokens;
827
+ private extractUploadableProfilePhoto;
828
+ private isProfilePhotoBlobUrl;
829
+ private profilePhotoToBlob;
650
830
  private request;
651
831
  /**
652
832
  * Ensure we have a valid access token, refreshing if necessary
@@ -691,11 +871,14 @@ declare function useTiquoAuth(auth: TiquoAuth): {
691
871
  verifyOTP: (email: string, otp: string) => Promise<VerifyOTPResult>;
692
872
  logout: () => Promise<void>;
693
873
  updateProfile: (updates: ProfileUpdateData) => Promise<ProfileUpdateResult>;
874
+ uploadProfilePhoto: (photo: Blob | string) => Promise<ProfilePhotoUploadResult>;
694
875
  getOrders: (options?: GetOrdersOptions) => Promise<GetOrdersResult>;
695
876
  getBookings: (options?: GetBookingsOptions) => Promise<GetBookingsResult>;
696
877
  getUpcomingBookings: (options?: Omit<GetBookingsOptions, "upcoming">) => Promise<GetBookingsResult>;
697
878
  getReceipt: (orderId: string) => Promise<TiquoReceipt>;
698
879
  getEnquiries: (options?: GetEnquiriesOptions) => Promise<GetEnquiriesResult>;
880
+ getCompanies: () => Promise<GetCompaniesResult>;
881
+ getCompanyColleagues: (companyId: string) => Promise<GetCompanyColleaguesResult>;
699
882
  getIframeToken: (flowId?: string) => Promise<IframeTokenResult>;
700
883
  embedCustomerFlow: (flowUrl: string, container: HTMLElement | string, options?: {
701
884
  width?: string;
@@ -758,4 +941,4 @@ declare class TiquoPhone {
758
941
  static buildPhone: typeof buildPhone;
759
942
  }
760
943
 
761
- export { type AuthStateChangeCallback, type CountryPhoneInfo, type GetBookingsOptions, type GetBookingsResult, type GetEnquiriesOptions, type GetEnquiriesResult, type GetOrdersOptions, type GetOrdersResult, type IframeTokenResult, type PhoneValidationResult, type ProfileUpdateData, type ProfileUpdateResult, type SendOTPResult, TiquoAuth, type TiquoAuthConfig, TiquoAuthError, type TiquoBooking, type TiquoCustomer, type TiquoCustomerEmail, type TiquoCustomerPhone, type TiquoEnquiry, type TiquoOrder, type TiquoOrderItem, TiquoPhone, type TiquoReceipt, type TiquoReceiptBusiness, type TiquoReceiptCustomer, type TiquoReceiptItem, type TiquoReceiptOrder, type TiquoSession, type TiquoUser, type VerifyOTPResult, addCustomerUserId, clearCachedEmail, TiquoAuth as default, getCustomerUserIds, getPrefilledEmailFromCookie, isDashboardSession, trackCustomerPresence, useTiquoAuth };
944
+ export { type AuthStateChangeCallback, type CountryPhoneInfo, type GetBookingsOptions, type GetBookingsResult, type GetCompaniesResult, type GetCompanyColleaguesResult, type GetEnquiriesOptions, type GetEnquiriesResult, type GetOrdersOptions, type GetOrdersResult, type IframeTokenResult, type PhoneValidationResult, type ProfilePhotoUploadResult, type ProfileUpdateData, type ProfileUpdateResult, type SendOTPResult, TiquoAuth, type TiquoAuthConfig, TiquoAuthError, type TiquoBooking, TiquoCMS, type TiquoCMSBlock, type TiquoCMSConfig, type TiquoCMSPage, type TiquoCMSPageRequest, type TiquoCompany, type TiquoCompanyColleague, type TiquoCompanyMembership, type TiquoCompanyRelationship, type TiquoCustomer, type TiquoCustomerEmail, type TiquoCustomerPhone, type TiquoEnquiry, type TiquoOrder, type TiquoOrderItem, TiquoPhone, type TiquoReceipt, type TiquoReceiptBusiness, type TiquoReceiptCustomer, type TiquoReceiptItem, type TiquoReceiptOrder, type TiquoSession, type TiquoUser, type VerifyOTPResult, addCustomerUserId, clearCachedEmail, TiquoAuth as default, getCustomerUserIds, getPrefilledEmailFromCookie, isDashboardSession, trackCustomerPresence, useTiquoAuth };
package/dist/index.d.ts CHANGED
@@ -209,6 +209,18 @@ declare function buildPhone(countryCode: string, nationalNumber: string): string
209
209
  * ```
210
210
  */
211
211
 
212
+ declare class TiquoCMS {
213
+ private config;
214
+ constructor(config: TiquoCMSConfig);
215
+ /**
216
+ * Fetch a published CMS page for the website attached to this public key.
217
+ *
218
+ * This intentionally goes through the Tiquo edge API rather than exposing
219
+ * Convex connection details to the consuming website.
220
+ */
221
+ getPage(request: TiquoCMSPageRequest): Promise<TiquoCMSPage | null>;
222
+ private log;
223
+ }
212
224
  interface TiquoAuthConfig {
213
225
  /** Public key from your Tiquo Auth DOM settings */
214
226
  publicKey: string;
@@ -225,6 +237,53 @@ interface TiquoAuthConfig {
225
237
  /** Pre-authenticated refresh token (for WebView injection from native apps) */
226
238
  refreshToken?: string;
227
239
  }
240
+ interface TiquoCMSConfig {
241
+ /** Public key from your Tiquo Auth DOM settings */
242
+ publicKey: string;
243
+ /** API endpoint (defaults to production) */
244
+ apiEndpoint?: string;
245
+ /** Enable debug logging */
246
+ debug?: boolean;
247
+ }
248
+ interface TiquoCMSPageRequest {
249
+ /** CMS website slug */
250
+ siteSlug: string;
251
+ /** CMS page slug, defaults to "home" */
252
+ pageSlug?: string;
253
+ /** Optional template contract guard */
254
+ templateId?: string;
255
+ }
256
+ interface TiquoCMSBlock {
257
+ id: string;
258
+ type: string;
259
+ props: Record<string, unknown>;
260
+ locked?: boolean;
261
+ lockedBy?: string;
262
+ required?: boolean;
263
+ }
264
+ interface TiquoCMSPage {
265
+ id: string;
266
+ site: {
267
+ id: string;
268
+ name: string;
269
+ slug?: string;
270
+ templateId?: string;
271
+ templateVersion?: number;
272
+ globalConfig?: unknown;
273
+ };
274
+ page: {
275
+ id: string;
276
+ title: string;
277
+ slug: string;
278
+ path: string;
279
+ blocks: TiquoCMSBlock[];
280
+ settings?: unknown;
281
+ metaTitle?: string;
282
+ metaDescription?: string;
283
+ publishedAt?: number;
284
+ updatedAt: number;
285
+ };
286
+ }
228
287
  declare global {
229
288
  interface Window {
230
289
  __TIQUO_INIT_TOKEN__?: {
@@ -305,7 +364,13 @@ interface ProfileUpdateData {
305
364
  * for country+national input, or TiquoPhone.validate() to pre-check.
306
365
  */
307
366
  phone?: string;
308
- profilePhoto?: string;
367
+ /**
368
+ * URL to a profile photo, a data/blob URI, or a browser File/Blob.
369
+ *
370
+ * File/Blob and data/blob URI values are uploaded to Tiquo storage before the
371
+ * profile is updated. Remote URLs are stored as-is.
372
+ */
373
+ profilePhoto?: string | Blob;
309
374
  emails?: TiquoCustomerEmail[];
310
375
  /**
311
376
  * Full phone list. Each number should be in E.164 format: +[country code][number].
@@ -317,6 +382,10 @@ interface ProfileUpdateResult {
317
382
  success: boolean;
318
383
  customer: TiquoCustomer;
319
384
  }
385
+ interface ProfilePhotoUploadResult extends ProfileUpdateResult {
386
+ profilePhoto: string;
387
+ storageId: string;
388
+ }
320
389
  interface TiquoOrderItem {
321
390
  id: string;
322
391
  name: string;
@@ -492,6 +561,80 @@ interface GetEnquiriesResult {
492
561
  hasMore: boolean;
493
562
  nextCursor?: string;
494
563
  }
564
+ type TiquoCompanyRelationship = 'employee' | 'contractor' | 'manager' | 'executive' | 'owner' | 'contact' | 'other';
565
+ /**
566
+ * The authenticated customer's membership in a company. Each company the
567
+ * customer belongs to has its own membership record — a customer can be
568
+ * an admin of one company and a regular employee of another.
569
+ */
570
+ interface TiquoCompanyMembership {
571
+ relationship: TiquoCompanyRelationship;
572
+ title?: string;
573
+ department?: string;
574
+ isPrimaryContact: boolean;
575
+ /** True when this customer can call `getCompanyColleagues(company.id)`. */
576
+ isCompanyAdmin: boolean;
577
+ startDate?: number;
578
+ }
579
+ /**
580
+ * A company the authenticated customer is a member of. Bundles the
581
+ * company's public-ish details with the caller's own role inside it
582
+ * (`membership`). CRM/financial fields are intentionally omitted.
583
+ */
584
+ interface TiquoCompany {
585
+ id: string;
586
+ name: string;
587
+ displayName?: string;
588
+ companyNumber: string;
589
+ description?: string;
590
+ email?: string;
591
+ phone?: string;
592
+ website?: string;
593
+ websites?: string[];
594
+ logo?: string;
595
+ addressLine1?: string;
596
+ addressLine2?: string;
597
+ city?: string;
598
+ state?: string;
599
+ postalCode?: string;
600
+ country?: string;
601
+ industry?: string;
602
+ companySize?: '1-10' | '11-50' | '51-200' | '201-500' | '501-1000' | '1000+';
603
+ status: 'active' | 'inactive' | 'archived';
604
+ type?: 'client' | 'prospect' | 'vendor' | 'partner' | 'other';
605
+ membership: TiquoCompanyMembership;
606
+ }
607
+ interface GetCompaniesResult {
608
+ companies: TiquoCompany[];
609
+ }
610
+ /**
611
+ * A colleague — another customer in the same company. Returned only to
612
+ * Company Admins via `getCompanyColleagues()`. Limited to display-card
613
+ * info; CRM/financial data is never exposed.
614
+ */
615
+ interface TiquoCompanyColleague {
616
+ id: string;
617
+ firstName?: string;
618
+ lastName?: string;
619
+ displayName?: string;
620
+ profilePhoto?: string;
621
+ email?: string;
622
+ phone?: string;
623
+ relationship: TiquoCompanyRelationship;
624
+ title?: string;
625
+ department?: string;
626
+ isPrimaryContact: boolean;
627
+ isCompanyAdmin: boolean;
628
+ /** True when this colleague is the authenticated customer themselves. */
629
+ isSelf: boolean;
630
+ }
631
+ interface GetCompanyColleaguesResult {
632
+ company: {
633
+ id: string;
634
+ name: string;
635
+ };
636
+ colleagues: TiquoCompanyColleague[];
637
+ }
495
638
  type AuthStateChangeCallback = (session: TiquoSession | null) => void;
496
639
  declare class TiquoAuthError extends Error {
497
640
  code: string;
@@ -541,6 +684,14 @@ declare class TiquoAuth {
541
684
  * to construct from a country selector and national number.
542
685
  */
543
686
  updateProfile(updates: ProfileUpdateData): Promise<ProfileUpdateResult>;
687
+ /**
688
+ * Upload and save the authenticated customer's profile photo.
689
+ *
690
+ * Accepts a browser File/Blob, `data:image/...` URI, or `blob:...` URI. The
691
+ * image is uploaded to Tiquo storage first, and the customer profile is
692
+ * updated with the URL.
693
+ */
694
+ uploadProfilePhoto(photo: Blob | string): Promise<ProfilePhotoUploadResult>;
544
695
  /**
545
696
  * Log out the current user
546
697
  */
@@ -603,6 +754,32 @@ declare class TiquoAuth {
603
754
  * Only returns enquiries for the logged-in customer
604
755
  */
605
756
  getEnquiries(options?: GetEnquiriesOptions): Promise<GetEnquiriesResult>;
757
+ /**
758
+ * Get the companies the authenticated customer belongs to.
759
+ *
760
+ * Each entry includes the customer's role inside that company via
761
+ * `membership` — branch on `membership.isCompanyAdmin` to decide whether
762
+ * to render admin features (e.g. a colleagues list).
763
+ *
764
+ * Returns an empty array (not an error) when the customer isn't a
765
+ * member of any company.
766
+ */
767
+ getCompanies(): Promise<GetCompaniesResult>;
768
+ /**
769
+ * Get the colleagues (other customers) inside a company the authenticated
770
+ * customer is a member of.
771
+ *
772
+ * Restricted to Company Admins — if the caller isn't flagged as
773
+ * `isCompanyAdmin` on that company, this throws `TiquoAuthError` with
774
+ * code `NOT_COMPANY_ADMIN` (status 403). Check `membership.isCompanyAdmin`
775
+ * from `getCompanies()` before calling this if you want to render the
776
+ * admin UI conditionally.
777
+ *
778
+ * Each colleague includes basic contact info and their relationship to
779
+ * the company. The authenticated customer is included in the list with
780
+ * `isSelf: true` so the UI can highlight or skip them.
781
+ */
782
+ getCompanyColleagues(companyId: string): Promise<GetCompanyColleaguesResult>;
606
783
  /**
607
784
  * Generate a short-lived token for customer flow iframe authentication
608
785
  */
@@ -647,6 +824,9 @@ declare class TiquoAuth {
647
824
  * Check for tokens injected by native apps (WebView integration)
648
825
  */
649
826
  private checkForInjectedTokens;
827
+ private extractUploadableProfilePhoto;
828
+ private isProfilePhotoBlobUrl;
829
+ private profilePhotoToBlob;
650
830
  private request;
651
831
  /**
652
832
  * Ensure we have a valid access token, refreshing if necessary
@@ -691,11 +871,14 @@ declare function useTiquoAuth(auth: TiquoAuth): {
691
871
  verifyOTP: (email: string, otp: string) => Promise<VerifyOTPResult>;
692
872
  logout: () => Promise<void>;
693
873
  updateProfile: (updates: ProfileUpdateData) => Promise<ProfileUpdateResult>;
874
+ uploadProfilePhoto: (photo: Blob | string) => Promise<ProfilePhotoUploadResult>;
694
875
  getOrders: (options?: GetOrdersOptions) => Promise<GetOrdersResult>;
695
876
  getBookings: (options?: GetBookingsOptions) => Promise<GetBookingsResult>;
696
877
  getUpcomingBookings: (options?: Omit<GetBookingsOptions, "upcoming">) => Promise<GetBookingsResult>;
697
878
  getReceipt: (orderId: string) => Promise<TiquoReceipt>;
698
879
  getEnquiries: (options?: GetEnquiriesOptions) => Promise<GetEnquiriesResult>;
880
+ getCompanies: () => Promise<GetCompaniesResult>;
881
+ getCompanyColleagues: (companyId: string) => Promise<GetCompanyColleaguesResult>;
699
882
  getIframeToken: (flowId?: string) => Promise<IframeTokenResult>;
700
883
  embedCustomerFlow: (flowUrl: string, container: HTMLElement | string, options?: {
701
884
  width?: string;
@@ -758,4 +941,4 @@ declare class TiquoPhone {
758
941
  static buildPhone: typeof buildPhone;
759
942
  }
760
943
 
761
- export { type AuthStateChangeCallback, type CountryPhoneInfo, type GetBookingsOptions, type GetBookingsResult, type GetEnquiriesOptions, type GetEnquiriesResult, type GetOrdersOptions, type GetOrdersResult, type IframeTokenResult, type PhoneValidationResult, type ProfileUpdateData, type ProfileUpdateResult, type SendOTPResult, TiquoAuth, type TiquoAuthConfig, TiquoAuthError, type TiquoBooking, type TiquoCustomer, type TiquoCustomerEmail, type TiquoCustomerPhone, type TiquoEnquiry, type TiquoOrder, type TiquoOrderItem, TiquoPhone, type TiquoReceipt, type TiquoReceiptBusiness, type TiquoReceiptCustomer, type TiquoReceiptItem, type TiquoReceiptOrder, type TiquoSession, type TiquoUser, type VerifyOTPResult, addCustomerUserId, clearCachedEmail, TiquoAuth as default, getCustomerUserIds, getPrefilledEmailFromCookie, isDashboardSession, trackCustomerPresence, useTiquoAuth };
944
+ export { type AuthStateChangeCallback, type CountryPhoneInfo, type GetBookingsOptions, type GetBookingsResult, type GetCompaniesResult, type GetCompanyColleaguesResult, type GetEnquiriesOptions, type GetEnquiriesResult, type GetOrdersOptions, type GetOrdersResult, type IframeTokenResult, type PhoneValidationResult, type ProfilePhotoUploadResult, type ProfileUpdateData, type ProfileUpdateResult, type SendOTPResult, TiquoAuth, type TiquoAuthConfig, TiquoAuthError, type TiquoBooking, TiquoCMS, type TiquoCMSBlock, type TiquoCMSConfig, type TiquoCMSPage, type TiquoCMSPageRequest, type TiquoCompany, type TiquoCompanyColleague, type TiquoCompanyMembership, type TiquoCompanyRelationship, type TiquoCustomer, type TiquoCustomerEmail, type TiquoCustomerPhone, type TiquoEnquiry, type TiquoOrder, type TiquoOrderItem, TiquoPhone, type TiquoReceipt, type TiquoReceiptBusiness, type TiquoReceiptCustomer, type TiquoReceiptItem, type TiquoReceiptOrder, type TiquoSession, type TiquoUser, type VerifyOTPResult, addCustomerUserId, clearCachedEmail, TiquoAuth as default, getCustomerUserIds, getPrefilledEmailFromCookie, isDashboardSession, trackCustomerPresence, useTiquoAuth };
package/dist/index.js CHANGED
@@ -22,6 +22,7 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  TiquoAuth: () => TiquoAuth,
24
24
  TiquoAuthError: () => TiquoAuthError,
25
+ TiquoCMS: () => TiquoCMS,
25
26
  TiquoPhone: () => TiquoPhone,
26
27
  addCustomerUserId: () => addCustomerUserId,
27
28
  clearCachedEmail: () => clearCachedEmail,
@@ -677,6 +678,68 @@ function printTiquoBranding() {
677
678
  }
678
679
  }
679
680
  printTiquoBranding();
681
+ var TiquoCMS = class {
682
+ constructor(config) {
683
+ if (!config.publicKey) {
684
+ throw new TiquoAuthError("publicKey is required", "MISSING_PUBLIC_KEY");
685
+ }
686
+ if (!config.publicKey.startsWith("pk_dom_")) {
687
+ throw new TiquoAuthError(
688
+ "Invalid public key format. Expected pk_dom_xxx",
689
+ "INVALID_PUBLIC_KEY"
690
+ );
691
+ }
692
+ this.config = {
693
+ publicKey: config.publicKey,
694
+ apiEndpoint: config.apiEndpoint || "https://edge.tiquo.app",
695
+ debug: config.debug || false
696
+ };
697
+ }
698
+ /**
699
+ * Fetch a published CMS page for the website attached to this public key.
700
+ *
701
+ * This intentionally goes through the Tiquo edge API rather than exposing
702
+ * Convex connection details to the consuming website.
703
+ */
704
+ async getPage(request) {
705
+ const response = await fetch(`${this.config.apiEndpoint}/api/dom-cms/page`, {
706
+ method: "POST",
707
+ headers: {
708
+ "Content-Type": "text/plain;charset=UTF-8"
709
+ },
710
+ credentials: "include",
711
+ body: JSON.stringify({
712
+ publicKey: this.config.publicKey,
713
+ siteSlug: request.siteSlug,
714
+ pageSlug: request.pageSlug || "home",
715
+ templateId: request.templateId
716
+ })
717
+ });
718
+ if (response.status === 404) {
719
+ return null;
720
+ }
721
+ if (!response.ok) {
722
+ let message = "Failed to fetch CMS page";
723
+ try {
724
+ const error = await response.json();
725
+ if (typeof error?.error === "string") message = error.error;
726
+ } catch {
727
+ }
728
+ throw new TiquoAuthError(message, "CMS_PAGE_FETCH_FAILED", response.status);
729
+ }
730
+ const result = await response.json();
731
+ if (!result?.success || !result?.data) {
732
+ return null;
733
+ }
734
+ this.log("Fetched CMS page", request.siteSlug, request.pageSlug || "home");
735
+ return result.data;
736
+ }
737
+ log(...args) {
738
+ if (this.config.debug) {
739
+ console.log("[TiquoCMS]", ...args);
740
+ }
741
+ }
742
+ };
680
743
  var TiquoAuthError = class extends Error {
681
744
  constructor(message, code, statusCode) {
682
745
  super(message);
@@ -843,6 +906,10 @@ var TiquoAuth = class {
843
906
  async updateProfile(updates) {
844
907
  await this.ensureValidToken();
845
908
  const normalizedUpdates = { ...updates };
909
+ const profilePhotoUpload = this.extractUploadableProfilePhoto(normalizedUpdates.profilePhoto);
910
+ if (profilePhotoUpload) {
911
+ delete normalizedUpdates.profilePhoto;
912
+ }
846
913
  if (normalizedUpdates.phone !== void 0 && normalizedUpdates.phone !== "") {
847
914
  const normalized = normalizePhone(normalizedUpdates.phone);
848
915
  const validation = validatePhone(normalizedUpdates.phone);
@@ -865,26 +932,109 @@ var TiquoAuth = class {
865
932
  });
866
933
  }
867
934
  this.log("Updating customer profile:", normalizedUpdates);
868
- const response = await this.request("/api/client/v1/profile", {
869
- method: "PATCH",
870
- body: JSON.stringify(normalizedUpdates)
935
+ let customer = this.session?.customer || void 0;
936
+ if (Object.keys(normalizedUpdates).length > 0) {
937
+ const response = await this.request("/api/client/v1/profile", {
938
+ method: "PATCH",
939
+ body: JSON.stringify(normalizedUpdates)
940
+ });
941
+ if (!response.ok) {
942
+ const error = await response.json().catch(() => ({ error: "Failed to update profile" }));
943
+ throw new TiquoAuthError(error.error || "Failed to update profile", "PROFILE_UPDATE_FAILED", response.status);
944
+ }
945
+ const result = await response.json();
946
+ if (this.session && result.data?.customer) {
947
+ this.session = {
948
+ ...this.session,
949
+ customer: result.data.customer
950
+ };
951
+ this.notifyListeners();
952
+ this.broadcastTabSync("SESSION_UPDATE");
953
+ }
954
+ customer = result.data?.customer;
955
+ }
956
+ if (profilePhotoUpload) {
957
+ return this.uploadProfilePhoto(profilePhotoUpload);
958
+ }
959
+ return {
960
+ success: true,
961
+ customer
962
+ };
963
+ }
964
+ /**
965
+ * Upload and save the authenticated customer's profile photo.
966
+ *
967
+ * Accepts a browser File/Blob, `data:image/...` URI, or `blob:...` URI. The
968
+ * image is uploaded to Tiquo storage first, and the customer profile is
969
+ * updated with the URL.
970
+ */
971
+ async uploadProfilePhoto(photo) {
972
+ await this.ensureValidToken();
973
+ const blob = await this.profilePhotoToBlob(photo);
974
+ if (!blob.type.startsWith("image/")) {
975
+ throw new TiquoAuthError("Profile photo must be an image", "INVALID_PROFILE_PHOTO");
976
+ }
977
+ const uploadUrlResponse = await this.request("/api/client/v1/profile/photo-upload-url", {
978
+ method: "POST",
979
+ body: JSON.stringify({})
871
980
  });
872
- if (!response.ok) {
873
- const error = await response.json().catch(() => ({ error: "Failed to update profile" }));
874
- throw new TiquoAuthError(error.error || "Failed to update profile", "PROFILE_UPDATE_FAILED", response.status);
981
+ if (!uploadUrlResponse.ok) {
982
+ const error = await uploadUrlResponse.json().catch(() => ({ error: "Failed to create profile photo upload URL" }));
983
+ throw new TiquoAuthError(
984
+ error.error || "Failed to create profile photo upload URL",
985
+ "PROFILE_PHOTO_UPLOAD_URL_FAILED",
986
+ uploadUrlResponse.status
987
+ );
875
988
  }
876
- const result = await response.json();
877
- if (this.session && result.data?.customer) {
989
+ const uploadUrlResult = await uploadUrlResponse.json();
990
+ const uploadUrl = uploadUrlResult.data?.uploadUrl;
991
+ if (!uploadUrl) {
992
+ throw new TiquoAuthError("Profile photo upload URL was missing", "PROFILE_PHOTO_UPLOAD_URL_FAILED");
993
+ }
994
+ const uploadResponse = await fetch(uploadUrl, {
995
+ method: "POST",
996
+ headers: { "Content-Type": blob.type },
997
+ body: blob
998
+ });
999
+ if (!uploadResponse.ok) {
1000
+ throw new TiquoAuthError("Failed to upload profile photo", "PROFILE_PHOTO_UPLOAD_FAILED", uploadResponse.status);
1001
+ }
1002
+ const uploadResult = await uploadResponse.json();
1003
+ const storageId = uploadResult.storageId;
1004
+ if (!storageId) {
1005
+ throw new TiquoAuthError("Profile photo storage ID was missing", "PROFILE_PHOTO_UPLOAD_FAILED");
1006
+ }
1007
+ const finalizeResponse = await this.request("/api/client/v1/profile/photo", {
1008
+ method: "POST",
1009
+ body: JSON.stringify({ storageId })
1010
+ });
1011
+ if (!finalizeResponse.ok) {
1012
+ const error = await finalizeResponse.json().catch(() => ({ error: "Failed to update profile photo" }));
1013
+ throw new TiquoAuthError(
1014
+ error.error || "Failed to update profile photo",
1015
+ "PROFILE_PHOTO_UPDATE_FAILED",
1016
+ finalizeResponse.status
1017
+ );
1018
+ }
1019
+ const finalizeResult = await finalizeResponse.json();
1020
+ const customer = finalizeResult.data?.customer;
1021
+ const profilePhoto = finalizeResult.data?.profilePhoto;
1022
+ if (!customer || !profilePhoto) {
1023
+ throw new TiquoAuthError("Profile photo update response was incomplete", "PROFILE_PHOTO_UPDATE_FAILED");
1024
+ }
1025
+ if (this.session && customer) {
878
1026
  this.session = {
879
1027
  ...this.session,
880
- customer: result.data.customer
1028
+ customer
881
1029
  };
882
1030
  this.notifyListeners();
883
1031
  this.broadcastTabSync("SESSION_UPDATE");
884
1032
  }
885
1033
  return {
886
1034
  success: true,
887
- customer: result.data?.customer
1035
+ customer,
1036
+ profilePhoto,
1037
+ storageId
888
1038
  };
889
1039
  }
890
1040
  /**
@@ -1074,6 +1224,67 @@ var TiquoAuth = class {
1074
1224
  const result = await response.json();
1075
1225
  return result.data || { enquiries: [], hasMore: false };
1076
1226
  }
1227
+ /**
1228
+ * Get the companies the authenticated customer belongs to.
1229
+ *
1230
+ * Each entry includes the customer's role inside that company via
1231
+ * `membership` — branch on `membership.isCompanyAdmin` to decide whether
1232
+ * to render admin features (e.g. a colleagues list).
1233
+ *
1234
+ * Returns an empty array (not an error) when the customer isn't a
1235
+ * member of any company.
1236
+ */
1237
+ async getCompanies() {
1238
+ await this.ensureValidToken();
1239
+ this.log("Fetching customer companies");
1240
+ const response = await fetch(`${this.config.apiEndpoint}/api/client/v1/companies`, {
1241
+ method: "GET",
1242
+ headers: {
1243
+ "Authorization": `Bearer ${this.accessToken}`
1244
+ },
1245
+ credentials: "include"
1246
+ });
1247
+ if (!response.ok) {
1248
+ const error = await response.json().catch(() => ({ error: "Failed to get companies" }));
1249
+ throw new TiquoAuthError(error.error || "Failed to get companies", "GET_COMPANIES_FAILED", response.status);
1250
+ }
1251
+ const result = await response.json();
1252
+ return result.data || { companies: [] };
1253
+ }
1254
+ /**
1255
+ * Get the colleagues (other customers) inside a company the authenticated
1256
+ * customer is a member of.
1257
+ *
1258
+ * Restricted to Company Admins — if the caller isn't flagged as
1259
+ * `isCompanyAdmin` on that company, this throws `TiquoAuthError` with
1260
+ * code `NOT_COMPANY_ADMIN` (status 403). Check `membership.isCompanyAdmin`
1261
+ * from `getCompanies()` before calling this if you want to render the
1262
+ * admin UI conditionally.
1263
+ *
1264
+ * Each colleague includes basic contact info and their relationship to
1265
+ * the company. The authenticated customer is included in the list with
1266
+ * `isSelf: true` so the UI can highlight or skip them.
1267
+ */
1268
+ async getCompanyColleagues(companyId) {
1269
+ await this.ensureValidToken();
1270
+ this.log("Fetching company colleagues for:", companyId);
1271
+ const url = new URL(`${this.config.apiEndpoint}/api/client/v1/companies/colleagues`);
1272
+ url.searchParams.set("companyId", companyId);
1273
+ const response = await fetch(url.toString(), {
1274
+ method: "GET",
1275
+ headers: {
1276
+ "Authorization": `Bearer ${this.accessToken}`
1277
+ },
1278
+ credentials: "include"
1279
+ });
1280
+ if (!response.ok) {
1281
+ const error = await response.json().catch(() => ({ error: "Failed to get colleagues" }));
1282
+ const code = response.status === 403 ? "NOT_COMPANY_ADMIN" : "GET_COLLEAGUES_FAILED";
1283
+ throw new TiquoAuthError(error.error || "Failed to get colleagues", code, response.status);
1284
+ }
1285
+ const result = await response.json();
1286
+ return result.data;
1287
+ }
1077
1288
  /**
1078
1289
  * Generate a short-lived token for customer flow iframe authentication
1079
1290
  */
@@ -1265,6 +1476,37 @@ var TiquoAuth = class {
1265
1476
  }
1266
1477
  }
1267
1478
  }
1479
+ extractUploadableProfilePhoto(profilePhoto) {
1480
+ if (typeof Blob !== "undefined" && profilePhoto instanceof Blob) {
1481
+ return profilePhoto;
1482
+ }
1483
+ if (typeof profilePhoto === "string") {
1484
+ const trimmed = profilePhoto.trim();
1485
+ if (trimmed.startsWith("data:") || trimmed.startsWith("blob:")) {
1486
+ return trimmed;
1487
+ }
1488
+ }
1489
+ return null;
1490
+ }
1491
+ isProfilePhotoBlobUrl(photo) {
1492
+ return photo.trim().startsWith("data:") || photo.trim().startsWith("blob:");
1493
+ }
1494
+ async profilePhotoToBlob(photo) {
1495
+ if (typeof Blob !== "undefined" && photo instanceof Blob) {
1496
+ return photo;
1497
+ }
1498
+ if (typeof photo === "string" && this.isProfilePhotoBlobUrl(photo)) {
1499
+ const response = await fetch(photo);
1500
+ if (!response.ok) {
1501
+ throw new TiquoAuthError("Failed to read profile photo URI", "INVALID_PROFILE_PHOTO");
1502
+ }
1503
+ return response.blob();
1504
+ }
1505
+ throw new TiquoAuthError(
1506
+ "uploadProfilePhoto expects a File, Blob, data URI, or blob URI. Use updateProfile({ profilePhoto: url }) for remote URLs.",
1507
+ "INVALID_PROFILE_PHOTO"
1508
+ );
1509
+ }
1268
1510
  async request(path, options) {
1269
1511
  const url = `${this.config.apiEndpoint}${path}`;
1270
1512
  const headers = {
@@ -1640,11 +1882,14 @@ function useTiquoAuth(auth) {
1640
1882
  verifyOTP: (email, otp) => auth.verifyOTP(email, otp),
1641
1883
  logout: () => auth.logout(),
1642
1884
  updateProfile: (updates) => auth.updateProfile(updates),
1885
+ uploadProfilePhoto: (photo) => auth.uploadProfilePhoto(photo),
1643
1886
  getOrders: (options) => auth.getOrders(options),
1644
1887
  getBookings: (options) => auth.getBookings(options),
1645
1888
  getUpcomingBookings: (options) => auth.getUpcomingBookings(options),
1646
1889
  getReceipt: (orderId) => auth.getReceipt(orderId),
1647
1890
  getEnquiries: (options) => auth.getEnquiries(options),
1891
+ getCompanies: () => auth.getCompanies(),
1892
+ getCompanyColleagues: (companyId) => auth.getCompanyColleagues(companyId),
1648
1893
  getIframeToken: (flowId) => auth.getIframeToken(flowId),
1649
1894
  embedCustomerFlow: auth.embedCustomerFlow.bind(auth),
1650
1895
  onAuthStateChange: (cb) => auth.onAuthStateChange(cb)
@@ -1671,6 +1916,7 @@ var index_default = TiquoAuth;
1671
1916
  0 && (module.exports = {
1672
1917
  TiquoAuth,
1673
1918
  TiquoAuthError,
1919
+ TiquoCMS,
1674
1920
  TiquoPhone,
1675
1921
  addCustomerUserId,
1676
1922
  clearCachedEmail,
package/dist/index.mjs CHANGED
@@ -641,6 +641,68 @@ function printTiquoBranding() {
641
641
  }
642
642
  }
643
643
  printTiquoBranding();
644
+ var TiquoCMS = class {
645
+ constructor(config) {
646
+ if (!config.publicKey) {
647
+ throw new TiquoAuthError("publicKey is required", "MISSING_PUBLIC_KEY");
648
+ }
649
+ if (!config.publicKey.startsWith("pk_dom_")) {
650
+ throw new TiquoAuthError(
651
+ "Invalid public key format. Expected pk_dom_xxx",
652
+ "INVALID_PUBLIC_KEY"
653
+ );
654
+ }
655
+ this.config = {
656
+ publicKey: config.publicKey,
657
+ apiEndpoint: config.apiEndpoint || "https://edge.tiquo.app",
658
+ debug: config.debug || false
659
+ };
660
+ }
661
+ /**
662
+ * Fetch a published CMS page for the website attached to this public key.
663
+ *
664
+ * This intentionally goes through the Tiquo edge API rather than exposing
665
+ * Convex connection details to the consuming website.
666
+ */
667
+ async getPage(request) {
668
+ const response = await fetch(`${this.config.apiEndpoint}/api/dom-cms/page`, {
669
+ method: "POST",
670
+ headers: {
671
+ "Content-Type": "text/plain;charset=UTF-8"
672
+ },
673
+ credentials: "include",
674
+ body: JSON.stringify({
675
+ publicKey: this.config.publicKey,
676
+ siteSlug: request.siteSlug,
677
+ pageSlug: request.pageSlug || "home",
678
+ templateId: request.templateId
679
+ })
680
+ });
681
+ if (response.status === 404) {
682
+ return null;
683
+ }
684
+ if (!response.ok) {
685
+ let message = "Failed to fetch CMS page";
686
+ try {
687
+ const error = await response.json();
688
+ if (typeof error?.error === "string") message = error.error;
689
+ } catch {
690
+ }
691
+ throw new TiquoAuthError(message, "CMS_PAGE_FETCH_FAILED", response.status);
692
+ }
693
+ const result = await response.json();
694
+ if (!result?.success || !result?.data) {
695
+ return null;
696
+ }
697
+ this.log("Fetched CMS page", request.siteSlug, request.pageSlug || "home");
698
+ return result.data;
699
+ }
700
+ log(...args) {
701
+ if (this.config.debug) {
702
+ console.log("[TiquoCMS]", ...args);
703
+ }
704
+ }
705
+ };
644
706
  var TiquoAuthError = class extends Error {
645
707
  constructor(message, code, statusCode) {
646
708
  super(message);
@@ -807,6 +869,10 @@ var TiquoAuth = class {
807
869
  async updateProfile(updates) {
808
870
  await this.ensureValidToken();
809
871
  const normalizedUpdates = { ...updates };
872
+ const profilePhotoUpload = this.extractUploadableProfilePhoto(normalizedUpdates.profilePhoto);
873
+ if (profilePhotoUpload) {
874
+ delete normalizedUpdates.profilePhoto;
875
+ }
810
876
  if (normalizedUpdates.phone !== void 0 && normalizedUpdates.phone !== "") {
811
877
  const normalized = normalizePhone(normalizedUpdates.phone);
812
878
  const validation = validatePhone(normalizedUpdates.phone);
@@ -829,26 +895,109 @@ var TiquoAuth = class {
829
895
  });
830
896
  }
831
897
  this.log("Updating customer profile:", normalizedUpdates);
832
- const response = await this.request("/api/client/v1/profile", {
833
- method: "PATCH",
834
- body: JSON.stringify(normalizedUpdates)
898
+ let customer = this.session?.customer || void 0;
899
+ if (Object.keys(normalizedUpdates).length > 0) {
900
+ const response = await this.request("/api/client/v1/profile", {
901
+ method: "PATCH",
902
+ body: JSON.stringify(normalizedUpdates)
903
+ });
904
+ if (!response.ok) {
905
+ const error = await response.json().catch(() => ({ error: "Failed to update profile" }));
906
+ throw new TiquoAuthError(error.error || "Failed to update profile", "PROFILE_UPDATE_FAILED", response.status);
907
+ }
908
+ const result = await response.json();
909
+ if (this.session && result.data?.customer) {
910
+ this.session = {
911
+ ...this.session,
912
+ customer: result.data.customer
913
+ };
914
+ this.notifyListeners();
915
+ this.broadcastTabSync("SESSION_UPDATE");
916
+ }
917
+ customer = result.data?.customer;
918
+ }
919
+ if (profilePhotoUpload) {
920
+ return this.uploadProfilePhoto(profilePhotoUpload);
921
+ }
922
+ return {
923
+ success: true,
924
+ customer
925
+ };
926
+ }
927
+ /**
928
+ * Upload and save the authenticated customer's profile photo.
929
+ *
930
+ * Accepts a browser File/Blob, `data:image/...` URI, or `blob:...` URI. The
931
+ * image is uploaded to Tiquo storage first, and the customer profile is
932
+ * updated with the URL.
933
+ */
934
+ async uploadProfilePhoto(photo) {
935
+ await this.ensureValidToken();
936
+ const blob = await this.profilePhotoToBlob(photo);
937
+ if (!blob.type.startsWith("image/")) {
938
+ throw new TiquoAuthError("Profile photo must be an image", "INVALID_PROFILE_PHOTO");
939
+ }
940
+ const uploadUrlResponse = await this.request("/api/client/v1/profile/photo-upload-url", {
941
+ method: "POST",
942
+ body: JSON.stringify({})
835
943
  });
836
- if (!response.ok) {
837
- const error = await response.json().catch(() => ({ error: "Failed to update profile" }));
838
- throw new TiquoAuthError(error.error || "Failed to update profile", "PROFILE_UPDATE_FAILED", response.status);
944
+ if (!uploadUrlResponse.ok) {
945
+ const error = await uploadUrlResponse.json().catch(() => ({ error: "Failed to create profile photo upload URL" }));
946
+ throw new TiquoAuthError(
947
+ error.error || "Failed to create profile photo upload URL",
948
+ "PROFILE_PHOTO_UPLOAD_URL_FAILED",
949
+ uploadUrlResponse.status
950
+ );
839
951
  }
840
- const result = await response.json();
841
- if (this.session && result.data?.customer) {
952
+ const uploadUrlResult = await uploadUrlResponse.json();
953
+ const uploadUrl = uploadUrlResult.data?.uploadUrl;
954
+ if (!uploadUrl) {
955
+ throw new TiquoAuthError("Profile photo upload URL was missing", "PROFILE_PHOTO_UPLOAD_URL_FAILED");
956
+ }
957
+ const uploadResponse = await fetch(uploadUrl, {
958
+ method: "POST",
959
+ headers: { "Content-Type": blob.type },
960
+ body: blob
961
+ });
962
+ if (!uploadResponse.ok) {
963
+ throw new TiquoAuthError("Failed to upload profile photo", "PROFILE_PHOTO_UPLOAD_FAILED", uploadResponse.status);
964
+ }
965
+ const uploadResult = await uploadResponse.json();
966
+ const storageId = uploadResult.storageId;
967
+ if (!storageId) {
968
+ throw new TiquoAuthError("Profile photo storage ID was missing", "PROFILE_PHOTO_UPLOAD_FAILED");
969
+ }
970
+ const finalizeResponse = await this.request("/api/client/v1/profile/photo", {
971
+ method: "POST",
972
+ body: JSON.stringify({ storageId })
973
+ });
974
+ if (!finalizeResponse.ok) {
975
+ const error = await finalizeResponse.json().catch(() => ({ error: "Failed to update profile photo" }));
976
+ throw new TiquoAuthError(
977
+ error.error || "Failed to update profile photo",
978
+ "PROFILE_PHOTO_UPDATE_FAILED",
979
+ finalizeResponse.status
980
+ );
981
+ }
982
+ const finalizeResult = await finalizeResponse.json();
983
+ const customer = finalizeResult.data?.customer;
984
+ const profilePhoto = finalizeResult.data?.profilePhoto;
985
+ if (!customer || !profilePhoto) {
986
+ throw new TiquoAuthError("Profile photo update response was incomplete", "PROFILE_PHOTO_UPDATE_FAILED");
987
+ }
988
+ if (this.session && customer) {
842
989
  this.session = {
843
990
  ...this.session,
844
- customer: result.data.customer
991
+ customer
845
992
  };
846
993
  this.notifyListeners();
847
994
  this.broadcastTabSync("SESSION_UPDATE");
848
995
  }
849
996
  return {
850
997
  success: true,
851
- customer: result.data?.customer
998
+ customer,
999
+ profilePhoto,
1000
+ storageId
852
1001
  };
853
1002
  }
854
1003
  /**
@@ -1038,6 +1187,67 @@ var TiquoAuth = class {
1038
1187
  const result = await response.json();
1039
1188
  return result.data || { enquiries: [], hasMore: false };
1040
1189
  }
1190
+ /**
1191
+ * Get the companies the authenticated customer belongs to.
1192
+ *
1193
+ * Each entry includes the customer's role inside that company via
1194
+ * `membership` — branch on `membership.isCompanyAdmin` to decide whether
1195
+ * to render admin features (e.g. a colleagues list).
1196
+ *
1197
+ * Returns an empty array (not an error) when the customer isn't a
1198
+ * member of any company.
1199
+ */
1200
+ async getCompanies() {
1201
+ await this.ensureValidToken();
1202
+ this.log("Fetching customer companies");
1203
+ const response = await fetch(`${this.config.apiEndpoint}/api/client/v1/companies`, {
1204
+ method: "GET",
1205
+ headers: {
1206
+ "Authorization": `Bearer ${this.accessToken}`
1207
+ },
1208
+ credentials: "include"
1209
+ });
1210
+ if (!response.ok) {
1211
+ const error = await response.json().catch(() => ({ error: "Failed to get companies" }));
1212
+ throw new TiquoAuthError(error.error || "Failed to get companies", "GET_COMPANIES_FAILED", response.status);
1213
+ }
1214
+ const result = await response.json();
1215
+ return result.data || { companies: [] };
1216
+ }
1217
+ /**
1218
+ * Get the colleagues (other customers) inside a company the authenticated
1219
+ * customer is a member of.
1220
+ *
1221
+ * Restricted to Company Admins — if the caller isn't flagged as
1222
+ * `isCompanyAdmin` on that company, this throws `TiquoAuthError` with
1223
+ * code `NOT_COMPANY_ADMIN` (status 403). Check `membership.isCompanyAdmin`
1224
+ * from `getCompanies()` before calling this if you want to render the
1225
+ * admin UI conditionally.
1226
+ *
1227
+ * Each colleague includes basic contact info and their relationship to
1228
+ * the company. The authenticated customer is included in the list with
1229
+ * `isSelf: true` so the UI can highlight or skip them.
1230
+ */
1231
+ async getCompanyColleagues(companyId) {
1232
+ await this.ensureValidToken();
1233
+ this.log("Fetching company colleagues for:", companyId);
1234
+ const url = new URL(`${this.config.apiEndpoint}/api/client/v1/companies/colleagues`);
1235
+ url.searchParams.set("companyId", companyId);
1236
+ const response = await fetch(url.toString(), {
1237
+ method: "GET",
1238
+ headers: {
1239
+ "Authorization": `Bearer ${this.accessToken}`
1240
+ },
1241
+ credentials: "include"
1242
+ });
1243
+ if (!response.ok) {
1244
+ const error = await response.json().catch(() => ({ error: "Failed to get colleagues" }));
1245
+ const code = response.status === 403 ? "NOT_COMPANY_ADMIN" : "GET_COLLEAGUES_FAILED";
1246
+ throw new TiquoAuthError(error.error || "Failed to get colleagues", code, response.status);
1247
+ }
1248
+ const result = await response.json();
1249
+ return result.data;
1250
+ }
1041
1251
  /**
1042
1252
  * Generate a short-lived token for customer flow iframe authentication
1043
1253
  */
@@ -1229,6 +1439,37 @@ var TiquoAuth = class {
1229
1439
  }
1230
1440
  }
1231
1441
  }
1442
+ extractUploadableProfilePhoto(profilePhoto) {
1443
+ if (typeof Blob !== "undefined" && profilePhoto instanceof Blob) {
1444
+ return profilePhoto;
1445
+ }
1446
+ if (typeof profilePhoto === "string") {
1447
+ const trimmed = profilePhoto.trim();
1448
+ if (trimmed.startsWith("data:") || trimmed.startsWith("blob:")) {
1449
+ return trimmed;
1450
+ }
1451
+ }
1452
+ return null;
1453
+ }
1454
+ isProfilePhotoBlobUrl(photo) {
1455
+ return photo.trim().startsWith("data:") || photo.trim().startsWith("blob:");
1456
+ }
1457
+ async profilePhotoToBlob(photo) {
1458
+ if (typeof Blob !== "undefined" && photo instanceof Blob) {
1459
+ return photo;
1460
+ }
1461
+ if (typeof photo === "string" && this.isProfilePhotoBlobUrl(photo)) {
1462
+ const response = await fetch(photo);
1463
+ if (!response.ok) {
1464
+ throw new TiquoAuthError("Failed to read profile photo URI", "INVALID_PROFILE_PHOTO");
1465
+ }
1466
+ return response.blob();
1467
+ }
1468
+ throw new TiquoAuthError(
1469
+ "uploadProfilePhoto expects a File, Blob, data URI, or blob URI. Use updateProfile({ profilePhoto: url }) for remote URLs.",
1470
+ "INVALID_PROFILE_PHOTO"
1471
+ );
1472
+ }
1232
1473
  async request(path, options) {
1233
1474
  const url = `${this.config.apiEndpoint}${path}`;
1234
1475
  const headers = {
@@ -1604,11 +1845,14 @@ function useTiquoAuth(auth) {
1604
1845
  verifyOTP: (email, otp) => auth.verifyOTP(email, otp),
1605
1846
  logout: () => auth.logout(),
1606
1847
  updateProfile: (updates) => auth.updateProfile(updates),
1848
+ uploadProfilePhoto: (photo) => auth.uploadProfilePhoto(photo),
1607
1849
  getOrders: (options) => auth.getOrders(options),
1608
1850
  getBookings: (options) => auth.getBookings(options),
1609
1851
  getUpcomingBookings: (options) => auth.getUpcomingBookings(options),
1610
1852
  getReceipt: (orderId) => auth.getReceipt(orderId),
1611
1853
  getEnquiries: (options) => auth.getEnquiries(options),
1854
+ getCompanies: () => auth.getCompanies(),
1855
+ getCompanyColleagues: (companyId) => auth.getCompanyColleagues(companyId),
1612
1856
  getIframeToken: (flowId) => auth.getIframeToken(flowId),
1613
1857
  embedCustomerFlow: auth.embedCustomerFlow.bind(auth),
1614
1858
  onAuthStateChange: (cb) => auth.onAuthStateChange(cb)
@@ -1634,6 +1878,7 @@ var index_default = TiquoAuth;
1634
1878
  export {
1635
1879
  TiquoAuth,
1636
1880
  TiquoAuthError,
1881
+ TiquoCMS,
1637
1882
  TiquoPhone,
1638
1883
  addCustomerUserId,
1639
1884
  clearCachedEmail,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tiquo/dom-package",
3
- "version": "1.4.1",
4
- "description": "Tiquo SDK for third-party websites - authentication, customer profiles, orders, bookings, and enquiries",
3
+ "version": "1.5.1",
4
+ "description": "Tiquo SDK for third-party websites - authentication, customer profiles, orders, bookings, enquiries, receipts, and companies",
5
5
  "sideEffects": true,
6
6
  "publishConfig": {
7
7
  "access": "restricted"