@nauticana/sail 1.1.39 → 1.1.42

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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  A shared Angular component library for building CRUD-based admin frontends. Provides table management, form handling, navigation, authentication, two-factor authentication, and trusted device management — all driven by metadata from a [keel](https://github.com/nauticana/keel) Go backend.
4
4
 
5
- > **Compatibility:** sail and keel are versioned in lock-step. The current line is **sail v1.1.x ↔ keel v1.2.x**; sail v1.1.39 adopts keel v1.2.115 (usage captions, provider-managed subscriptions), sail v1.1.37 adopts keel v1.2.114 (scopes validated against a constant list), sail v1.1.36 adopts keel v1.2.113 (API key expiry, caller-aware scope policy), sail v1.1.35 adopts keel v1.2.112 (minor-unit money, the `currency` catalog, OAuth grant routes), sail v1.1.33 adopts keel v1.2.106 (`prompt` on the login page, new-device step-up, sign-in sessions), sail v1.1.32 adopts keel v1.2.101 (the OAuth connect callback ticket), sail v1.1.31 adopts keel v1.2.100 (tenant single sign-on, DNS and HTTP-file domain verification), sail v1.1.30 adopts keel v1.2.99 (password routes; a password change signs out), sail v1.1.29 adopts keel v1.2.94 (signup: confirmation signs in, partner setup, sign-in hand-off code), sail v1.1.28 adopts keel v1.2.93 (absolute `return` for the OAuth hand-off), sail v1.1.18 adopts keel v1.2.58 (refresh-token rotation, `/public/logout`, setup intents, the realtime hub), the agency components in sail v1.1.14 require keel v1.2.41 (sail v1.1.9 needs keel v1.2.16 for the OAuth-connect layer). Earlier lines: **sail v0.5.x ↔ keel v0.5.x**, **sail v0.6.x / v0.7.x ↔ keel v0.7.x**, **sail v0.8.x ↔ keel v0.8.x**, **sail v0.9.x ↔ keel v0.9.x**. Newer sail releases extend the contract — older keel servers reject unknown endpoints with HTTP 404 / 400. The v0.8.x line additionally ships the `table_action` framework (per-table custom buttons surfaced in `TableList` / `TableSearch` / `TableEdit` / `TableDetail`); see the [Migrating to v0.7.0 §5 — TableAction](#migrating-to-v070--payout-user-payment-methods-table-actions) section for the seed shape (basis `table_action` + `authorization_object` + `authorization_object_action` rows) and the [keel/README Table Actions](https://github.com/nauticana/keel#table-actions) section for backend wiring via `handler.WrapTableAction`.
5
+ > **Compatibility:** sail and keel are versioned in lock-step. The current line is **sail v1.1.x ↔ keel v1.2.x**; sail v1.1.42 adopts keel v1.2.118 (RFC 3339 timestamps, `X-Time-Zone`), sail v1.1.41 adopts keel v1.2.117 (scheduled plan changes), sail v1.1.39 and v1.1.40 adopt keel v1.2.115 (usage captions, provider-managed subscriptions, the connect disconnect route), sail v1.1.37 adopts keel v1.2.114 (scopes validated against a constant list), sail v1.1.36 adopts keel v1.2.113 (API key expiry, caller-aware scope policy), sail v1.1.35 adopts keel v1.2.112 (minor-unit money, the `currency` catalog, OAuth grant routes), sail v1.1.33 adopts keel v1.2.106 (`prompt` on the login page, new-device step-up, sign-in sessions), sail v1.1.32 adopts keel v1.2.101 (the OAuth connect callback ticket), sail v1.1.31 adopts keel v1.2.100 (tenant single sign-on, DNS and HTTP-file domain verification), sail v1.1.30 adopts keel v1.2.99 (password routes; a password change signs out), sail v1.1.29 adopts keel v1.2.94 (signup: confirmation signs in, partner setup, sign-in hand-off code), sail v1.1.28 adopts keel v1.2.93 (absolute `return` for the OAuth hand-off), sail v1.1.18 adopts keel v1.2.58 (refresh-token rotation, `/public/logout`, setup intents, the realtime hub), the agency components in sail v1.1.14 require keel v1.2.41 (sail v1.1.9 needs keel v1.2.16 for the OAuth-connect layer). Earlier lines: **sail v0.5.x ↔ keel v0.5.x**, **sail v0.6.x / v0.7.x ↔ keel v0.7.x**, **sail v0.8.x ↔ keel v0.8.x**, **sail v0.9.x ↔ keel v0.9.x**. Newer sail releases extend the contract — older keel servers reject unknown endpoints with HTTP 404 / 400. The v0.8.x line additionally ships the `table_action` framework (per-table custom buttons surfaced in `TableList` / `TableSearch` / `TableEdit` / `TableDetail`); see the [Migrating to v0.7.0 §5 — TableAction](#migrating-to-v070--payout-user-payment-methods-table-actions) section for the seed shape (basis `table_action` + `authorization_object` + `authorization_object_action` rows) and the [keel/README Table Actions](https://github.com/nauticana/keel#table-actions) section for backend wiring via `handler.WrapTableAction`.
6
6
 
7
7
  **Recent additions:** `BaseAuthService.acceptToken(jwt)` — adopt an externally-minted JWT (registration / SSO-handoff flows) and run the full post-login sequence (store under the canonical `jwt` key, load appdata, init routes); apps must use this instead of writing `localStorage` directly. `BaseRestService.analytic<T>(endpoint, params?)` — GET a keel `analytic/<endpoint>` report and return its rows. `passwordPolicyValidator` + `BaseAuthService.ensurePasswordPolicy()` — validate passwords against keel's policy (via `SailGuiConfig.passwordPolicyUrl`); see Configuration reference.
8
8
 
@@ -19,7 +19,7 @@ A shared Angular component library for building CRUD-based admin frontends. Prov
19
19
  | **Security** | `TwoFactorSetupComponent`, `TwoFactorVerifyComponent`, `TrustedDevicesComponent`, `SessionsComponent`, `ApiKeysComponent`, `ConnectedAppsComponent`, `AccountDeletionComponent` |
20
20
  | **Account** | `MyAccountComponent` (self-service hub), `ProfileEditorComponent` (name/locale immediate; email/phone verify-before-apply) |
21
21
  | **Auth** | `ConsentGateComponent`, `OtpInputComponent`, `SocialLoginComponent`, `SsoLoginComponent`, `SsoReturnComponent` |
22
- | **Billing** | `PlanSelectorComponent`, `PriceSelectorComponent`, `CheckoutButtonComponent`, `PaymentMethodsComponent`, `PortalButtonComponent`, `UsageMeterComponent`, `StatusChipComponent`, `TrialBannerComponent`, `SeatSelectorComponent`, `DunningBannerComponent` |
22
+ | **Billing** | `PlanSelectorComponent`, `PriceSelectorComponent`, `CheckoutButtonComponent`, `PaymentMethodsComponent`, `PortalButtonComponent`, `UsageMeterComponent`, `StatusChipComponent`, `TrialBannerComponent`, `ScheduledChangeBannerComponent`, `SeatSelectorComponent`, `DunningBannerComponent` |
23
23
  | **Dashboard** | `DashboardShellComponent`, `DataCardComponent`, `StatTileComponent`, `LockedOverlayComponent`, `ActionCenterComponent`, `EntitySelectorComponent`, `VerificationFlowComponent` |
24
24
  | **Payout** | `PayoutProviderOnboardingComponent`, `PayoutBankInfoFormComponent` |
25
25
  | **Agency** | `AgencyClientsComponent`, `AgencyStatusBannerComponent`, `AgencyAcceptComponent`, `AgencyEarningsSummaryComponent`, `CommissionLedgerComponent`, `AgencyPayoutHistoryComponent`, `AgencyPayoutProfileComponent` |
@@ -29,7 +29,7 @@ A shared Angular component library for building CRUD-based admin frontends. Prov
29
29
  | **Services** | `BaseAuthService` (OTP / social / push / deleteAccount / logoutEverywhere / profile: `getProfile`, `updateProfile`, `request`+`confirmEmailChange`, `request`+`confirmPhoneChange`), `BillingService`, `AgencyService`, `BackendService`, `PayoutService`, `UserPaymentMethodService`, `PartnerDocumentService`, `NotificationInboxService`, `loadScript()`, `authInterceptor`, `apiResponseInterceptor` |
30
30
  | **Abstracts** | `BaseTable`, `BaseForm`, `BaseView`, `AbstractEditableView`, `BaseAsync`, `BaseRestService` |
31
31
  | **Config** | `SAIL_GUI_CONFIG`, `SailGuiConfig`, `configureRestUrls()` |
32
- | **Models** | `ApplicationData`, `TableDefinition`, `SiudAction`, `ApplicationMenu`, `ConstantValue`, `UserAccount`, `RestReport`, `ReportParam`, `TrustedDevice`, `PublicPlan`, `PlanPrice`, `PaymentMethod`, `Subscription`, `Invoice`, `CheckoutRequest`/`CheckoutResponse`, `PortalResponse`, `UsageMeter`, `ChargeResult`, `UserPaymentMethod`, `TableAction`, `TableActionParameter`, `ReusableAccount`, `PayoutOnboardingSession`, `BankInfoFormValue`, `Currency`, `Country`, `OtpRequest`/`OtpResponse`, `SignupConsent`, `ConsentState`, `ConsentOption`, `SocialProvider`, `PushPlatform`, `InboxMessage`/`InboxPage`, `PartnerDocument`/`DocumentUploadRequest`, 2FA types, etc. |
32
+ | **Models** | `ApplicationData`, `TableDefinition`, `SiudAction`, `ApplicationMenu`, `ConstantValue`, `UserAccount`, `RestReport`, `ReportParam`, `TrustedDevice`, `PublicPlan`, `PlanPrice`, `PaymentMethod`, `Subscription`, `PlanChangeResult`, `Invoice`, `CheckoutRequest`/`CheckoutResponse`, `PortalResponse`, `UsageMeter`, `ChargeResult`, `UserPaymentMethod`, `TableAction`, `TableActionParameter`, `ReusableAccount`, `PayoutOnboardingSession`, `BankInfoFormValue`, `Currency`, `Country`, `OtpRequest`/`OtpResponse`, `SignupConsent`, `ConsentState`, `ConsentOption`, `SocialProvider`, `PushPlatform`, `InboxMessage`/`InboxPage`, `PartnerDocument`/`DocumentUploadRequest`, 2FA types, etc. |
33
33
  | **Decorators** | `@IsSailString()`, `@IsSailNumeric()` (class-validator based) |
34
34
  | **Utils** | `titleCase()`, `fromPriceLabel()`, `formatMinor()`, `CurrencyService`, `sailMoney` pipe |
35
35
 
@@ -153,6 +153,8 @@ bootstrapApplication(App, {
153
153
 
154
154
  > **Auth-loop breaker.** `authInterceptor` carries a built-in circuit breaker: after 5 `401`s on token-bearing keel API calls within 10s it opens for 30s and refuses those requests **locally** (never sent), so a stale/rejected session can't drive the app to hammer the API and the edge/CDN. Only `401` counts — a `403` is a legitimate permission denial, not a session failure. A later success **or accepting a new token** (login) closes it, so a freshly authenticated session is never blocked. No wiring needed beyond registering `authInterceptor`.
155
155
 
156
+ > **Time zone.** `authInterceptor` also sends `X-Time-Zone` (the browser's IANA zone, read once per page load) on every keel `/api/` and `/public/` request, signed in or not, so keel renders emails, notifications and exports in the user's zone (keel v1.2.118). Timestamps in responses are RFC 3339 UTC; render them with the `date` pipe, without a zone argument.
157
+
156
158
  > **Phone-number change (opt-in).** `ProfileEditorComponent` and `MyAccountComponent` gate phone change behind a `phoneChangeEnabled` input (default `false`, since it needs an SMS provider — keel returns 503 without `Notify`). Enable it once your backend has SMS wired: `<sail-my-account [phoneChangeEnabled]="true" />` or `<sail-profile-editor [phoneChangeEnabled]="true" />`.
157
159
 
158
160
  ### 5. Create the root component
@@ -470,7 +472,7 @@ export class PricingPage {
470
472
  | `createCheckout(req)` | `POST /api/billing/checkout` | `Observable<CheckoutResponse>` |
471
473
  | `getSubscription()` | `GET /api/billing/subscription` | `Observable<Subscription>` |
472
474
  | `cancelSubscription()` | `POST /api/billing/subscription/cancel` | `Observable<void>` |
473
- | `changePlan(planId)` | `POST /api/billing/subscription/change` | `Observable<void>` |
475
+ | `changePlan(planId)` | `POST /api/billing/subscription/change` | `Observable<PlanChangeResult>` |
474
476
  | `setSeats(seats)` | `POST /api/billing/subscription/seats` | `Observable<void>` |
475
477
  | `listInvoices()` | `GET /api/billing/invoices` | `Observable<Invoice[]>` |
476
478
  | `listPaymentMethods()` | `GET /api/billing/payment-methods` | `Observable<PaymentMethod[]>` |
@@ -653,7 +655,7 @@ An action with `parameters` (keel v1.2.75 `table_action_parameter`) opens a dial
653
655
 
654
656
  ### Plan change vs first-time checkout (v1.0.1)
655
657
 
656
- First-time checkout uses `<sail-checkout-button>` (provider-hosted). For an **existing** subscription, upgrade/downgrade is a distinct path — `BillingService.changePlan(planId)` routes to keel's `SubscriptionLifecycle.ChangePlan` via an endpoint the app exposes. Wire the picker's selection to whichever path applies:
658
+ First-time checkout uses `<sail-checkout-button>` (provider-hosted). For an **existing** subscription, upgrade/downgrade is a distinct path — `BillingService.changePlan(planId)` routes to keel's `SubscriptionLifecycle.ChangePlan` via an endpoint the app exposes and answers `{ status: 'changed' | 'scheduled' }`: a scheduled change sets `Subscription.scheduledAt` and `scheduledPlanId`, and re-selecting the current plan cancels it. Wire the picker's selection to whichever path applies:
657
659
 
658
660
  ```typescript
659
661
  onPlanPicked(planId: string) {
@@ -671,6 +673,14 @@ onPlanPicked(planId: string) {
671
673
  <sail-dunning-banner [status]="sub()?.status" [providerManaged]="sub()?.providerManaged ?? true"></sail-dunning-banner>
672
674
  ```
673
675
 
676
+ ### Scheduled change banner (v1.1.41)
677
+
678
+ `<sail-scheduled-change-banner>` shows while `Subscription.scheduledAt` is set: the ending plan, its end date and the plan that follows (captioned from `[plans]`), with a **Keep my plan** button that re-selects the current plan and emits `kept`:
679
+
680
+ ```html
681
+ <sail-scheduled-change-banner [subscription]="sub()" [plans]="plans()" (kept)="reload()" />
682
+ ```
683
+
674
684
  ### Compose a full Billing screen
675
685
 
676
686
  A complete screen is the pieces above assembled over `BillingService` — no metadata CRUD (billing data is partner-scoped, read-mostly, money-formatted, with an external-redirect action, which `TableList`/`TableEdit` don't model):
@@ -678,6 +688,7 @@ A complete screen is the pieces above assembled over `BillingService` — no met
678
688
  ```html
679
689
  <sail-dunning-banner [status]="sub()?.status"></sail-dunning-banner>
680
690
  <sail-trial-banner [trialEnd]="sub()?.trialEnd"></sail-trial-banner>
691
+ <sail-scheduled-change-banner [subscription]="sub()" [plans]="plans()" (kept)="reload()" />
681
692
  <sail-status-chip [value]="sub()?.status"></sail-status-chip>
682
693
  <sail-plan-selector [plans]="plans()" [selected]="sub()?.planId" (selectionChange)="onPlanPicked($event)"></sail-plan-selector>
683
694
  <sail-price-selector [prices]="selectedPlan()?.prices ?? []" [selected]="offer()?.priceId" (selectionChange)="offer.set($event)"></sail-price-selector>
@@ -1277,7 +1288,8 @@ catalog and any routing via the outputs.
1277
1288
  `FrontendReturnURL` must require sign-in before the panel runs.**
1278
1289
  - **`OAuthConnectionService`** — `startOAuth(provider, extra)` → consent URL,
1279
1290
  `completeOAuth(provider, ticket)` → `{status, provider}`, `testConnection`,
1280
- `list(entityId)` / `disconnect` (generic `partner_credential` REST), and
1291
+ `list(entityId)` (generic `partner_credential` REST), `disconnect(provider, entityId)`
1292
+ (POSTs `/api/oauth/disconnect`, so keel announces the change), and
1281
1293
  `saveApiKey` (POSTs the sealed `/api/oauth/apikey`).
1282
1294
 
1283
1295
  ```html
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nauticana/sail",
3
- "version": "1.1.39",
3
+ "version": "1.1.42",
4
4
  "description": "Metadata-driven Angular component library for CRUD admin frontends backed by the keel Go backend. Tables, forms, navigation, OTP/2FA/social auth, billing, payout onboarding.",
5
5
  "main": "src/index.ts",
6
6
  "exports": {
@@ -0,0 +1,14 @@
1
+ @if (subscription(); as sub) {
2
+ @if (sub.scheduledAt) {
3
+ <div class="scheduled-change-banner" role="status">
4
+ <span class="scheduled-change-message">
5
+ Your {{ sub.caption || sub.planId }} plan ends on {{ sub.scheduledAt | date: 'mediumDate' }}.
6
+ @if (next()) { {{ next() }} starts then. }
7
+ </span>
8
+ <button matButton type="button" [disabled]="loading()" (click)="keep()">Keep my plan</button>
9
+ @if (errorMessage()) {
10
+ <div class="auth-error" role="alert">{{ errorMessage() }}</div>
11
+ }
12
+ </div>
13
+ }
14
+ }
@@ -0,0 +1,39 @@
1
+ import { ChangeDetectionStrategy, Component, ViewEncapsulation, computed, inject, input, output } from '@angular/core';
2
+ import { DatePipe } from '@angular/common';
3
+ import { MatButtonModule } from '@angular/material/button';
4
+ import { BaseAsync } from '../abstract/base_async';
5
+ import { BillingService } from '../../service/billing.service';
6
+ import { PublicPlan, Subscription } from '../../model/appdata';
7
+
8
+ /**
9
+ * Scheduled plan change banner, bound to `Subscription.scheduledAt`: names the
10
+ * ending plan, its end date and the plan that follows, with a Keep my plan
11
+ * action that re-selects the current plan. Renders nothing when no change is scheduled.
12
+ */
13
+ @Component({
14
+ selector: 'sail-scheduled-change-banner',
15
+ templateUrl: './scheduled_change_banner.html',
16
+ changeDetection: ChangeDetectionStrategy.OnPush,
17
+ encapsulation: ViewEncapsulation.None,
18
+ imports: [DatePipe, MatButtonModule],
19
+ })
20
+ export class ScheduledChangeBannerComponent extends BaseAsync {
21
+ private readonly billing = inject(BillingService);
22
+
23
+ readonly subscription = input<Subscription | null | undefined>(undefined);
24
+ /** Catalog plans, to caption the following plan. */
25
+ readonly plans = input<PublicPlan[]>([]);
26
+ /** Emits once the current plan renews again; reload the subscription. */
27
+ readonly kept = output<void>();
28
+
29
+ readonly next = computed(() => {
30
+ const id = this.subscription()?.scheduledPlanId;
31
+ return id ? (this.plans().find((p) => p.id === id)?.caption ?? id) : '';
32
+ });
33
+
34
+ keep(): void {
35
+ const sub = this.subscription();
36
+ if (!sub) return;
37
+ this.run(this.billing.changePlan(sub.planId), () => this.kept.emit(), 'Could not keep your plan. Please try again.');
38
+ }
39
+ }
@@ -245,9 +245,9 @@ export class OAuthConnectionPanelComponent implements OnInit {
245
245
  }
246
246
 
247
247
  disconnect(v: OAuthConnectionView): void {
248
- if (!v.connection?.Id) return;
248
+ if (!v.connection) return;
249
249
  this.busy.set(v.provider.code);
250
- this.svc.disconnect(v.connection.Id).subscribe({
250
+ this.svc.disconnect(v.provider.code, this.entityId()).subscribe({
251
251
  next: () => {
252
252
  this.busy.set('');
253
253
  this.disconnected.emit(v.provider.code);
package/src/index.ts CHANGED
@@ -67,6 +67,7 @@ export * from './component/billing/status_chip';
67
67
  export * from './component/billing/trial_banner';
68
68
  export * from './component/billing/seat_selector';
69
69
  export * from './component/billing/dunning_banner';
70
+ export * from './component/billing/scheduled_change_banner';
70
71
  export * from './component/payout/provider_onboarding';
71
72
  export * from './component/payout/bank_info_form';
72
73
  export * from './component/agency/agency_status_banner';
@@ -406,6 +406,14 @@ export interface Subscription {
406
406
  seats?: number;
407
407
  // False when the provider holds no subscription or customer: hide provider actions (portal, cancel, plan change).
408
408
  providerManaged: boolean;
409
+ // The plan that starts at scheduledAt, when this one ends; absent when nothing follows.
410
+ scheduledPlanId?: string;
411
+ scheduledAt?: string;
412
+ }
413
+
414
+ // keel's plan-change outcome: applied now, or at the period end.
415
+ export interface PlanChangeResult {
416
+ status: 'changed' | 'scheduled';
409
417
  }
410
418
 
411
419
  // Mirrors keel `billing.Invoice`; `status` is keel's one-character invoice status code.
@@ -1,7 +1,7 @@
1
1
  import { HttpErrorResponse, HttpEvent, HttpEventType, HttpHandlerFn, HttpInterceptorFn, HttpRequest } from '@angular/common/http';
2
2
  import { inject, Injector } from '@angular/core';
3
3
  import { Observable, catchError, switchMap, tap, throwError } from 'rxjs';
4
- import { isKeelApiUrl } from './rest_url';
4
+ import { isKeelApiUrl, isKeelUrl } from './rest_url';
5
5
  import { BaseAuthService, RefreshSupersededError } from './auth.service';
6
6
 
7
7
  // A 401 on an authed call first tries one refresh-token rotation (keel
@@ -24,6 +24,9 @@ const OPEN_KEY = 'keel_auth_circuit_until';
24
24
  * real 401 and from a network failure (status 0). */
25
25
  export const AUTH_CIRCUIT_OPEN_CODE = 'sail_auth_circuit_open';
26
26
 
27
+ // The browser's IANA zone, read once per page load; keel renders emails, notifications and exports in it.
28
+ const TIME_ZONE = Intl.DateTimeFormat().resolvedOptions().timeZone;
29
+
27
30
  export const authInterceptor: HttpInterceptorFn = (req, next) => {
28
31
  // Resolved lazily per event to avoid a DI cycle (BaseAuthService → HttpClient
29
32
  // → interceptors) and so an open circuit clears the dead session even when it
@@ -32,6 +35,10 @@ export const authInterceptor: HttpInterceptorFn = (req, next) => {
32
35
  const auth = () => injector.get(BaseAuthService, null, { optional: true });
33
36
  const expireSession = () => auth()?.sessionExpired();
34
37
 
38
+ if (isKeelUrl(req.url)) {
39
+ req = req.clone({ setHeaders: { 'X-Time-Zone': TIME_ZONE } });
40
+ }
41
+
35
42
  const token = localStorage.getItem('jwt');
36
43
  const authedApi = !!token && isKeelApiUrl(req.url);
37
44
  if (authedApi) {
@@ -7,6 +7,7 @@ import {
7
7
  CheckoutResponse,
8
8
  Invoice,
9
9
  PaymentMethod,
10
+ PlanChangeResult,
10
11
  PortalResponse,
11
12
  PublicPlan,
12
13
  SetupIntentResponse,
@@ -52,11 +53,12 @@ export class BillingService extends BaseRestService {
52
53
 
53
54
  /**
54
55
  * Upgrade / downgrade the current subscription to `planId` — distinct from
55
- * first-time checkout. Routes to keel's `SubscriptionLifecycle.ChangePlan`
56
- * (atomic close+open). 403 without PARTNER_PLAN_SUBSCRIPTION / CHANGE.
56
+ * first-time checkout. keel applies it now (`changed`) or at the period end
57
+ * (`scheduled`); the current plan resumes renewal. 403 without
58
+ * PARTNER_PLAN_SUBSCRIPTION / CHANGE.
57
59
  */
58
- changePlan(planId: string): Observable<void> {
59
- return this.http.post<void>(this.url(RestURL.changePlanURL), { planId });
60
+ changePlan(planId: string): Observable<PlanChangeResult> {
61
+ return this.http.post<PlanChangeResult>(this.url(RestURL.changePlanURL), { planId });
60
62
  }
61
63
 
62
64
  /** Set the seat quantity on the current active/trial subscription. */
@@ -8,8 +8,8 @@ import { PartnerCredential } from '../model/oauth_connection';
8
8
  /**
9
9
  * OAuthConnectionService — frontend client for keel oauth/connect.
10
10
  *
11
- * OAuth flow endpoints live under /api/oauth/*; the stored connection rows are
12
- * read/removed through the generic REST `partner_credential` API. entityId (0 =
11
+ * Flow and write endpoints live under /api/oauth/*; stored connection rows are
12
+ * listed through the generic REST `partner_credential` API. entityId (0 =
13
13
  * tenant-wide, >0 = a specific business) scopes per-entity connections.
14
14
  */
15
15
  @Injectable({ providedIn: 'root' })
@@ -49,9 +49,9 @@ export class OAuthConnectionService extends BaseRestService {
49
49
  return this.backend.list<PartnerCredential>(this.crudApi, { EntityId: String(entityId) });
50
50
  }
51
51
 
52
- /** Remove a stored connection by id. */
53
- disconnect(id: number): Observable<{ message: string }> {
54
- return this.backend.delete(this.crudApi, { Id: String(id) });
52
+ /** Remove the partner's connection to a provider in one entity scope. */
53
+ disconnect(provider: string, entityId = 0): Observable<{ status: string }> {
54
+ return this.http.post<{ status: string }>(this.url('/api/oauth/disconnect'), { provider, entity_id: entityId });
55
55
  }
56
56
 
57
57
  /** Save an API-key connection (sealed server-side by the connect handler). */