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