dsh-plugin-subscription-oauth 0.13.0 → 0.15.0

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.
@@ -77,6 +77,13 @@ export interface ProviderUsageRead {
77
77
  plan?: string;
78
78
  renewsAt?: number;
79
79
  windows: readonly UsageWindow[];
80
+ /** Daily check-in state, when the provider states one. */
81
+ checkin?: {
82
+ active: boolean;
83
+ todayCheckedIn: boolean;
84
+ streakDays: number;
85
+ todayCredit: number;
86
+ };
80
87
  /**
81
88
  * Why these figures are not the provider's primary accounting.
82
89
  *
@@ -496,6 +496,7 @@ export class ProviderController {
496
496
  fetchedAt: Date.now(),
497
497
  ...read.plan === undefined ? {} : { plan: read.plan },
498
498
  ...read.renewsAt === undefined ? {} : { renewsAt: read.renewsAt },
499
+ ...read.checkin === undefined ? {} : { checkin: read.checkin },
499
500
  ...read.usageError === undefined ? {} : { substitute: true },
500
501
  };
501
502
  this.usageByAccount.set(account, entry);
@@ -32,6 +32,12 @@ export interface ProviderRoute {
32
32
  removeModel(id: string): Promise<ProviderStatus>;
33
33
  usage(force?: boolean): Promise<ProviderUsageResult>;
34
34
  reload(): Promise<ProviderStatus>;
35
+ /** Claim one account's daily check-in reward, when the route has one. */
36
+ checkin?(account: string): Promise<{
37
+ credit: number;
38
+ streakDays: number;
39
+ already: boolean;
40
+ }>;
35
41
  /** Fast and image variants for one model, when the route's discovery reported them. */
36
42
  modelVariants?(id: string): Promise<ModelVariants | undefined> | ModelVariants | undefined;
37
43
  }
@@ -101,4 +107,10 @@ export declare class SubscriptionRegistry {
101
107
  setAutoOn(enabled: boolean): Promise<{
102
108
  enabled: boolean;
103
109
  }>;
110
+ /** Claim one account's daily check-in reward, when the route supports it. */
111
+ checkin(provider: string, account: string): Promise<{
112
+ credit: number;
113
+ streakDays: number;
114
+ already: boolean;
115
+ }>;
104
116
  }
@@ -161,4 +161,11 @@ export class SubscriptionRegistry {
161
161
  await this.preferences.setAutoOn(enabled);
162
162
  return { enabled };
163
163
  }
164
+ /** Claim one account's daily check-in reward, when the route supports it. */
165
+ async checkin(provider, account) {
166
+ const route = this.route(provider);
167
+ if (route.checkin === undefined)
168
+ throw new ProviderError(`${provider} has no check-in`, BAD_REQUEST);
169
+ return route.checkin(account);
170
+ }
164
171
  }
package/lib/auth/rpc.js CHANGED
@@ -163,6 +163,12 @@ async function runEndpoint(registry, endpoint, payload, balance) {
163
163
  throw new ProviderError('setAutoOn requires an enabled flag', BAD_REQUEST);
164
164
  return registry.setAutoOn(enabled);
165
165
  }
166
+ case 'checkin': {
167
+ const account = stringField(payload, 'account');
168
+ if (account === undefined)
169
+ throw new ProviderError('checkin requires an account', BAD_REQUEST);
170
+ return registry.checkin(requiredProvider(payload, endpoint), account);
171
+ }
166
172
  case 'deepseek-balance': {
167
173
  // The DeepSeek account's balance, read on the node half through the host's own
168
174
  // account service. The browser cannot call that service directly (it sits behind
@@ -101,8 +101,30 @@ export interface WorkBuddyUsageWindow {
101
101
  limit?: number;
102
102
  resetsAt?: number;
103
103
  }
104
+ /** The account's check-in state, from the billing activity endpoint. */
105
+ export interface WorkBuddyCheckin {
106
+ active: boolean;
107
+ todayCheckedIn: boolean;
108
+ streakDays: number;
109
+ todayCredit: number;
110
+ }
111
+ /** Read the account's daily check-in state, without changing anything. */
112
+ export declare function readWorkBuddyCheckin(token: WorkBuddyTokenSet, fetchFn: typeof fetch): Promise<WorkBuddyCheckin>;
113
+ /** Claim today's check-in reward; resolves with the credit granted and the streak. */
114
+ export declare function claimWorkBuddyCheckin(token: WorkBuddyTokenSet, fetchFn: typeof fetch): Promise<{
115
+ credit: number;
116
+ streakDays: number;
117
+ }>;
118
+ /** The account's precise remaining credits, summed across its live packages. */
119
+ export declare function readWorkBuddyCredits(token: WorkBuddyTokenSet, fetchFn: typeof fetch): Promise<{
120
+ remaining: number;
121
+ nearestExpiryMs?: number;
122
+ } | null>;
104
123
  /** Read the account's plan and metered credits for the Settings card. */
105
- export declare function readWorkBuddyUsage(token: WorkBuddyTokenSet, plan: string | undefined, fetchFn: typeof fetch): Promise<{
124
+ export declare function readWorkBuddyUsage(token: WorkBuddyTokenSet, plan: string | undefined, fetchFn: typeof fetch, spend?: {
125
+ spent: number;
126
+ }): Promise<{
106
127
  plan: string;
107
128
  windows: WorkBuddyUsageWindow[];
129
+ checkin?: WorkBuddyCheckin;
108
130
  }>;
@@ -311,6 +311,79 @@ async function workbuddyMeter(token, path, body, fetchFn) {
311
311
  throw new Error(String(env.msg ?? `error ${code}`));
312
312
  return isRecord(env.data) ? env.data : null;
313
313
  }
314
+ /** Read the account's daily check-in state, without changing anything. */
315
+ export async function readWorkBuddyCheckin(token, fetchFn) {
316
+ const data = await workbuddyMeter(token, '/billing/meter/checkin-activity-status', {}, fetchFn);
317
+ if (data === null)
318
+ return { active: false, todayCheckedIn: false, streakDays: 0, todayCredit: 0 };
319
+ const num = (key) => typeof data[key] === 'number' ? data[key] : 0;
320
+ return {
321
+ active: data.active === true,
322
+ todayCheckedIn: data.today_checked_in === true,
323
+ streakDays: num('streak_days'),
324
+ todayCredit: num('today_credit'),
325
+ };
326
+ }
327
+ /** Claim today's check-in reward; resolves with the credit granted and the streak. */
328
+ export async function claimWorkBuddyCheckin(token, fetchFn) {
329
+ const data = await workbuddyMeter(token, '/billing/meter/daily-checkin', {}, fetchFn);
330
+ if (data === null)
331
+ return { credit: 0, streakDays: 0 };
332
+ const num = (key) => typeof data[key] === 'number' ? data[key] : 0;
333
+ return { credit: num('credit'), streakDays: num('streak_days') };
334
+ }
335
+ /** The account's precise remaining credits, summed across its live packages. */
336
+ export async function readWorkBuddyCredits(token, fetchFn) {
337
+ const pad = (n) => n.toString().padStart(2, '0');
338
+ const fmt = (date) => `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
339
+ const now = new Date();
340
+ const data = await workbuddyMeter(token, '/billing/meter/get-user-resource', {
341
+ PageNumber: 1,
342
+ PageSize: 100,
343
+ ProductCode: 'p_tcaca',
344
+ Status: [0, 3],
345
+ PackageEndTimeRangeBegin: fmt(now),
346
+ PackageEndTimeRangeEnd: fmt(new Date(now.getTime() + 3185136e6)),
347
+ }, fetchFn);
348
+ if (data === null)
349
+ return null;
350
+ const response = isRecord(data.Response) ? data.Response : {};
351
+ const inner = isRecord(response.Data) ? response.Data : {};
352
+ const accounts = Array.isArray(inner.Accounts) ? inner.Accounts.filter(isRecord) : [];
353
+ const parseDate = (raw) => {
354
+ if (typeof raw === 'number' && raw > 0xe8d4a51000)
355
+ return raw;
356
+ if (typeof raw === 'string' && raw !== '') {
357
+ const parsed = Date.parse(raw);
358
+ if (!Number.isNaN(parsed))
359
+ return parsed;
360
+ }
361
+ return undefined;
362
+ };
363
+ let remaining = 0;
364
+ let nearestExpiryMs;
365
+ let livePackages = 0;
366
+ for (const account of accounts) {
367
+ const num = (key) => capacity(account[key]);
368
+ const monthly = num('CapacityType') === 4;
369
+ const remain = monthly ? num('CycleCapacityRemain') : num('CapacityRemain');
370
+ const capped = remain < 0 ? 0 : remain;
371
+ const cycleEndMs = parseDate(account.CycleEndTime);
372
+ const expiresAtMs = monthly ? undefined : parseDate(account.ExpiredTime) ?? cycleEndMs;
373
+ if (!monthly && (capped <= 0 || (expiresAtMs !== undefined && expiresAtMs <= Date.now())))
374
+ continue;
375
+ livePackages += 1;
376
+ remaining += capped;
377
+ if (expiresAtMs !== undefined && (nearestExpiryMs === undefined || expiresAtMs < nearestExpiryMs)) {
378
+ nearestExpiryMs = expiresAtMs;
379
+ }
380
+ }
381
+ // No live package reads as "this account doesn't use the package list" — an enterprise
382
+ // account, say — so the caller falls through to its own meter.
383
+ if (livePackages === 0)
384
+ return null;
385
+ return { remaining, ...nearestExpiryMs === undefined ? {} : { nearestExpiryMs } };
386
+ }
314
387
  /** The account's credits from its resource summary: the plan's used against granted. */
315
388
  function usageOfSummary(summary, plan) {
316
389
  const isPaid = summary?.IsPaidUser === true;
@@ -349,7 +422,34 @@ function enterpriseUsageOf(data) {
349
422
  return window;
350
423
  }
351
424
  /** Read the account's plan and metered credits for the Settings card. */
352
- export async function readWorkBuddyUsage(token, plan, fetchFn) {
425
+ export async function readWorkBuddyUsage(token, plan, fetchFn, spend) {
426
+ // The precise figures: the live packages' remaining and total size, and the account's
427
+ // accumulated spend from the real per-turn credits. Spent-against-limit reads as the
428
+ // true billed share, not an estimate from tokens.
429
+ try {
430
+ const credits = await readWorkBuddyCredits(token, fetchFn);
431
+ if (credits !== null) {
432
+ const window = { label: 'Credits', used: spend?.spent ?? 0 };
433
+ if (credits.remaining > 0 || (spend?.spent ?? 0) > 0) {
434
+ const limit = credits.remaining + (spend?.spent ?? 0);
435
+ window.limit = limit;
436
+ window.usedPercent = limit > 0 ? (100 * (spend?.spent ?? 0)) / limit : 0;
437
+ }
438
+ if (credits.nearestExpiryMs !== undefined)
439
+ window.resetsAt = credits.nearestExpiryMs;
440
+ const out = { plan: plan ?? 'Free', windows: [window] };
441
+ try {
442
+ out.checkin = await readWorkBuddyCheckin(token, fetchFn);
443
+ }
444
+ catch {
445
+ // A check-in read failure never hides the credits.
446
+ }
447
+ return out;
448
+ }
449
+ }
450
+ catch {
451
+ // Fall through to the summary when the package list can't be read.
452
+ }
353
453
  const summary = await workbuddyMeter(token, '/billing/meter/get-user-resource-summary', {}, fetchFn);
354
454
  const out = usageOfSummary(summary, plan);
355
455
  // An enterprise account's summary has no packages: its credits are the enterprise meter's.
@@ -357,12 +457,20 @@ export async function readWorkBuddyUsage(token, plan, fetchFn) {
357
457
  try {
358
458
  const enterprise = enterpriseUsageOf(await workbuddyMeter(token, '/billing/meter/get-enterprise-user-usage', {}, fetchFn));
359
459
  if (enterprise !== null) {
360
- return { plan: plan ?? 'Enterprise', windows: [enterprise] };
460
+ out.windows = [enterprise];
461
+ out.plan = plan ?? 'Enterprise';
361
462
  }
362
463
  }
363
464
  catch {
364
465
  // The personal-account refusal (10001) leaves the card as the summary said.
365
466
  }
366
467
  }
468
+ // Fold the check-in state in beside the credits: the card shows today's status and streak.
469
+ try {
470
+ out.checkin = await readWorkBuddyCheckin(token, fetchFn);
471
+ }
472
+ catch {
473
+ // A check-in read failure never hides the credits.
474
+ }
367
475
  return out;
368
476
  }
@@ -313,12 +313,13 @@ function windowAmount(window) {
313
313
  return `${String(window.used)}/${String(window.limit)}`;
314
314
  return '—';
315
315
  }
316
- /** One account's metered windows, rendered under its account row. */
316
+ /** One account's metered windows and check-in, rendered under its account row. */
317
317
  function AccountUsage(props) {
318
- const { entry, t } = props;
318
+ const { entry, t, busyAction, onCheckin } = props;
319
319
  if (entry === undefined)
320
320
  return null;
321
- return (_jsxs("div", { style: styles.usageInline, children: [entry.windows.map((window) => (_jsxs("div", { style: styles.usageRow, children: [_jsxs("div", { style: styles.usageLine, children: [_jsxs("span", { style: styles.modelName, children: [windowLabel(t, window), ' · ', windowAmount(window)] }), _jsx("span", { style: styles.modelMeta, children: window.resetsAt === undefined ? '' : formatReset(window.resetsAt, t) })] }), window.usedPercent !== undefined && (_jsx("div", { className: "dsh-subscription-meter", children: _jsx("span", { style: { width: `${String(Math.min(100, Math.max(0, window.usedPercent)))}%` }, "data-exhausted": window.usedPercent >= 90 ? 'true' : 'false' }) }))] }, `${entry.account}-${window.label}`))), entry.windows.length === 0 && (_jsx("p", { style: styles.statusLine, children: t('usageNone') }))] }));
321
+ const checkin = entry.checkin;
322
+ return (_jsxs("div", { style: styles.usageInline, children: [checkin?.active === true && (_jsxs("div", { style: styles.usageLine, children: [_jsxs("span", { style: styles.modelName, children: [t('checkinTitle'), ' · ', checkin.todayCheckedIn ? t('checkinDone', { days: checkin.streakDays }) : t('checkinPending')] }), !checkin.todayCheckedIn && onCheckin !== undefined && (_jsx("button", { type: "button", className: "dsh-subscription-quiet", style: styles.pushRight, disabled: busyAction, onClick: onCheckin, children: t('checkinClaim') }))] })), entry.windows.map((window) => (_jsxs("div", { style: styles.usageRow, children: [_jsxs("div", { style: styles.usageLine, children: [_jsxs("span", { style: styles.modelName, children: [windowLabel(t, window), ' · ', windowAmount(window)] }), _jsx("span", { style: styles.modelMeta, children: window.resetsAt === undefined ? '' : formatReset(window.resetsAt, t) })] }), window.usedPercent !== undefined && (_jsx("div", { className: "dsh-subscription-meter", children: _jsx("span", { style: { width: `${String(Math.min(100, Math.max(0, window.usedPercent)))}%` }, "data-exhausted": window.usedPercent >= 90 ? 'true' : 'false' }) }))] }, `${entry.account}-${window.label}`))), entry.windows.length === 0 && (_jsx("p", { style: styles.statusLine, children: t('usageNone') }))] }));
322
323
  }
323
324
  /** The plan-and-usage block for the whole route. */
324
325
  function UsageBlock(props) {
@@ -523,7 +524,7 @@ function ProviderCard(props) {
523
524
  const usageEntry = (status.usage ?? []).find((entry) => entry.account === account.account);
524
525
  return (_jsxs("div", { style: styles.accountRow, children: [_jsxs("div", { style: styles.accountHeader, children: [_jsx("button", { type: "button", style: styles.starButton, title: account.isDefault ? t('defaultBadge') : t('makeDefault'), "aria-label": account.isDefault ? t('defaultBadge') : t('makeDefault'), disabled: busyAction || account.isDefault, onClick: () => { props.onSetDefault(account.account); }, children: account.isDefault ? '★' : '☆' }), _jsx("span", { style: styles.accountName, children: account.label }), account.isDefault && _jsx("span", { style: styles.badgeDefault, children: t('defaultBadge') }), account.needsLogin === true && (_jsx("span", { style: styles.badgeWarn, title: t('accountNeedsLoginHint'), children: t('accountNeedsLogin') })), usageEntry?.plan !== undefined && (_jsx("span", { style: styles.badge, children: t('usagePlan', { plan: usageEntry.plan }) })), _jsx("span", { style: styles.statusLine, children: account.expired
525
526
  ? t('accountExpired')
526
- : t('accountExpires', { time: formatTime(account.expiresAt) }) }), _jsx("button", { type: "button", className: "dsh-subscription-quiet", style: styles.pushRight, disabled: busyAction, onClick: () => { props.onLogout(account.account); }, children: t('logout') })] }), _jsx(AccountUsage, { entry: usageEntry, t: t })] }, account.account));
527
+ : t('accountExpires', { time: formatTime(account.expiresAt) }) }), _jsx("button", { type: "button", className: "dsh-subscription-quiet", style: styles.pushRight, disabled: busyAction, onClick: () => { props.onLogout(account.account); }, children: t('logout') })] }), _jsx(AccountUsage, { entry: usageEntry, t: t, busyAction: busyAction, ...props.onCheckin === undefined ? {} : { onCheckin: () => { props.onCheckin?.(account.account); } } })] }, account.account));
527
528
  }), status.loggedIn && (_jsxs("div", { style: styles.catalog, children: [_jsxs("div", { style: styles.catalogHeader, children: [_jsxs("button", { type: "button", className: "dsh-subscription-disclosure", style: styles.disclosure, "aria-expanded": modelsOpen, "aria-controls": `dsh-subscription-models-${status.provider}`, onClick: toggleModelsOpen, children: [_jsx("span", { style: styles.chevron, "aria-hidden": "true", children: modelsOpen ? '▾' : '▸' }), _jsx("span", { style: styles.catalogTitle, children: t('modelsTitle') })] }), _jsxs("span", { style: styles.catalogMeta, children: [t('modelsCount', { count: models.length }), ' · ', liveCatalog ? t('modelsLive') : t('modelsBuiltIn'), hiddenCount === 0 ? '' : ` ${t('modelsHiddenCount', { count: hiddenCount })}`] }), _jsx("button", { type: "button", className: "dsh-subscription-quiet", style: styles.pushRight, disabled: busyAction, onClick: props.onRefreshModels, children: busyAction ? t('refreshing') : t('refresh') }), _jsx("button", { type: "button", className: "dsh-subscription-quiet", disabled: busyAction || !connectable, title: t('modelsAddHint'), onClick: () => { setAdding((open) => !open); }, children: t('modelsAdd') })] }), adding && (_jsxs("form", { style: styles.addRow, onSubmit: (event) => {
528
529
  event.preventDefault();
529
530
  const id = draft.trim();
@@ -616,6 +617,35 @@ export function SubscriptionsSection(props) {
616
617
  setBusyAction(false);
617
618
  }
618
619
  }, [call, refresh, t]);
620
+ /** Claim one account's daily check-in reward, then refresh the figures. */
621
+ const checkin = useCallback(async (provider, account) => {
622
+ if (call === undefined)
623
+ return;
624
+ setBusyAction(true);
625
+ try {
626
+ const result = await call('checkin', { provider, account });
627
+ if (!mounted.current)
628
+ return;
629
+ setMessages((previous) => ({
630
+ ...previous,
631
+ [provider]: {
632
+ tone: 'success',
633
+ text: result.already
634
+ ? t('checkinAlready', { days: result.streakDays })
635
+ : t('checkinClaimed', { credit: result.credit, days: result.streakDays }),
636
+ },
637
+ }));
638
+ await refresh();
639
+ }
640
+ catch (caught) {
641
+ if (mounted.current)
642
+ setError(t('actionFailed', { message: messageOf(caught) }));
643
+ }
644
+ finally {
645
+ if (mounted.current)
646
+ setBusyAction(false);
647
+ }
648
+ }, [call, refresh, t]);
619
649
  const stopPolling = useCallback(() => {
620
650
  if (timer.current !== undefined) {
621
651
  clearInterval(timer.current);
@@ -859,5 +889,5 @@ export function SubscriptionsSection(props) {
859
889
  }, [act, call]);
860
890
  if (call === undefined)
861
891
  return _jsx("p", { style: styles.intro, children: t('unavailable') });
862
- return (_jsxs("div", { className: "dsh-subscriptions-settings", style: styles.section, children: [_jsx("style", { children: subscriptionsCss }), _jsxs("div", { style: styles.sectionHeader, children: [_jsx("h2", { style: styles.heading, children: t('nav') }), _jsx("p", { style: styles.intro, children: t('intro') })] }), error !== undefined && _jsx("p", { className: "dsh-subscription-error", children: error }), status === undefined && _jsx("p", { style: styles.statusLine, children: t('loading') }), orderedProviders.map((provider) => (_jsx(ProviderCard, { status: provider, t: t, busyAction: busyAction, ...messages[provider.provider] === undefined ? {} : { message: messages[provider.provider] }, onLogin: () => { login(provider.provider); }, onCancel: () => { cancel(provider.provider); }, onLogout: (account) => { logout(provider.provider, account); }, onSetDefault: (account) => { setDefault(provider.provider, account); }, onRefreshToken: (account) => { refreshToken(provider.provider, account); }, onRefreshModels: () => { refreshModels(provider.provider); }, onSetModelVisible: (id, visible) => { setModelVisible(provider.provider, id, visible); }, onSetModelOrder: (ids) => { setModelOrder(provider.provider, ids); }, onAddModel: (id) => { addModel(provider.provider, id); }, onRemoveModel: (id) => { removeModel(provider.provider, id); }, onRefreshUsage: () => { refreshUsage(provider.provider); }, onReload: () => { reload(provider.provider); }, ...provider.provider !== 'auto' ? {} : { autoOn, onSetAutoOn: (enabled) => { void setAuto(enabled); } } }, provider.provider)))] }));
892
+ return (_jsxs("div", { className: "dsh-subscriptions-settings", style: styles.section, children: [_jsx("style", { children: subscriptionsCss }), _jsxs("div", { style: styles.sectionHeader, children: [_jsx("h2", { style: styles.heading, children: t('nav') }), _jsx("p", { style: styles.intro, children: t('intro') })] }), error !== undefined && _jsx("p", { className: "dsh-subscription-error", children: error }), status === undefined && _jsx("p", { style: styles.statusLine, children: t('loading') }), orderedProviders.map((provider) => (_jsx(ProviderCard, { status: provider, t: t, busyAction: busyAction, ...messages[provider.provider] === undefined ? {} : { message: messages[provider.provider] }, onLogin: () => { login(provider.provider); }, onCancel: () => { cancel(provider.provider); }, onLogout: (account) => { logout(provider.provider, account); }, onSetDefault: (account) => { setDefault(provider.provider, account); }, onRefreshToken: (account) => { refreshToken(provider.provider, account); }, onRefreshModels: () => { refreshModels(provider.provider); }, onSetModelVisible: (id, visible) => { setModelVisible(provider.provider, id, visible); }, onSetModelOrder: (ids) => { setModelOrder(provider.provider, ids); }, onAddModel: (id) => { addModel(provider.provider, id); }, onRemoveModel: (id) => { removeModel(provider.provider, id); }, onRefreshUsage: () => { refreshUsage(provider.provider); }, onReload: () => { reload(provider.provider); }, ...provider.provider !== 'auto' ? {} : { autoOn, onSetAutoOn: (enabled) => { void setAuto(enabled); } }, ...provider.provider !== 'workbuddy' ? {} : { onCheckin: (account) => { void checkin(provider.provider, account); } } }, provider.provider)))] }));
863
893
  }
@@ -61,6 +61,12 @@ export declare const en: {
61
61
  readonly modelsAutoChainOff: "Take out of Auto";
62
62
  readonly modelsAutoChainEmpty: "No model is enabled, so Auto has nothing to try.";
63
63
  readonly autoSwitchLabel: "Offer Auto in the model picker";
64
+ readonly checkinTitle: "Daily check-in";
65
+ readonly checkinPending: "not checked in today";
66
+ readonly checkinDone: "checked in · {days}-day streak";
67
+ readonly checkinClaim: "Check in";
68
+ readonly checkinClaimed: "Checked in, +{credit} credits (streak {days}d)";
69
+ readonly checkinAlready: "Already checked in today (streak {days}d)";
64
70
  readonly modelsPinned: "pinned";
65
71
  readonly usageTitle: "Plan and usage";
66
72
  readonly usagePlan: "Plan: {plan}";
@@ -159,6 +165,12 @@ export declare const zh: {
159
165
  modelsAutoChainOff: string;
160
166
  modelsAutoChainEmpty: string;
161
167
  autoSwitchLabel: string;
168
+ checkinTitle: string;
169
+ checkinPending: string;
170
+ checkinDone: string;
171
+ checkinClaim: string;
172
+ checkinClaimed: string;
173
+ checkinAlready: string;
162
174
  modelsPinned: string;
163
175
  usageTitle: string;
164
176
  usagePlan: string;
@@ -61,6 +61,12 @@ export const en = {
61
61
  modelsAutoChainOff: 'Take out of Auto',
62
62
  modelsAutoChainEmpty: 'No model is enabled, so Auto has nothing to try.',
63
63
  autoSwitchLabel: 'Offer Auto in the model picker',
64
+ checkinTitle: 'Daily check-in',
65
+ checkinPending: 'not checked in today',
66
+ checkinDone: 'checked in · {days}-day streak',
67
+ checkinClaim: 'Check in',
68
+ checkinClaimed: 'Checked in, +{credit} credits (streak {days}d)',
69
+ checkinAlready: 'Already checked in today (streak {days}d)',
64
70
  modelsPinned: 'pinned',
65
71
  usageTitle: 'Plan and usage',
66
72
  usagePlan: 'Plan: {plan}',
@@ -157,6 +163,12 @@ export const zh = {
157
163
  modelsAutoChainOff: '从自动中移除',
158
164
  modelsAutoChainEmpty: '没有启用任何模型,自动没有可尝试的模型。',
159
165
  autoSwitchLabel: '在模型选择器中提供 Auto',
166
+ checkinTitle: '每日签到',
167
+ checkinPending: '今日未签到',
168
+ checkinDone: '已签到 · 连签 {days} 天',
169
+ checkinClaim: '签到',
170
+ checkinClaimed: '签到成功,+{credit} 积分(连签 {days} 天)',
171
+ checkinAlready: '今日已签到(连签 {days} 天)',
160
172
  modelsPinned: '固定',
161
173
  usageTitle: '套餐与额度',
162
174
  usagePlan: '套餐:{plan}',
package/lib/client.js CHANGED
@@ -92,6 +92,12 @@ const en = {
92
92
  modelsAutoChainOff: "Take out of Auto",
93
93
  modelsAutoChainEmpty: "No model is enabled, so Auto has nothing to try.",
94
94
  autoSwitchLabel: "Offer Auto in the model picker",
95
+ checkinTitle: "Daily check-in",
96
+ checkinPending: "not checked in today",
97
+ checkinDone: "checked in · {days}-day streak",
98
+ checkinClaim: "Check in",
99
+ checkinClaimed: "Checked in, +{credit} credits (streak {days}d)",
100
+ checkinAlready: "Already checked in today (streak {days}d)",
95
101
  modelsPinned: "pinned",
96
102
  usageTitle: "Plan and usage",
97
103
  usagePlan: "Plan: {plan}",
@@ -188,6 +194,12 @@ const zh = {
188
194
  modelsAutoChainOff: "从自动中移除",
189
195
  modelsAutoChainEmpty: "没有启用任何模型,自动没有可尝试的模型。",
190
196
  autoSwitchLabel: "在模型选择器中提供 Auto",
197
+ checkinTitle: "每日签到",
198
+ checkinPending: "今日未签到",
199
+ checkinDone: "已签到 · 连签 {days} 天",
200
+ checkinClaim: "签到",
201
+ checkinClaimed: "签到成功,+{credit} 积分(连签 {days} 天)",
202
+ checkinAlready: "今日已签到(连签 {days} 天)",
191
203
  modelsPinned: "固定",
192
204
  usageTitle: "套餐与额度",
193
205
  usagePlan: "套餐:{plan}",
@@ -1327,38 +1339,60 @@ function windowAmount$1(window$1) {
1327
1339
  if (window$1.used !== void 0 && window$1.limit !== void 0) return `${String(window$1.used)}/${String(window$1.limit)}`;
1328
1340
  return "—";
1329
1341
  }
1330
- /** One account's metered windows, rendered under its account row. */
1342
+ /** One account's metered windows and check-in, rendered under its account row. */
1331
1343
  function AccountUsage(props) {
1332
- const { entry, t } = props;
1344
+ const { entry, t, busyAction, onCheckin } = props;
1333
1345
  if (entry === void 0) return null;
1346
+ const checkin = entry.checkin;
1334
1347
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1335
1348
  style: styles.usageInline,
1336
- children: [entry.windows.map((window$1) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1337
- style: styles.usageRow,
1338
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1349
+ children: [
1350
+ checkin?.active === true && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1339
1351
  style: styles.usageLine,
1340
1352
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1341
1353
  style: styles.modelName,
1342
1354
  children: [
1343
- windowLabel(t, window$1),
1355
+ t("checkinTitle"),
1344
1356
  " · ",
1345
- windowAmount$1(window$1)
1357
+ checkin.todayCheckedIn ? t("checkinDone", { days: checkin.streakDays }) : t("checkinPending")
1346
1358
  ]
1347
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1348
- style: styles.modelMeta,
1349
- children: window$1.resetsAt === void 0 ? "" : formatReset$1(window$1.resetsAt, t)
1359
+ }), !checkin.todayCheckedIn && onCheckin !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1360
+ type: "button",
1361
+ className: "dsh-subscription-quiet",
1362
+ style: styles.pushRight,
1363
+ disabled: busyAction,
1364
+ onClick: onCheckin,
1365
+ children: t("checkinClaim")
1350
1366
  })]
1351
- }), window$1.usedPercent !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1352
- className: "dsh-subscription-meter",
1353
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1354
- style: { width: `${String(Math.min(100, Math.max(0, window$1.usedPercent)))}%` },
1355
- "data-exhausted": window$1.usedPercent >= 90 ? "true" : "false"
1356
- })
1357
- })]
1358
- }, `${entry.account}-${window$1.label}`)), entry.windows.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1359
- style: styles.statusLine,
1360
- children: t("usageNone")
1361
- })]
1367
+ }),
1368
+ entry.windows.map((window$1) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1369
+ style: styles.usageRow,
1370
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1371
+ style: styles.usageLine,
1372
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1373
+ style: styles.modelName,
1374
+ children: [
1375
+ windowLabel(t, window$1),
1376
+ " · ",
1377
+ windowAmount$1(window$1)
1378
+ ]
1379
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1380
+ style: styles.modelMeta,
1381
+ children: window$1.resetsAt === void 0 ? "" : formatReset$1(window$1.resetsAt, t)
1382
+ })]
1383
+ }), window$1.usedPercent !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1384
+ className: "dsh-subscription-meter",
1385
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1386
+ style: { width: `${String(Math.min(100, Math.max(0, window$1.usedPercent)))}%` },
1387
+ "data-exhausted": window$1.usedPercent >= 90 ? "true" : "false"
1388
+ })
1389
+ })]
1390
+ }, `${entry.account}-${window$1.label}`)),
1391
+ entry.windows.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1392
+ style: styles.statusLine,
1393
+ children: t("usageNone")
1394
+ })
1395
+ ]
1362
1396
  });
1363
1397
  }
1364
1398
  /** The pending-login block: a device code, or a link to the sign-in page. */
@@ -1703,7 +1737,11 @@ function ProviderCard(props) {
1703
1737
  ]
1704
1738
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AccountUsage, {
1705
1739
  entry: usageEntry,
1706
- t
1740
+ t,
1741
+ busyAction,
1742
+ ...props.onCheckin === void 0 ? {} : { onCheckin: () => {
1743
+ props.onCheckin?.(account.account);
1744
+ } }
1707
1745
  })]
1708
1746
  }, account.account);
1709
1747
  }),
@@ -2112,6 +2150,37 @@ function SubscriptionsSection(props) {
2112
2150
  refresh,
2113
2151
  t
2114
2152
  ]);
2153
+ /** Claim one account's daily check-in reward, then refresh the figures. */
2154
+ const checkin = (0, react.useCallback)(async (provider, account) => {
2155
+ if (call === void 0) return;
2156
+ setBusyAction(true);
2157
+ try {
2158
+ const result = await call("checkin", {
2159
+ provider,
2160
+ account
2161
+ });
2162
+ if (!mounted.current) return;
2163
+ setMessages((previous) => ({
2164
+ ...previous,
2165
+ [provider]: {
2166
+ tone: "success",
2167
+ text: result.already ? t("checkinAlready", { days: result.streakDays }) : t("checkinClaimed", {
2168
+ credit: result.credit,
2169
+ days: result.streakDays
2170
+ })
2171
+ }
2172
+ }));
2173
+ await refresh();
2174
+ } catch (caught) {
2175
+ if (mounted.current) setError(t("actionFailed", { message: messageOf(caught) }));
2176
+ } finally {
2177
+ if (mounted.current) setBusyAction(false);
2178
+ }
2179
+ }, [
2180
+ call,
2181
+ refresh,
2182
+ t
2183
+ ]);
2115
2184
  const stopPolling = (0, react.useCallback)(() => {
2116
2185
  if (timer.current !== void 0) {
2117
2186
  clearInterval(timer.current);
@@ -2431,7 +2500,10 @@ function SubscriptionsSection(props) {
2431
2500
  onSetAutoOn: (enabled) => {
2432
2501
  setAuto(enabled);
2433
2502
  }
2434
- }
2503
+ },
2504
+ ...provider.provider !== "workbuddy" ? {} : { onCheckin: (account) => {
2505
+ checkin(provider.provider, account);
2506
+ } }
2435
2507
  }, provider.provider))
2436
2508
  ]
2437
2509
  });