@scalemule/nextjs 0.1.1 → 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.d.mts +139 -2
- package/dist/index.d.ts +139 -2
- package/dist/index.js +362 -28
- package/dist/index.mjs +362 -29
- package/package.json +2 -2
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
|
-
|
|
5171
|
-
|
|
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,21 +10070,13 @@ 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
|
-
enableRating = true,
|
|
9782
|
-
ratingLabel = "How would you rate your experience?",
|
|
9783
|
-
onSubmitted
|
|
9784
|
-
} = props;
|
|
9785
10073
|
const sm = useScaleMule();
|
|
10074
|
+
const { client } = sm;
|
|
9786
10075
|
const { submit } = useFeedback({ enabled: false });
|
|
10076
|
+
const [serverConfig, setServerConfig] = react.useState(null);
|
|
10077
|
+
const [configLoaded, setConfigLoaded] = react.useState(false);
|
|
9787
10078
|
const [open, setOpen] = react.useState(false);
|
|
9788
|
-
const [type, setType] = react.useState(
|
|
10079
|
+
const [type, setType] = react.useState("feature_request");
|
|
9789
10080
|
const [title, setTitle] = react.useState("");
|
|
9790
10081
|
const [description, setDescription] = react.useState("");
|
|
9791
10082
|
const [email, setEmail] = react.useState("");
|
|
@@ -9794,10 +10085,39 @@ function FeedbackWidget(props) {
|
|
|
9794
10085
|
const [submitting, setSubmitting] = react.useState(false);
|
|
9795
10086
|
const [errMsg, setErrMsg] = react.useState(null);
|
|
9796
10087
|
const [done, setDone] = react.useState(false);
|
|
9797
|
-
Boolean(sm.user || sm.user === void 0 ? false : false);
|
|
9798
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
|
+
}
|
|
9799
10119
|
function reset() {
|
|
9800
|
-
setType(
|
|
10120
|
+
setType(resolvedDefaultType);
|
|
9801
10121
|
setTitle("");
|
|
9802
10122
|
setDescription("");
|
|
9803
10123
|
setEmail("");
|
|
@@ -9809,6 +10129,10 @@ function FeedbackWidget(props) {
|
|
|
9809
10129
|
async function handleSubmit(e) {
|
|
9810
10130
|
e.preventDefault();
|
|
9811
10131
|
if (!title.trim() || !description.trim()) return;
|
|
10132
|
+
if (anonymousBlocked) {
|
|
10133
|
+
setErrMsg("Please sign in to send feedback for this app");
|
|
10134
|
+
return;
|
|
10135
|
+
}
|
|
9812
10136
|
if (!signedIn && !email.trim()) {
|
|
9813
10137
|
setErrMsg("email is required when not signed in");
|
|
9814
10138
|
return;
|
|
@@ -9825,7 +10149,7 @@ function FeedbackWidget(props) {
|
|
|
9825
10149
|
tags
|
|
9826
10150
|
});
|
|
9827
10151
|
setDone(true);
|
|
9828
|
-
onSubmitted?.(item);
|
|
10152
|
+
props.onSubmitted?.(item);
|
|
9829
10153
|
} catch (err) {
|
|
9830
10154
|
const message = err && typeof err === "object" && "message" in err ? String(err.message) : "Submit failed";
|
|
9831
10155
|
setErrMsg(message);
|
|
@@ -9837,16 +10161,16 @@ function FeedbackWidget(props) {
|
|
|
9837
10161
|
setOpen(false);
|
|
9838
10162
|
setTimeout(reset, 200);
|
|
9839
10163
|
}
|
|
9840
|
-
const dark =
|
|
10164
|
+
const dark = resolvedTheme === "dark" || resolvedTheme === "auto" && typeof window !== "undefined" && window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
|
|
9841
10165
|
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
9842
10166
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
9843
10167
|
"button",
|
|
9844
10168
|
{
|
|
9845
10169
|
type: "button",
|
|
9846
10170
|
onClick: () => setOpen(true),
|
|
9847
|
-
className,
|
|
9848
|
-
style: { ...STYLES.trigger, ...POSITION_STYLES[
|
|
9849
|
-
children: triggerLabel
|
|
10171
|
+
className: props.className,
|
|
10172
|
+
style: { ...STYLES.trigger, ...POSITION_STYLES[resolvedPosition] },
|
|
10173
|
+
children: props.triggerLabel ?? "Feedback"
|
|
9850
10174
|
}
|
|
9851
10175
|
),
|
|
9852
10176
|
open && /* @__PURE__ */ jsxRuntime.jsx("div", { role: "dialog", "aria-modal": "true", style: STYLES.overlay, onClick: handleClose, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
@@ -9881,8 +10205,8 @@ function FeedbackWidget(props) {
|
|
|
9881
10205
|
}
|
|
9882
10206
|
)
|
|
9883
10207
|
] }),
|
|
9884
|
-
enableRating && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
9885
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { style: STYLES.label, children: ratingLabel }),
|
|
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?" }),
|
|
9886
10210
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", gap: 4, marginTop: 4 }, children: [
|
|
9887
10211
|
[1, 2, 3, 4, 5].map((n) => {
|
|
9888
10212
|
const active = (hoverRating ?? rating ?? 0) >= n;
|
|
@@ -9936,7 +10260,7 @@ function FeedbackWidget(props) {
|
|
|
9936
10260
|
style: STYLES.field,
|
|
9937
10261
|
value: type,
|
|
9938
10262
|
onChange: (e) => setType(e.target.value),
|
|
9939
|
-
children:
|
|
10263
|
+
children: resolvedAllowedTypes.map((t) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: t, children: TYPE_LABELS[t] }, t))
|
|
9940
10264
|
}
|
|
9941
10265
|
)
|
|
9942
10266
|
] }),
|
|
@@ -9968,7 +10292,7 @@ function FeedbackWidget(props) {
|
|
|
9968
10292
|
}
|
|
9969
10293
|
)
|
|
9970
10294
|
] }),
|
|
9971
|
-
!signedIn && /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
|
|
10295
|
+
!signedIn && allowAnonymous && /* @__PURE__ */ jsxRuntime.jsxs("label", { children: [
|
|
9972
10296
|
/* @__PURE__ */ jsxRuntime.jsx("div", { style: STYLES.label, children: "Email" }),
|
|
9973
10297
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
9974
10298
|
"input",
|
|
@@ -9982,10 +10306,19 @@ function FeedbackWidget(props) {
|
|
|
9982
10306
|
}
|
|
9983
10307
|
)
|
|
9984
10308
|
] }),
|
|
10309
|
+
anonymousBlocked && /* @__PURE__ */ jsxRuntime.jsx("div", { style: STYLES.note, children: "This app only accepts feedback from signed-in users." }),
|
|
9985
10310
|
errMsg && /* @__PURE__ */ jsxRuntime.jsx("div", { style: STYLES.error, children: errMsg }),
|
|
9986
10311
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { style: STYLES.rowEnd, children: [
|
|
9987
10312
|
/* @__PURE__ */ jsxRuntime.jsx("button", { type: "button", style: STYLES.secondaryBtn, onClick: handleClose, children: "Cancel" }),
|
|
9988
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
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
|
+
)
|
|
9989
10322
|
] })
|
|
9990
10323
|
] })
|
|
9991
10324
|
}
|
|
@@ -10308,6 +10641,7 @@ exports.useBilling = useBilling;
|
|
|
10308
10641
|
exports.useContent = useContent;
|
|
10309
10642
|
exports.useFeatureFlags = useFeatureFlags;
|
|
10310
10643
|
exports.useFeedback = useFeedback;
|
|
10644
|
+
exports.useMedia = useMedia;
|
|
10311
10645
|
exports.useMoney = useMoney;
|
|
10312
10646
|
exports.useMoneyClient = useMoneyClient;
|
|
10313
10647
|
exports.usePushNotifications = usePushNotifications;
|