@scalemule/nextjs 0.1.0 → 0.1.2

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
  // --------------------------------------------------------------------------
@@ -3788,6 +3822,18 @@ var ChatService = class extends ServiceModule {
3788
3822
  return this.post(`/messages/${messageId}/reactions`, data, options);
3789
3823
  }
3790
3824
  // --------------------------------------------------------------------------
3825
+ // Pins
3826
+ // --------------------------------------------------------------------------
3827
+ async pinMessage(messageId, options) {
3828
+ return this.post(`/messages/${messageId}/pin`, void 0, options);
3829
+ }
3830
+ async unpinMessage(messageId, options) {
3831
+ return this.del(`/messages/${messageId}/pin`, options);
3832
+ }
3833
+ async getPinnedMessages(conversationId, options) {
3834
+ return this._get(`/conversations/${conversationId}/pins`, options);
3835
+ }
3836
+ // --------------------------------------------------------------------------
3791
3837
  // Typing & Read Receipts
3792
3838
  // --------------------------------------------------------------------------
3793
3839
  async sendTyping(conversationId, options) {
@@ -3998,15 +4044,80 @@ var BillingService = class extends ServiceModule {
3998
4044
  );
3999
4045
  }
4000
4046
  // --------------------------------------------------------------------------
4001
- // Customers
4047
+ // Customers (legacy user-scoped — deprecated in favor of account-scoped)
4002
4048
  // --------------------------------------------------------------------------
4049
+ /** @deprecated Use `getAccountCustomer({ account_id })`. Targets the user-scoped legacy endpoint. */
4003
4050
  async createCustomer(data, options) {
4004
4051
  return this.post("/customers", data, options);
4005
4052
  }
4053
+ /** @deprecated Use `attachAccountPaymentMethod({ account_id }, ...)`. Targets the user-scoped legacy endpoint. */
4006
4054
  async addPaymentMethod(data, options) {
4007
4055
  return this.post("/payment-methods", data, options);
4008
4056
  }
4009
4057
  // --------------------------------------------------------------------------
4058
+ // Account-Scoped Billing (v2 — ADR-2026-04-25)
4059
+ //
4060
+ // The customer/payment-method/invoice record set belongs to an `app_account`
4061
+ // (typically a `customer_org`), not to a single user. These methods target
4062
+ // the `/v1/billing/accounts/{account_id}/*` endpoints described in
4063
+ // openapi/scalemule-money-billing-v1.yaml.
4064
+ // --------------------------------------------------------------------------
4065
+ accountBase(accountId) {
4066
+ return `/v1/billing/accounts/${encodeURIComponent(accountId)}`;
4067
+ }
4068
+ async getAccountCustomer(req, options) {
4069
+ const path = this.withQuery(`${this.accountBase(req.account_id)}/customer`, {
4070
+ billing_mode: req.billing_mode
4071
+ });
4072
+ return this.client.get(path, options);
4073
+ }
4074
+ async listAccountPaymentMethods(req, options) {
4075
+ const path = this.withQuery(`${this.accountBase(req.account_id)}/payment-methods`, {
4076
+ billing_mode: req.billing_mode
4077
+ });
4078
+ return this.client.get(path, options);
4079
+ }
4080
+ async attachAccountPaymentMethod(req, body, options) {
4081
+ const path = this.withQuery(`${this.accountBase(req.account_id)}/payment-methods`, {
4082
+ billing_mode: req.billing_mode
4083
+ });
4084
+ return this.client.post(path, body, options);
4085
+ }
4086
+ async detachAccountPaymentMethod(req, paymentMethodId, options) {
4087
+ const path = this.withQuery(
4088
+ `${this.accountBase(req.account_id)}/payment-methods/${encodeURIComponent(paymentMethodId)}`,
4089
+ { billing_mode: req.billing_mode }
4090
+ );
4091
+ return this.client.del(path, options);
4092
+ }
4093
+ async setDefaultAccountPaymentMethod(req, paymentMethodId, options) {
4094
+ const path = this.withQuery(
4095
+ `${this.accountBase(req.account_id)}/payment-methods/${encodeURIComponent(paymentMethodId)}/default`,
4096
+ { billing_mode: req.billing_mode }
4097
+ );
4098
+ return this.client.put(path, void 0, options);
4099
+ }
4100
+ async listAccountInvoices(req, params, options) {
4101
+ const path = this.withQuery(`${this.accountBase(req.account_id)}/invoices`, {
4102
+ billing_mode: req.billing_mode,
4103
+ limit: params?.limit,
4104
+ cursor: params?.cursor
4105
+ });
4106
+ return this.client.get(path, options);
4107
+ }
4108
+ async getAccountInvoice(req, invoiceId, options) {
4109
+ const path = this.withQuery(`${this.accountBase(req.account_id)}/invoices/${encodeURIComponent(invoiceId)}`, {
4110
+ billing_mode: req.billing_mode
4111
+ });
4112
+ return this.client.get(path, options);
4113
+ }
4114
+ async createAccountPortalSession(req, body, options) {
4115
+ const path = this.withQuery(`${this.accountBase(req.account_id)}/portal-session`, {
4116
+ billing_mode: req.billing_mode
4117
+ });
4118
+ return this.client.post(path, body ?? {}, options);
4119
+ }
4120
+ // --------------------------------------------------------------------------
4010
4121
  // Subscriptions
4011
4122
  // --------------------------------------------------------------------------
4012
4123
  async subscribe(data, options) {
@@ -5167,8 +5278,14 @@ var SearchService = class extends ServiceModule {
5167
5278
  };
5168
5279
  var PHOTO_BREAKPOINTS = [36, 150, 320, 640, 1080];
5169
5280
  var PhotoService = class extends ServiceModule {
5170
- constructor() {
5171
- super(...arguments);
5281
+ /**
5282
+ * @param storage Required for {@link uploadViaStorage}. Wired up by the
5283
+ * top-level {@link ScaleMule} constructor — most call sites should not
5284
+ * instantiate `PhotoService` directly.
5285
+ */
5286
+ constructor(client, storage) {
5287
+ super(client);
5288
+ this.storage = storage;
5172
5289
  this.basePath = "/v1/photos";
5173
5290
  }
5174
5291
  async upload(file, uploadOptions, requestOptions) {
@@ -5308,6 +5425,87 @@ var PhotoService = class extends ServiceModule {
5308
5425
  }
5309
5426
  };
5310
5427
  }
5428
+ /**
5429
+ * Upload a file to storage (browser → S3 direct, private, uncompressed) and
5430
+ * register it with the photo service so optimization + transform URLs work.
5431
+ *
5432
+ * The canonical chat-attachment / progressive-image upload primitive. Same
5433
+ * presigned-direct-to-S3 path as `storage.uploadPrivate()`, plus a follow-up
5434
+ * `photo.register()` call so `getTransformUrl()` / `getOptimalUrl()` return
5435
+ * usable URLs.
5436
+ *
5437
+ * The returned `optimized_url_promise` resolves once the photo's
5438
+ * `optimization_status` flips to `completed` (or once a 10s timeout fires;
5439
+ * the caller can still use `getTransformUrl()` directly — the on-demand
5440
+ * transform path produces a variant even before pre-rendered breakpoints
5441
+ * are cached). Phase 3 of ADR-2026-04-26 replaces the poll with a realtime
5442
+ * subscription.
5443
+ *
5444
+ * If `register()` fails after a successful storage upload (e.g. scan times
5445
+ * out), the file is *not* lost: the returned `file_id` is still valid as a
5446
+ * generic storage file. The SDK logs a warning and resolves
5447
+ * `optimized_url_promise` to `null`.
5448
+ */
5449
+ async uploadViaStorage(file, uploadOptions, requestOptions) {
5450
+ const uploadResult = await this.storage.uploadPrivate(file, {
5451
+ filename: uploadOptions?.filename,
5452
+ metadata: uploadOptions?.metadata,
5453
+ onProgress: uploadOptions?.onProgress,
5454
+ signal: uploadOptions?.signal
5455
+ });
5456
+ if (uploadResult.error || !uploadResult.data) {
5457
+ return { data: null, error: uploadResult.error };
5458
+ }
5459
+ const fileInfo = uploadResult.data;
5460
+ const fileId = fileInfo.id;
5461
+ const originalViewUrl = fileInfo.url ?? null;
5462
+ const registerResult = await this.register({ fileId, userId: uploadOptions?.userId }, requestOptions);
5463
+ if (registerResult.error || !registerResult.data) {
5464
+ console.warn(
5465
+ "[scalemule-sdk] photo.register() failed after storage upload; optimized variants unavailable.",
5466
+ registerResult.error
5467
+ );
5468
+ return {
5469
+ data: {
5470
+ file_id: fileId,
5471
+ photo_id: null,
5472
+ original_view_url: originalViewUrl,
5473
+ optimized_url_promise: Promise.resolve(null)
5474
+ },
5475
+ error: null
5476
+ };
5477
+ }
5478
+ const photoId = registerResult.data.id;
5479
+ const optimizedUrlPromise = this.pollOptimizationComplete(photoId, requestOptions);
5480
+ return {
5481
+ data: {
5482
+ file_id: fileId,
5483
+ photo_id: photoId,
5484
+ original_view_url: originalViewUrl,
5485
+ optimized_url_promise: optimizedUrlPromise
5486
+ },
5487
+ error: null
5488
+ };
5489
+ }
5490
+ /**
5491
+ * Poll {@link get} until the photo's `optimization_status` is `completed`
5492
+ * or the 10s timeout fires. Resolves to a usable transform URL on success;
5493
+ * `null` on timeout (caller should fall back to {@link getTransformUrl}
5494
+ * which uses the on-demand transform path).
5495
+ */
5496
+ async pollOptimizationComplete(photoId, requestOptions) {
5497
+ const intervalMs = 250;
5498
+ const maxAttempts = 40;
5499
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
5500
+ const result = await this.get(photoId, requestOptions);
5501
+ const status = result.data?.optimization_status;
5502
+ if (status === "completed") {
5503
+ return this.getOptimalUrl(photoId, 1080);
5504
+ }
5505
+ await new Promise((r) => setTimeout(r, intervalMs));
5506
+ }
5507
+ return null;
5508
+ }
5311
5509
  /** @deprecated Use upload() instead */
5312
5510
  async uploadPhoto(file, options) {
5313
5511
  return this.upload(file, options);
@@ -6139,7 +6337,7 @@ var ScaleMule = class {
6139
6337
  this.events = new EventsService(this._client);
6140
6338
  this.graph = new GraphService(this._client);
6141
6339
  this.functions = new FunctionsService(this._client);
6142
- this.photo = new PhotoService(this._client);
6340
+ this.photo = new PhotoService(this._client, this.storage);
6143
6341
  this.flagContent = new FlagContentService(this._client);
6144
6342
  this.creatorMaker = new CreatorMakerService(this._client);
6145
6343
  this.compliance = new ComplianceService(this._client);
@@ -7224,6 +7422,8 @@ function ScaleMuleProvider({
7224
7422
  client,
7225
7423
  money: money$1,
7226
7424
  realtime: baseClient.realtime,
7425
+ storage: baseClient.storage,
7426
+ photo: baseClient.photo,
7227
7427
  user,
7228
7428
  setUser: handleSetUser,
7229
7429
  initializing,
@@ -8536,6 +8736,101 @@ function useContent(options = {}) {
8536
8736
  ]
8537
8737
  );
8538
8738
  }
8739
+ function useMedia() {
8740
+ const { storage, photo } = useScaleMule();
8741
+ const [uploading, setUploading] = react.useState(false);
8742
+ const [error, setError] = react.useState(null);
8743
+ const upload = react.useCallback(
8744
+ async (file, options) => {
8745
+ setUploading(true);
8746
+ setError(null);
8747
+ const isPublic = options?.is_public ?? false;
8748
+ const mimeType = file.type || "application/octet-stream";
8749
+ const sharedOpts = {
8750
+ filename: options?.filename,
8751
+ metadata: options?.metadata,
8752
+ onProgress: options?.onProgress,
8753
+ signal: options?.signal
8754
+ };
8755
+ try {
8756
+ if (mimeType.startsWith("image/") && !options?.skipPhotoRegister && !isPublic) {
8757
+ const r2 = await photo.uploadViaStorage(file, sharedOpts);
8758
+ if (r2.error || !r2.data) {
8759
+ throw r2.error ?? { code: "upload_error", message: "Upload failed", status: 0 };
8760
+ }
8761
+ return {
8762
+ file_id: r2.data.file_id,
8763
+ photo_id: r2.data.photo_id,
8764
+ original_view_url: r2.data.original_view_url,
8765
+ optimized_url_promise: r2.data.optimized_url_promise,
8766
+ hls_url_promise: Promise.resolve(null),
8767
+ mime_type: mimeType,
8768
+ is_public: false
8769
+ };
8770
+ }
8771
+ if (mimeType.startsWith("image/") && isPublic) {
8772
+ const r2 = await storage.upload(file, {
8773
+ ...sharedOpts,
8774
+ isPublic: true,
8775
+ skipCompression: true
8776
+ });
8777
+ if (r2.error || !r2.data) {
8778
+ throw r2.error ?? { code: "upload_error", message: "Upload failed", status: 0 };
8779
+ }
8780
+ const f2 = r2.data;
8781
+ return {
8782
+ file_id: f2.id,
8783
+ photo_id: null,
8784
+ original_view_url: f2.url ?? null,
8785
+ optimized_url_promise: Promise.resolve(null),
8786
+ hls_url_promise: Promise.resolve(null),
8787
+ mime_type: mimeType,
8788
+ is_public: f2.is_public ?? true
8789
+ };
8790
+ }
8791
+ const r = isPublic ? await storage.upload(file, { ...sharedOpts, isPublic: true, skipCompression: true }) : await storage.uploadPrivate(file, sharedOpts);
8792
+ if (r.error || !r.data) {
8793
+ throw r.error ?? { code: "upload_error", message: "Upload failed", status: 0 };
8794
+ }
8795
+ const f = r.data;
8796
+ return {
8797
+ file_id: f.id,
8798
+ photo_id: null,
8799
+ original_view_url: f.url ?? null,
8800
+ optimized_url_promise: Promise.resolve(null),
8801
+ hls_url_promise: Promise.resolve(null),
8802
+ mime_type: mimeType,
8803
+ is_public: f.is_public ?? isPublic
8804
+ };
8805
+ } catch (err) {
8806
+ const e = err;
8807
+ setError(e);
8808
+ throw e;
8809
+ } finally {
8810
+ setUploading(false);
8811
+ }
8812
+ },
8813
+ [storage, photo]
8814
+ );
8815
+ const cancelUpload = react.useCallback(
8816
+ async (fileId) => {
8817
+ setError(null);
8818
+ try {
8819
+ const r = await storage.delete(fileId);
8820
+ if (r.error) {
8821
+ if (r.error.status === 404) return;
8822
+ throw r.error;
8823
+ }
8824
+ } catch (err) {
8825
+ const e = err;
8826
+ setError(e);
8827
+ throw e;
8828
+ }
8829
+ },
8830
+ [storage]
8831
+ );
8832
+ return { upload, cancelUpload, error, uploading };
8833
+ }
8539
8834
 
8540
8835
  // src/hooks/useMoney.ts
8541
8836
  var useMoney = useMoneyClient;
@@ -9762,6 +10057,10 @@ var STYLES = {
9762
10057
  fontSize: 13,
9763
10058
  color: "#dc2626"
9764
10059
  },
10060
+ note: {
10061
+ fontSize: 13,
10062
+ color: "#475569"
10063
+ },
9765
10064
  success: {
9766
10065
  fontSize: 14,
9767
10066
  fontWeight: 500,
@@ -9771,38 +10070,69 @@ var STYLES = {
9771
10070
  };
9772
10071
  var ALL_TYPES = ["bug_report", "feature_request", "improvement", "other"];
9773
10072
  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
- onSubmitted
9782
- } = props;
9783
10073
  const sm = useScaleMule();
10074
+ const { client } = sm;
9784
10075
  const { submit } = useFeedback({ enabled: false });
10076
+ const [serverConfig, setServerConfig] = react.useState(null);
10077
+ const [configLoaded, setConfigLoaded] = react.useState(false);
9785
10078
  const [open, setOpen] = react.useState(false);
9786
- const [type, setType] = react.useState(defaultType);
10079
+ const [type, setType] = react.useState("feature_request");
9787
10080
  const [title, setTitle] = react.useState("");
9788
10081
  const [description, setDescription] = react.useState("");
9789
10082
  const [email, setEmail] = react.useState("");
10083
+ const [rating, setRating] = react.useState(null);
10084
+ const [hoverRating, setHoverRating] = react.useState(null);
9790
10085
  const [submitting, setSubmitting] = react.useState(false);
9791
10086
  const [errMsg, setErrMsg] = react.useState(null);
9792
10087
  const [done, setDone] = react.useState(false);
9793
- Boolean(sm.user || sm.user === void 0 ? false : false);
9794
10088
  const signedIn = Boolean(sm.user);
10089
+ const resolvedAllowedTypes = props.allowedTypes ?? serverConfig?.allowed_types ?? ALL_TYPES;
10090
+ const resolvedDefaultType = (props.defaultType && resolvedAllowedTypes.includes(props.defaultType) ? props.defaultType : resolvedAllowedTypes[0]) ?? "feature_request";
10091
+ const resolvedPosition = props.position ?? serverConfig?.widget_position ?? "bottom-right";
10092
+ const resolvedTheme = props.theme ?? serverConfig?.widget_theme ?? "auto";
10093
+ const allowAnonymous = serverConfig?.allow_anonymous ?? true;
10094
+ const anonymousBlocked = !signedIn && !allowAnonymous;
10095
+ react.useEffect(() => {
10096
+ let active = true;
10097
+ client.get("/v1/feedback/widget-config").then((config) => {
10098
+ if (active) setServerConfig(config);
10099
+ }).catch(() => {
10100
+ if (active) setServerConfig(null);
10101
+ }).finally(() => {
10102
+ if (active) setConfigLoaded(true);
10103
+ });
10104
+ return () => {
10105
+ active = false;
10106
+ };
10107
+ }, [client]);
10108
+ react.useEffect(() => {
10109
+ setType(
10110
+ (current) => resolvedAllowedTypes.includes(current) ? current : resolvedDefaultType
10111
+ );
10112
+ }, [resolvedAllowedTypes, resolvedDefaultType]);
10113
+ if (!configLoaded) {
10114
+ return null;
10115
+ }
10116
+ if (serverConfig && !serverConfig.enabled) {
10117
+ return null;
10118
+ }
9795
10119
  function reset() {
9796
- setType(defaultType);
10120
+ setType(resolvedDefaultType);
9797
10121
  setTitle("");
9798
10122
  setDescription("");
9799
10123
  setEmail("");
10124
+ setRating(null);
10125
+ setHoverRating(null);
9800
10126
  setErrMsg(null);
9801
10127
  setDone(false);
9802
10128
  }
9803
10129
  async function handleSubmit(e) {
9804
10130
  e.preventDefault();
9805
10131
  if (!title.trim() || !description.trim()) return;
10132
+ if (anonymousBlocked) {
10133
+ setErrMsg("Please sign in to send feedback for this app");
10134
+ return;
10135
+ }
9806
10136
  if (!signedIn && !email.trim()) {
9807
10137
  setErrMsg("email is required when not signed in");
9808
10138
  return;
@@ -9810,14 +10140,16 @@ function FeedbackWidget(props) {
9810
10140
  setSubmitting(true);
9811
10141
  setErrMsg(null);
9812
10142
  try {
10143
+ const tags = rating != null ? [`rating:${rating}`] : void 0;
9813
10144
  const item = await submit({
9814
10145
  type,
9815
10146
  title: title.trim(),
9816
10147
  description: description.trim(),
9817
- email: signedIn ? void 0 : email.trim() || void 0
10148
+ email: signedIn ? void 0 : email.trim() || void 0,
10149
+ tags
9818
10150
  });
9819
10151
  setDone(true);
9820
- onSubmitted?.(item);
10152
+ props.onSubmitted?.(item);
9821
10153
  } catch (err) {
9822
10154
  const message = err && typeof err === "object" && "message" in err ? String(err.message) : "Submit failed";
9823
10155
  setErrMsg(message);
@@ -9829,16 +10161,16 @@ function FeedbackWidget(props) {
9829
10161
  setOpen(false);
9830
10162
  setTimeout(reset, 200);
9831
10163
  }
9832
- const dark = theme === "dark" || theme === "auto" && typeof window !== "undefined" && window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
10164
+ const dark = resolvedTheme === "dark" || resolvedTheme === "auto" && typeof window !== "undefined" && window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
9833
10165
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
9834
10166
  /* @__PURE__ */ jsxRuntime.jsx(
9835
10167
  "button",
9836
10168
  {
9837
10169
  type: "button",
9838
10170
  onClick: () => setOpen(true),
9839
- className,
9840
- style: { ...STYLES.trigger, ...POSITION_STYLES[position] },
9841
- children: triggerLabel
10171
+ className: props.className,
10172
+ style: { ...STYLES.trigger, ...POSITION_STYLES[resolvedPosition] },
10173
+ children: props.triggerLabel ?? "Feedback"
9842
10174
  }
9843
10175
  ),
9844
10176
  open && /* @__PURE__ */ jsxRuntime.jsx("div", { role: "dialog", "aria-modal": "true", style: STYLES.overlay, onClick: handleClose, children: /* @__PURE__ */ jsxRuntime.jsx(
@@ -9873,6 +10205,53 @@ function FeedbackWidget(props) {
9873
10205
  }
9874
10206
  )
9875
10207
  ] }),
10208
+ props.enableRating !== false && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
10209
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: STYLES.label, children: props.ratingLabel ?? "How would you rate your experience?" }),
10210
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: 4, marginTop: 4 }, children: [
10211
+ [1, 2, 3, 4, 5].map((n) => {
10212
+ const active = (hoverRating ?? rating ?? 0) >= n;
10213
+ return /* @__PURE__ */ jsxRuntime.jsx(
10214
+ "button",
10215
+ {
10216
+ type: "button",
10217
+ "aria-label": `${n} star${n === 1 ? "" : "s"}`,
10218
+ onClick: () => setRating(rating === n ? null : n),
10219
+ onMouseEnter: () => setHoverRating(n),
10220
+ onMouseLeave: () => setHoverRating(null),
10221
+ style: {
10222
+ background: "transparent",
10223
+ border: "none",
10224
+ cursor: "pointer",
10225
+ padding: 2,
10226
+ fontSize: 24,
10227
+ lineHeight: 1,
10228
+ color: active ? "#f59e0b" : "#cbd5e1",
10229
+ transition: "color 80ms ease"
10230
+ },
10231
+ children: active ? "\u2605" : "\u2606"
10232
+ },
10233
+ n
10234
+ );
10235
+ }),
10236
+ rating != null && /* @__PURE__ */ jsxRuntime.jsx(
10237
+ "button",
10238
+ {
10239
+ type: "button",
10240
+ onClick: () => setRating(null),
10241
+ style: {
10242
+ marginLeft: 8,
10243
+ background: "transparent",
10244
+ border: "none",
10245
+ color: "inherit",
10246
+ opacity: 0.6,
10247
+ fontSize: 12,
10248
+ cursor: "pointer"
10249
+ },
10250
+ children: "clear"
10251
+ }
10252
+ )
10253
+ ] })
10254
+ ] }),
9876
10255
  /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
9877
10256
  /* @__PURE__ */ jsxRuntime.jsx("div", { style: STYLES.label, children: "Type" }),
9878
10257
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -9881,7 +10260,7 @@ function FeedbackWidget(props) {
9881
10260
  style: STYLES.field,
9882
10261
  value: type,
9883
10262
  onChange: (e) => setType(e.target.value),
9884
- children: allowedTypes.map((t) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: t, children: TYPE_LABELS[t] }, t))
10263
+ children: resolvedAllowedTypes.map((t) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: t, children: TYPE_LABELS[t] }, t))
9885
10264
  }
9886
10265
  )
9887
10266
  ] }),
@@ -9913,7 +10292,7 @@ function FeedbackWidget(props) {
9913
10292
  }
9914
10293
  )
9915
10294
  ] }),
9916
- !signedIn && /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
10295
+ !signedIn && allowAnonymous && /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
9917
10296
  /* @__PURE__ */ jsxRuntime.jsx("div", { style: STYLES.label, children: "Email" }),
9918
10297
  /* @__PURE__ */ jsxRuntime.jsx(
9919
10298
  "input",
@@ -9927,10 +10306,19 @@ function FeedbackWidget(props) {
9927
10306
  }
9928
10307
  )
9929
10308
  ] }),
10309
+ anonymousBlocked && /* @__PURE__ */ jsxRuntime.jsx("div", { style: STYLES.note, children: "This app only accepts feedback from signed-in users." }),
9930
10310
  errMsg && /* @__PURE__ */ jsxRuntime.jsx("div", { style: STYLES.error, children: errMsg }),
9931
10311
  /* @__PURE__ */ jsxRuntime.jsxs("div", { style: STYLES.rowEnd, children: [
9932
10312
  /* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", style: STYLES.secondaryBtn, onClick: handleClose, children: "Cancel" }),
9933
- /* @__PURE__ */ jsxRuntime.jsx("button", { type: "submit", style: STYLES.primaryBtn, disabled: submitting, children: submitting ? "Sending\u2026" : "Send" })
10313
+ /* @__PURE__ */ jsxRuntime.jsx(
10314
+ "button",
10315
+ {
10316
+ type: "submit",
10317
+ style: STYLES.primaryBtn,
10318
+ disabled: submitting || anonymousBlocked,
10319
+ children: submitting ? "Sending\u2026" : "Send"
10320
+ }
10321
+ )
9934
10322
  ] })
9935
10323
  ] })
9936
10324
  }
@@ -10253,6 +10641,7 @@ exports.useBilling = useBilling;
10253
10641
  exports.useContent = useContent;
10254
10642
  exports.useFeatureFlags = useFeatureFlags;
10255
10643
  exports.useFeedback = useFeedback;
10644
+ exports.useMedia = useMedia;
10256
10645
  exports.useMoney = useMoney;
10257
10646
  exports.useMoneyClient = useMoneyClient;
10258
10647
  exports.usePushNotifications = usePushNotifications;