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