@scalemule/nextjs 0.1.1 → 0.1.3

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/dist/index.js CHANGED
@@ -2099,6 +2099,40 @@ var StorageService = class extends ServiceModule {
2099
2099
  return { data: null, error: { code: "upload_error", message, status: 0 } };
2100
2100
  }
2101
2101
  }
2102
+ /**
2103
+ * Upload a file as a private attachment (e.g. chat / DM attachment).
2104
+ *
2105
+ * Same browser→S3 pipeline as {@link upload}, but enforces:
2106
+ * - `is_public: false` — caller cannot opt out
2107
+ * - no client-side compression — preserve the original bytes
2108
+ * - fail-closed if storage returns `is_public: true` for any reason
2109
+ *
2110
+ * The shared primitive used by `@scalemule/chat`'s chat-attachment uploader
2111
+ * and by `@scalemule/nextjs`'s `useMedia()` when an app's media policy is
2112
+ * `fast_trusted`. Both packages call this method via `@scalemule/sdk` —
2113
+ * `@scalemule/nextjs` does not depend on `@scalemule/chat`.
2114
+ *
2115
+ * See ADR-2026-04-26 (realtime-chat media pipeline) and
2116
+ * docs/MEDIA-UPLOADS.md for the full pattern.
2117
+ */
2118
+ async uploadPrivate(file, options) {
2119
+ const result = await this.upload(file, {
2120
+ ...options,
2121
+ isPublic: false,
2122
+ skipCompression: true
2123
+ });
2124
+ if (result.data && result.data.is_public === true) {
2125
+ return {
2126
+ data: null,
2127
+ error: {
2128
+ code: "visibility_violation",
2129
+ message: "Storage returned is_public=true for an uploadPrivate() call. This usually indicates a server-side bug or an admin override; the caller asked for a private upload.",
2130
+ status: 0
2131
+ }
2132
+ };
2133
+ }
2134
+ return result;
2135
+ }
2102
2136
  // --------------------------------------------------------------------------
2103
2137
  // Direct Upload (3-step with retry + stall)
2104
2138
  // --------------------------------------------------------------------------
@@ -3471,8 +3505,13 @@ var RealtimeService = class extends ServiceModule {
3471
3505
  var DEFAULT_CHUNK_SIZE = 5 * 1024 * 1024;
3472
3506
  var MIN_CHUNK_SIZE = 5 * 1024 * 1024;
3473
3507
  var VideoService = class extends ServiceModule {
3474
- constructor() {
3475
- super(...arguments);
3508
+ /**
3509
+ * @param storage Required for {@link uploadViaStorage}. Wired up by the
3510
+ * top-level {@link ScaleMule} constructor.
3511
+ */
3512
+ constructor(client, storage) {
3513
+ super(client);
3514
+ this.storage = storage;
3476
3515
  this.basePath = "/v1/videos";
3477
3516
  }
3478
3517
  // --------------------------------------------------------------------------
@@ -3546,6 +3585,115 @@ var VideoService = class extends ServiceModule {
3546
3585
  async get(videoId, options) {
3547
3586
  return super._get(`/${videoId}`, options);
3548
3587
  }
3588
+ /**
3589
+ * Register a video from a file already uploaded to scalemule-storage.
3590
+ *
3591
+ * The synchronous handshake for storage-uploaded videos. Verifies the
3592
+ * caller's app owns the storage `file_id` and reads scan status:
3593
+ * - clean → 201, video record created with status='uploading',
3594
+ * transcode worker advances on `scan.file.completed`
3595
+ * - pending → 202, video record created with status='pending_scan',
3596
+ * worker advances when scan completes
3597
+ * - threat → 409 (rejected; row not created)
3598
+ *
3599
+ * Idempotent: calling twice with the same file_id returns the existing
3600
+ * row's data (the underlying INSERT uses ON DUPLICATE KEY UPDATE).
3601
+ *
3602
+ * For storage-backed videos `video_id == file_id` — the response's
3603
+ * `video_id` and `file_id` are identical, and either can be used with
3604
+ * `getStreamUrl()` / `get()`.
3605
+ *
3606
+ * Most callers should use {@link uploadViaStorage} which composes
3607
+ * `storage.uploadPrivate()` + `register()` in a single call.
3608
+ */
3609
+ async register(request, options) {
3610
+ return this.post(
3611
+ "/register",
3612
+ {
3613
+ file_id: request.fileId,
3614
+ sm_user_id: request.userId
3615
+ },
3616
+ options
3617
+ );
3618
+ }
3619
+ /**
3620
+ * Upload a video to storage (browser → S3 direct, private, no
3621
+ * compression) and register it with the video service so transcoding
3622
+ * + HLS streaming work.
3623
+ *
3624
+ * The canonical chat-attachment / progressive-video upload primitive.
3625
+ * Same private-direct-to-S3 path as `storage.uploadPrivate()`, plus a
3626
+ * follow-up `video.register()` call so HLS playback URLs become live
3627
+ * once the transcoder finishes.
3628
+ *
3629
+ * The returned `hls_url_promise` resolves once the video's status
3630
+ * flips to `ready` (or after a 30s timeout — caller can still serve
3631
+ * `original_view_url` as the fallback). Phase 3 of the realtime-chat
3632
+ * media pipeline ADR replaces the poll with a realtime subscription.
3633
+ *
3634
+ * If `register()` fails after a successful storage upload, the file
3635
+ * is *not* lost: the returned `file_id` is still valid as a generic
3636
+ * storage file. SDK logs a warning and resolves `hls_url_promise`
3637
+ * to `null`.
3638
+ */
3639
+ async uploadViaStorage(file, uploadOptions, requestOptions) {
3640
+ const uploadResult = await this.storage.uploadPrivate(file, {
3641
+ filename: uploadOptions?.filename,
3642
+ metadata: uploadOptions?.metadata,
3643
+ onProgress: uploadOptions?.onProgress,
3644
+ signal: uploadOptions?.signal
3645
+ });
3646
+ if (uploadResult.error || !uploadResult.data) {
3647
+ return { data: null, error: uploadResult.error };
3648
+ }
3649
+ const fileInfo = uploadResult.data;
3650
+ const fileId = fileInfo.id;
3651
+ const originalViewUrl = fileInfo.url ?? null;
3652
+ const registerResult = await this.register({ fileId, userId: uploadOptions?.userId }, requestOptions);
3653
+ if (registerResult.error || !registerResult.data) {
3654
+ console.warn(
3655
+ "[scalemule-sdk] video.register() failed after storage upload; HLS variants unavailable.",
3656
+ registerResult.error
3657
+ );
3658
+ return {
3659
+ data: {
3660
+ file_id: fileId,
3661
+ video_id: fileId,
3662
+ original_view_url: originalViewUrl,
3663
+ hls_url_promise: Promise.resolve(null)
3664
+ },
3665
+ error: null
3666
+ };
3667
+ }
3668
+ const videoId = registerResult.data.video_id;
3669
+ const hlsUrlPromise = this.pollTranscodeComplete(videoId, requestOptions);
3670
+ return {
3671
+ data: {
3672
+ file_id: fileId,
3673
+ video_id: videoId,
3674
+ original_view_url: originalViewUrl,
3675
+ hls_url_promise: hlsUrlPromise
3676
+ },
3677
+ error: null
3678
+ };
3679
+ }
3680
+ /**
3681
+ * Poll {@link get} until the video's status is `ready` or the 30s
3682
+ * timeout fires. Resolves to the HLS master playlist URL on success;
3683
+ * `null` on timeout (caller should fall back to `original_view_url`).
3684
+ */
3685
+ async pollTranscodeComplete(videoId, requestOptions) {
3686
+ const intervalMs = 1e3;
3687
+ const maxAttempts = 30;
3688
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
3689
+ const result = await this.get(videoId, requestOptions);
3690
+ if (result.data?.status === "ready") {
3691
+ return `${this.client.getBaseUrl()}${this.basePath}/${videoId}/playlist.m3u8`;
3692
+ }
3693
+ await new Promise((r) => setTimeout(r, intervalMs));
3694
+ }
3695
+ return null;
3696
+ }
3549
3697
  /**
3550
3698
  * Get the HLS master playlist URL for streaming.
3551
3699
  * Returns the playlist URL that can be passed to a video player.
@@ -3788,6 +3936,18 @@ var ChatService = class extends ServiceModule {
3788
3936
  return this.post(`/messages/${messageId}/reactions`, data, options);
3789
3937
  }
3790
3938
  // --------------------------------------------------------------------------
3939
+ // Pins
3940
+ // --------------------------------------------------------------------------
3941
+ async pinMessage(messageId, options) {
3942
+ return this.post(`/messages/${messageId}/pin`, void 0, options);
3943
+ }
3944
+ async unpinMessage(messageId, options) {
3945
+ return this.del(`/messages/${messageId}/pin`, options);
3946
+ }
3947
+ async getPinnedMessages(conversationId, options) {
3948
+ return this._get(`/conversations/${conversationId}/pins`, options);
3949
+ }
3950
+ // --------------------------------------------------------------------------
3791
3951
  // Typing & Read Receipts
3792
3952
  // --------------------------------------------------------------------------
3793
3953
  async sendTyping(conversationId, options) {
@@ -3998,15 +4158,80 @@ var BillingService = class extends ServiceModule {
3998
4158
  );
3999
4159
  }
4000
4160
  // --------------------------------------------------------------------------
4001
- // Customers
4161
+ // Customers (legacy user-scoped — deprecated in favor of account-scoped)
4002
4162
  // --------------------------------------------------------------------------
4163
+ /** @deprecated Use `getAccountCustomer({ account_id })`. Targets the user-scoped legacy endpoint. */
4003
4164
  async createCustomer(data, options) {
4004
4165
  return this.post("/customers", data, options);
4005
4166
  }
4167
+ /** @deprecated Use `attachAccountPaymentMethod({ account_id }, ...)`. Targets the user-scoped legacy endpoint. */
4006
4168
  async addPaymentMethod(data, options) {
4007
4169
  return this.post("/payment-methods", data, options);
4008
4170
  }
4009
4171
  // --------------------------------------------------------------------------
4172
+ // Account-Scoped Billing (v2 — ADR-2026-04-25)
4173
+ //
4174
+ // The customer/payment-method/invoice record set belongs to an `app_account`
4175
+ // (typically a `customer_org`), not to a single user. These methods target
4176
+ // the `/v1/billing/accounts/{account_id}/*` endpoints described in
4177
+ // openapi/scalemule-money-billing-v1.yaml.
4178
+ // --------------------------------------------------------------------------
4179
+ accountBase(accountId) {
4180
+ return `/v1/billing/accounts/${encodeURIComponent(accountId)}`;
4181
+ }
4182
+ async getAccountCustomer(req, options) {
4183
+ const path = this.withQuery(`${this.accountBase(req.account_id)}/customer`, {
4184
+ billing_mode: req.billing_mode
4185
+ });
4186
+ return this.client.get(path, options);
4187
+ }
4188
+ async listAccountPaymentMethods(req, options) {
4189
+ const path = this.withQuery(`${this.accountBase(req.account_id)}/payment-methods`, {
4190
+ billing_mode: req.billing_mode
4191
+ });
4192
+ return this.client.get(path, options);
4193
+ }
4194
+ async attachAccountPaymentMethod(req, body, options) {
4195
+ const path = this.withQuery(`${this.accountBase(req.account_id)}/payment-methods`, {
4196
+ billing_mode: req.billing_mode
4197
+ });
4198
+ return this.client.post(path, body, options);
4199
+ }
4200
+ async detachAccountPaymentMethod(req, paymentMethodId, options) {
4201
+ const path = this.withQuery(
4202
+ `${this.accountBase(req.account_id)}/payment-methods/${encodeURIComponent(paymentMethodId)}`,
4203
+ { billing_mode: req.billing_mode }
4204
+ );
4205
+ return this.client.del(path, options);
4206
+ }
4207
+ async setDefaultAccountPaymentMethod(req, paymentMethodId, options) {
4208
+ const path = this.withQuery(
4209
+ `${this.accountBase(req.account_id)}/payment-methods/${encodeURIComponent(paymentMethodId)}/default`,
4210
+ { billing_mode: req.billing_mode }
4211
+ );
4212
+ return this.client.put(path, void 0, options);
4213
+ }
4214
+ async listAccountInvoices(req, params, options) {
4215
+ const path = this.withQuery(`${this.accountBase(req.account_id)}/invoices`, {
4216
+ billing_mode: req.billing_mode,
4217
+ limit: params?.limit,
4218
+ cursor: params?.cursor
4219
+ });
4220
+ return this.client.get(path, options);
4221
+ }
4222
+ async getAccountInvoice(req, invoiceId, options) {
4223
+ const path = this.withQuery(`${this.accountBase(req.account_id)}/invoices/${encodeURIComponent(invoiceId)}`, {
4224
+ billing_mode: req.billing_mode
4225
+ });
4226
+ return this.client.get(path, options);
4227
+ }
4228
+ async createAccountPortalSession(req, body, options) {
4229
+ const path = this.withQuery(`${this.accountBase(req.account_id)}/portal-session`, {
4230
+ billing_mode: req.billing_mode
4231
+ });
4232
+ return this.client.post(path, body ?? {}, options);
4233
+ }
4234
+ // --------------------------------------------------------------------------
4010
4235
  // Subscriptions
4011
4236
  // --------------------------------------------------------------------------
4012
4237
  async subscribe(data, options) {
@@ -5167,8 +5392,14 @@ var SearchService = class extends ServiceModule {
5167
5392
  };
5168
5393
  var PHOTO_BREAKPOINTS = [36, 150, 320, 640, 1080];
5169
5394
  var PhotoService = class extends ServiceModule {
5170
- constructor() {
5171
- super(...arguments);
5395
+ /**
5396
+ * @param storage Required for {@link uploadViaStorage}. Wired up by the
5397
+ * top-level {@link ScaleMule} constructor — most call sites should not
5398
+ * instantiate `PhotoService` directly.
5399
+ */
5400
+ constructor(client, storage) {
5401
+ super(client);
5402
+ this.storage = storage;
5172
5403
  this.basePath = "/v1/photos";
5173
5404
  }
5174
5405
  async upload(file, uploadOptions, requestOptions) {
@@ -5308,6 +5539,87 @@ var PhotoService = class extends ServiceModule {
5308
5539
  }
5309
5540
  };
5310
5541
  }
5542
+ /**
5543
+ * Upload a file to storage (browser → S3 direct, private, uncompressed) and
5544
+ * register it with the photo service so optimization + transform URLs work.
5545
+ *
5546
+ * The canonical chat-attachment / progressive-image upload primitive. Same
5547
+ * presigned-direct-to-S3 path as `storage.uploadPrivate()`, plus a follow-up
5548
+ * `photo.register()` call so `getTransformUrl()` / `getOptimalUrl()` return
5549
+ * usable URLs.
5550
+ *
5551
+ * The returned `optimized_url_promise` resolves once the photo's
5552
+ * `optimization_status` flips to `completed` (or once a 10s timeout fires;
5553
+ * the caller can still use `getTransformUrl()` directly — the on-demand
5554
+ * transform path produces a variant even before pre-rendered breakpoints
5555
+ * are cached). Phase 3 of ADR-2026-04-26 replaces the poll with a realtime
5556
+ * subscription.
5557
+ *
5558
+ * If `register()` fails after a successful storage upload (e.g. scan times
5559
+ * out), the file is *not* lost: the returned `file_id` is still valid as a
5560
+ * generic storage file. The SDK logs a warning and resolves
5561
+ * `optimized_url_promise` to `null`.
5562
+ */
5563
+ async uploadViaStorage(file, uploadOptions, requestOptions) {
5564
+ const uploadResult = await this.storage.uploadPrivate(file, {
5565
+ filename: uploadOptions?.filename,
5566
+ metadata: uploadOptions?.metadata,
5567
+ onProgress: uploadOptions?.onProgress,
5568
+ signal: uploadOptions?.signal
5569
+ });
5570
+ if (uploadResult.error || !uploadResult.data) {
5571
+ return { data: null, error: uploadResult.error };
5572
+ }
5573
+ const fileInfo = uploadResult.data;
5574
+ const fileId = fileInfo.id;
5575
+ const originalViewUrl = fileInfo.url ?? null;
5576
+ const registerResult = await this.register({ fileId, userId: uploadOptions?.userId }, requestOptions);
5577
+ if (registerResult.error || !registerResult.data) {
5578
+ console.warn(
5579
+ "[scalemule-sdk] photo.register() failed after storage upload; optimized variants unavailable.",
5580
+ registerResult.error
5581
+ );
5582
+ return {
5583
+ data: {
5584
+ file_id: fileId,
5585
+ photo_id: null,
5586
+ original_view_url: originalViewUrl,
5587
+ optimized_url_promise: Promise.resolve(null)
5588
+ },
5589
+ error: null
5590
+ };
5591
+ }
5592
+ const photoId = registerResult.data.id;
5593
+ const optimizedUrlPromise = this.pollOptimizationComplete(photoId, requestOptions);
5594
+ return {
5595
+ data: {
5596
+ file_id: fileId,
5597
+ photo_id: photoId,
5598
+ original_view_url: originalViewUrl,
5599
+ optimized_url_promise: optimizedUrlPromise
5600
+ },
5601
+ error: null
5602
+ };
5603
+ }
5604
+ /**
5605
+ * Poll {@link get} until the photo's `optimization_status` is `completed`
5606
+ * or the 10s timeout fires. Resolves to a usable transform URL on success;
5607
+ * `null` on timeout (caller should fall back to {@link getTransformUrl}
5608
+ * which uses the on-demand transform path).
5609
+ */
5610
+ async pollOptimizationComplete(photoId, requestOptions) {
5611
+ const intervalMs = 250;
5612
+ const maxAttempts = 40;
5613
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
5614
+ const result = await this.get(photoId, requestOptions);
5615
+ const status = result.data?.optimization_status;
5616
+ if (status === "completed") {
5617
+ return this.getOptimalUrl(photoId, 1080);
5618
+ }
5619
+ await new Promise((r) => setTimeout(r, intervalMs));
5620
+ }
5621
+ return null;
5622
+ }
5311
5623
  /** @deprecated Use upload() instead */
5312
5624
  async uploadPhoto(file, options) {
5313
5625
  return this.upload(file, options);
@@ -6112,7 +6424,7 @@ var ScaleMule = class {
6112
6424
  this.auth = new AuthService(this._client);
6113
6425
  this.storage = new StorageService(this._client);
6114
6426
  this.realtime = new RealtimeService(this._client);
6115
- this.video = new VideoService(this._client);
6427
+ this.video = new VideoService(this._client, this.storage);
6116
6428
  this.data = new DataService(this._client);
6117
6429
  this.chat = new ChatService(this._client);
6118
6430
  this.conference = new ConferenceService(this._client);
@@ -6139,7 +6451,7 @@ var ScaleMule = class {
6139
6451
  this.events = new EventsService(this._client);
6140
6452
  this.graph = new GraphService(this._client);
6141
6453
  this.functions = new FunctionsService(this._client);
6142
- this.photo = new PhotoService(this._client);
6454
+ this.photo = new PhotoService(this._client, this.storage);
6143
6455
  this.flagContent = new FlagContentService(this._client);
6144
6456
  this.creatorMaker = new CreatorMakerService(this._client);
6145
6457
  this.compliance = new ComplianceService(this._client);
@@ -7224,6 +7536,9 @@ function ScaleMuleProvider({
7224
7536
  client,
7225
7537
  money: money$1,
7226
7538
  realtime: baseClient.realtime,
7539
+ storage: baseClient.storage,
7540
+ photo: baseClient.photo,
7541
+ video: baseClient.video,
7227
7542
  user,
7228
7543
  setUser: handleSetUser,
7229
7544
  initializing,
@@ -8536,6 +8851,116 @@ function useContent(options = {}) {
8536
8851
  ]
8537
8852
  );
8538
8853
  }
8854
+ function useMedia() {
8855
+ const { storage, photo, video } = useScaleMule();
8856
+ const [uploading, setUploading] = react.useState(false);
8857
+ const [error, setError] = react.useState(null);
8858
+ const upload = react.useCallback(
8859
+ async (file, options) => {
8860
+ setUploading(true);
8861
+ setError(null);
8862
+ const isPublic = options?.is_public ?? false;
8863
+ const mimeType = file.type || "application/octet-stream";
8864
+ const sharedOpts = {
8865
+ filename: options?.filename,
8866
+ metadata: options?.metadata,
8867
+ onProgress: options?.onProgress,
8868
+ signal: options?.signal
8869
+ };
8870
+ try {
8871
+ if (mimeType.startsWith("image/") && !options?.skipPhotoRegister && !isPublic) {
8872
+ const r2 = await photo.uploadViaStorage(file, sharedOpts);
8873
+ if (r2.error || !r2.data) {
8874
+ throw r2.error ?? { code: "upload_error", message: "Upload failed", status: 0 };
8875
+ }
8876
+ return {
8877
+ file_id: r2.data.file_id,
8878
+ photo_id: r2.data.photo_id,
8879
+ original_view_url: r2.data.original_view_url,
8880
+ optimized_url_promise: r2.data.optimized_url_promise,
8881
+ hls_url_promise: Promise.resolve(null),
8882
+ mime_type: mimeType,
8883
+ is_public: false
8884
+ };
8885
+ }
8886
+ if (mimeType.startsWith("video/") && !isPublic) {
8887
+ const r2 = await video.uploadViaStorage(file, sharedOpts);
8888
+ if (r2.error || !r2.data) {
8889
+ throw r2.error ?? { code: "upload_error", message: "Upload failed", status: 0 };
8890
+ }
8891
+ return {
8892
+ file_id: r2.data.file_id,
8893
+ photo_id: null,
8894
+ original_view_url: r2.data.original_view_url,
8895
+ optimized_url_promise: Promise.resolve(null),
8896
+ hls_url_promise: r2.data.hls_url_promise,
8897
+ mime_type: mimeType,
8898
+ is_public: false
8899
+ };
8900
+ }
8901
+ if (mimeType.startsWith("image/") && isPublic) {
8902
+ const r2 = await storage.upload(file, {
8903
+ ...sharedOpts,
8904
+ isPublic: true,
8905
+ skipCompression: true
8906
+ });
8907
+ if (r2.error || !r2.data) {
8908
+ throw r2.error ?? { code: "upload_error", message: "Upload failed", status: 0 };
8909
+ }
8910
+ const f2 = r2.data;
8911
+ return {
8912
+ file_id: f2.id,
8913
+ photo_id: null,
8914
+ original_view_url: f2.url ?? null,
8915
+ optimized_url_promise: Promise.resolve(null),
8916
+ hls_url_promise: Promise.resolve(null),
8917
+ mime_type: mimeType,
8918
+ is_public: f2.is_public ?? true
8919
+ };
8920
+ }
8921
+ const r = isPublic ? await storage.upload(file, { ...sharedOpts, isPublic: true, skipCompression: true }) : await storage.uploadPrivate(file, sharedOpts);
8922
+ if (r.error || !r.data) {
8923
+ throw r.error ?? { code: "upload_error", message: "Upload failed", status: 0 };
8924
+ }
8925
+ const f = r.data;
8926
+ return {
8927
+ file_id: f.id,
8928
+ photo_id: null,
8929
+ original_view_url: f.url ?? null,
8930
+ optimized_url_promise: Promise.resolve(null),
8931
+ hls_url_promise: Promise.resolve(null),
8932
+ mime_type: mimeType,
8933
+ is_public: f.is_public ?? isPublic
8934
+ };
8935
+ } catch (err) {
8936
+ const e = err;
8937
+ setError(e);
8938
+ throw e;
8939
+ } finally {
8940
+ setUploading(false);
8941
+ }
8942
+ },
8943
+ [storage, photo]
8944
+ );
8945
+ const cancelUpload = react.useCallback(
8946
+ async (fileId) => {
8947
+ setError(null);
8948
+ try {
8949
+ const r = await storage.delete(fileId);
8950
+ if (r.error) {
8951
+ if (r.error.status === 404) return;
8952
+ throw r.error;
8953
+ }
8954
+ } catch (err) {
8955
+ const e = err;
8956
+ setError(e);
8957
+ throw e;
8958
+ }
8959
+ },
8960
+ [storage]
8961
+ );
8962
+ return { upload, cancelUpload, error, uploading };
8963
+ }
8539
8964
 
8540
8965
  // src/hooks/useMoney.ts
8541
8966
  var useMoney = useMoneyClient;
@@ -9762,6 +10187,10 @@ var STYLES = {
9762
10187
  fontSize: 13,
9763
10188
  color: "#dc2626"
9764
10189
  },
10190
+ note: {
10191
+ fontSize: 13,
10192
+ color: "#475569"
10193
+ },
9765
10194
  success: {
9766
10195
  fontSize: 14,
9767
10196
  fontWeight: 500,
@@ -9771,21 +10200,13 @@ var STYLES = {
9771
10200
  };
9772
10201
  var ALL_TYPES = ["bug_report", "feature_request", "improvement", "other"];
9773
10202
  function FeedbackWidget(props) {
9774
- const {
9775
- position = "bottom-right",
9776
- defaultType = "feature_request",
9777
- allowedTypes = ALL_TYPES,
9778
- triggerLabel = "Feedback",
9779
- theme = "auto",
9780
- className,
9781
- enableRating = true,
9782
- ratingLabel = "How would you rate your experience?",
9783
- onSubmitted
9784
- } = props;
9785
10203
  const sm = useScaleMule();
10204
+ const { client } = sm;
9786
10205
  const { submit } = useFeedback({ enabled: false });
10206
+ const [serverConfig, setServerConfig] = react.useState(null);
10207
+ const [configLoaded, setConfigLoaded] = react.useState(false);
9787
10208
  const [open, setOpen] = react.useState(false);
9788
- const [type, setType] = react.useState(defaultType);
10209
+ const [type, setType] = react.useState("feature_request");
9789
10210
  const [title, setTitle] = react.useState("");
9790
10211
  const [description, setDescription] = react.useState("");
9791
10212
  const [email, setEmail] = react.useState("");
@@ -9794,10 +10215,39 @@ function FeedbackWidget(props) {
9794
10215
  const [submitting, setSubmitting] = react.useState(false);
9795
10216
  const [errMsg, setErrMsg] = react.useState(null);
9796
10217
  const [done, setDone] = react.useState(false);
9797
- Boolean(sm.user || sm.user === void 0 ? false : false);
9798
10218
  const signedIn = Boolean(sm.user);
10219
+ const resolvedAllowedTypes = props.allowedTypes ?? serverConfig?.allowed_types ?? ALL_TYPES;
10220
+ const resolvedDefaultType = (props.defaultType && resolvedAllowedTypes.includes(props.defaultType) ? props.defaultType : resolvedAllowedTypes[0]) ?? "feature_request";
10221
+ const resolvedPosition = props.position ?? serverConfig?.widget_position ?? "bottom-right";
10222
+ const resolvedTheme = props.theme ?? serverConfig?.widget_theme ?? "auto";
10223
+ const allowAnonymous = serverConfig?.allow_anonymous ?? true;
10224
+ const anonymousBlocked = !signedIn && !allowAnonymous;
10225
+ react.useEffect(() => {
10226
+ let active = true;
10227
+ client.get("/v1/feedback/widget-config").then((config) => {
10228
+ if (active) setServerConfig(config);
10229
+ }).catch(() => {
10230
+ if (active) setServerConfig(null);
10231
+ }).finally(() => {
10232
+ if (active) setConfigLoaded(true);
10233
+ });
10234
+ return () => {
10235
+ active = false;
10236
+ };
10237
+ }, [client]);
10238
+ react.useEffect(() => {
10239
+ setType(
10240
+ (current) => resolvedAllowedTypes.includes(current) ? current : resolvedDefaultType
10241
+ );
10242
+ }, [resolvedAllowedTypes, resolvedDefaultType]);
10243
+ if (!configLoaded) {
10244
+ return null;
10245
+ }
10246
+ if (serverConfig && !serverConfig.enabled) {
10247
+ return null;
10248
+ }
9799
10249
  function reset() {
9800
- setType(defaultType);
10250
+ setType(resolvedDefaultType);
9801
10251
  setTitle("");
9802
10252
  setDescription("");
9803
10253
  setEmail("");
@@ -9809,6 +10259,10 @@ function FeedbackWidget(props) {
9809
10259
  async function handleSubmit(e) {
9810
10260
  e.preventDefault();
9811
10261
  if (!title.trim() || !description.trim()) return;
10262
+ if (anonymousBlocked) {
10263
+ setErrMsg("Please sign in to send feedback for this app");
10264
+ return;
10265
+ }
9812
10266
  if (!signedIn && !email.trim()) {
9813
10267
  setErrMsg("email is required when not signed in");
9814
10268
  return;
@@ -9825,7 +10279,7 @@ function FeedbackWidget(props) {
9825
10279
  tags
9826
10280
  });
9827
10281
  setDone(true);
9828
- onSubmitted?.(item);
10282
+ props.onSubmitted?.(item);
9829
10283
  } catch (err) {
9830
10284
  const message = err && typeof err === "object" && "message" in err ? String(err.message) : "Submit failed";
9831
10285
  setErrMsg(message);
@@ -9837,16 +10291,16 @@ function FeedbackWidget(props) {
9837
10291
  setOpen(false);
9838
10292
  setTimeout(reset, 200);
9839
10293
  }
9840
- const dark = theme === "dark" || theme === "auto" && typeof window !== "undefined" && window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
10294
+ const dark = resolvedTheme === "dark" || resolvedTheme === "auto" && typeof window !== "undefined" && window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
9841
10295
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
9842
10296
  /* @__PURE__ */ jsxRuntime.jsx(
9843
10297
  "button",
9844
10298
  {
9845
10299
  type: "button",
9846
10300
  onClick: () => setOpen(true),
9847
- className,
9848
- style: { ...STYLES.trigger, ...POSITION_STYLES[position] },
9849
- children: triggerLabel
10301
+ className: props.className,
10302
+ style: { ...STYLES.trigger, ...POSITION_STYLES[resolvedPosition] },
10303
+ children: props.triggerLabel ?? "Feedback"
9850
10304
  }
9851
10305
  ),
9852
10306
  open && /* @__PURE__ */ jsxRuntime.jsx("div", { role: "dialog", "aria-modal": "true", style: STYLES.overlay, onClick: handleClose, children: /* @__PURE__ */ jsxRuntime.jsx(
@@ -9881,8 +10335,8 @@ function FeedbackWidget(props) {
9881
10335
  }
9882
10336
  )
9883
10337
  ] }),
9884
- enableRating && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
9885
- /* @__PURE__ */ jsxRuntime.jsx("div", { style: STYLES.label, children: ratingLabel }),
10338
+ props.enableRating !== false && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
10339
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: STYLES.label, children: props.ratingLabel ?? "How would you rate your experience?" }),
9886
10340
  /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: 4, marginTop: 4 }, children: [
9887
10341
  [1, 2, 3, 4, 5].map((n) => {
9888
10342
  const active = (hoverRating ?? rating ?? 0) >= n;
@@ -9936,7 +10390,7 @@ function FeedbackWidget(props) {
9936
10390
  style: STYLES.field,
9937
10391
  value: type,
9938
10392
  onChange: (e) => setType(e.target.value),
9939
- children: allowedTypes.map((t) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: t, children: TYPE_LABELS[t] }, t))
10393
+ children: resolvedAllowedTypes.map((t) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: t, children: TYPE_LABELS[t] }, t))
9940
10394
  }
9941
10395
  )
9942
10396
  ] }),
@@ -9968,7 +10422,7 @@ function FeedbackWidget(props) {
9968
10422
  }
9969
10423
  )
9970
10424
  ] }),
9971
- !signedIn && /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
10425
+ !signedIn && allowAnonymous && /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
9972
10426
  /* @__PURE__ */ jsxRuntime.jsx("div", { style: STYLES.label, children: "Email" }),
9973
10427
  /* @__PURE__ */ jsxRuntime.jsx(
9974
10428
  "input",
@@ -9982,10 +10436,19 @@ function FeedbackWidget(props) {
9982
10436
  }
9983
10437
  )
9984
10438
  ] }),
10439
+ anonymousBlocked && /* @__PURE__ */ jsxRuntime.jsx("div", { style: STYLES.note, children: "This app only accepts feedback from signed-in users." }),
9985
10440
  errMsg && /* @__PURE__ */ jsxRuntime.jsx("div", { style: STYLES.error, children: errMsg }),
9986
10441
  /* @__PURE__ */ jsxRuntime.jsxs("div", { style: STYLES.rowEnd, children: [
9987
10442
  /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", style: STYLES.secondaryBtn, onClick: handleClose, children: "Cancel" }),
9988
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "submit", style: STYLES.primaryBtn, disabled: submitting, children: submitting ? "Sending\u2026" : "Send" })
10443
+ /* @__PURE__ */ jsxRuntime.jsx(
10444
+ "button",
10445
+ {
10446
+ type: "submit",
10447
+ style: STYLES.primaryBtn,
10448
+ disabled: submitting || anonymousBlocked,
10449
+ children: submitting ? "Sending\u2026" : "Send"
10450
+ }
10451
+ )
9989
10452
  ] })
9990
10453
  ] })
9991
10454
  }
@@ -10308,6 +10771,7 @@ exports.useBilling = useBilling;
10308
10771
  exports.useContent = useContent;
10309
10772
  exports.useFeatureFlags = useFeatureFlags;
10310
10773
  exports.useFeedback = useFeedback;
10774
+ exports.useMedia = useMedia;
10311
10775
  exports.useMoney = useMoney;
10312
10776
  exports.useMoneyClient = useMoneyClient;
10313
10777
  exports.usePushNotifications = usePushNotifications;