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