@tiquo/dom-package 1.5.0 → 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;
@@ -615,6 +684,14 @@ declare class TiquoAuth {
615
684
  * to construct from a country selector and national number.
616
685
  */
617
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>;
618
695
  /**
619
696
  * Log out the current user
620
697
  */
@@ -747,6 +824,9 @@ declare class TiquoAuth {
747
824
  * Check for tokens injected by native apps (WebView integration)
748
825
  */
749
826
  private checkForInjectedTokens;
827
+ private extractUploadableProfilePhoto;
828
+ private isProfilePhotoBlobUrl;
829
+ private profilePhotoToBlob;
750
830
  private request;
751
831
  /**
752
832
  * Ensure we have a valid access token, refreshing if necessary
@@ -791,6 +871,7 @@ declare function useTiquoAuth(auth: TiquoAuth): {
791
871
  verifyOTP: (email: string, otp: string) => Promise<VerifyOTPResult>;
792
872
  logout: () => Promise<void>;
793
873
  updateProfile: (updates: ProfileUpdateData) => Promise<ProfileUpdateResult>;
874
+ uploadProfilePhoto: (photo: Blob | string) => Promise<ProfilePhotoUploadResult>;
794
875
  getOrders: (options?: GetOrdersOptions) => Promise<GetOrdersResult>;
795
876
  getBookings: (options?: GetBookingsOptions) => Promise<GetBookingsResult>;
796
877
  getUpcomingBookings: (options?: Omit<GetBookingsOptions, "upcoming">) => Promise<GetBookingsResult>;
@@ -860,4 +941,4 @@ declare class TiquoPhone {
860
941
  static buildPhone: typeof buildPhone;
861
942
  }
862
943
 
863
- 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 ProfileUpdateData, type ProfileUpdateResult, type SendOTPResult, TiquoAuth, type TiquoAuthConfig, TiquoAuthError, type TiquoBooking, 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 };
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;
@@ -615,6 +684,14 @@ declare class TiquoAuth {
615
684
  * to construct from a country selector and national number.
616
685
  */
617
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>;
618
695
  /**
619
696
  * Log out the current user
620
697
  */
@@ -747,6 +824,9 @@ declare class TiquoAuth {
747
824
  * Check for tokens injected by native apps (WebView integration)
748
825
  */
749
826
  private checkForInjectedTokens;
827
+ private extractUploadableProfilePhoto;
828
+ private isProfilePhotoBlobUrl;
829
+ private profilePhotoToBlob;
750
830
  private request;
751
831
  /**
752
832
  * Ensure we have a valid access token, refreshing if necessary
@@ -791,6 +871,7 @@ declare function useTiquoAuth(auth: TiquoAuth): {
791
871
  verifyOTP: (email: string, otp: string) => Promise<VerifyOTPResult>;
792
872
  logout: () => Promise<void>;
793
873
  updateProfile: (updates: ProfileUpdateData) => Promise<ProfileUpdateResult>;
874
+ uploadProfilePhoto: (photo: Blob | string) => Promise<ProfilePhotoUploadResult>;
794
875
  getOrders: (options?: GetOrdersOptions) => Promise<GetOrdersResult>;
795
876
  getBookings: (options?: GetBookingsOptions) => Promise<GetBookingsResult>;
796
877
  getUpcomingBookings: (options?: Omit<GetBookingsOptions, "upcoming">) => Promise<GetBookingsResult>;
@@ -860,4 +941,4 @@ declare class TiquoPhone {
860
941
  static buildPhone: typeof buildPhone;
861
942
  }
862
943
 
863
- 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 ProfileUpdateData, type ProfileUpdateResult, type SendOTPResult, TiquoAuth, type TiquoAuthConfig, TiquoAuthError, type TiquoBooking, 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 };
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
  /**
@@ -1326,6 +1476,37 @@ var TiquoAuth = class {
1326
1476
  }
1327
1477
  }
1328
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
+ }
1329
1510
  async request(path, options) {
1330
1511
  const url = `${this.config.apiEndpoint}${path}`;
1331
1512
  const headers = {
@@ -1701,6 +1882,7 @@ function useTiquoAuth(auth) {
1701
1882
  verifyOTP: (email, otp) => auth.verifyOTP(email, otp),
1702
1883
  logout: () => auth.logout(),
1703
1884
  updateProfile: (updates) => auth.updateProfile(updates),
1885
+ uploadProfilePhoto: (photo) => auth.uploadProfilePhoto(photo),
1704
1886
  getOrders: (options) => auth.getOrders(options),
1705
1887
  getBookings: (options) => auth.getBookings(options),
1706
1888
  getUpcomingBookings: (options) => auth.getUpcomingBookings(options),
@@ -1734,6 +1916,7 @@ var index_default = TiquoAuth;
1734
1916
  0 && (module.exports = {
1735
1917
  TiquoAuth,
1736
1918
  TiquoAuthError,
1919
+ TiquoCMS,
1737
1920
  TiquoPhone,
1738
1921
  addCustomerUserId,
1739
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
  /**
@@ -1290,6 +1439,37 @@ var TiquoAuth = class {
1290
1439
  }
1291
1440
  }
1292
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
+ }
1293
1473
  async request(path, options) {
1294
1474
  const url = `${this.config.apiEndpoint}${path}`;
1295
1475
  const headers = {
@@ -1665,6 +1845,7 @@ function useTiquoAuth(auth) {
1665
1845
  verifyOTP: (email, otp) => auth.verifyOTP(email, otp),
1666
1846
  logout: () => auth.logout(),
1667
1847
  updateProfile: (updates) => auth.updateProfile(updates),
1848
+ uploadProfilePhoto: (photo) => auth.uploadProfilePhoto(photo),
1668
1849
  getOrders: (options) => auth.getOrders(options),
1669
1850
  getBookings: (options) => auth.getBookings(options),
1670
1851
  getUpcomingBookings: (options) => auth.getUpcomingBookings(options),
@@ -1697,6 +1878,7 @@ var index_default = TiquoAuth;
1697
1878
  export {
1698
1879
  TiquoAuth,
1699
1880
  TiquoAuthError,
1881
+ TiquoCMS,
1700
1882
  TiquoPhone,
1701
1883
  addCustomerUserId,
1702
1884
  clearCachedEmail,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiquo/dom-package",
3
- "version": "1.5.0",
3
+ "version": "1.5.1",
4
4
  "description": "Tiquo SDK for third-party websites - authentication, customer profiles, orders, bookings, enquiries, receipts, and companies",
5
5
  "sideEffects": true,
6
6
  "publishConfig": {