@nauticana/sail 1.1.38 → 1.1.39

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.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.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`.
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
 
@@ -598,12 +598,14 @@ Add to `src/styles.css` to match the rest of the library:
598
598
  `createPortalSession()` + `<sail-portal-button>` mirror checkout — POST `/api/billing/portal`, then redirect to the provider-hosted customer portal where the partner manages their payment method / subscription:
599
599
 
600
600
  ```html
601
- <sail-portal-button [label]="'Manage billing'"></sail-portal-button>
601
+ <sail-portal-button [label]="'Manage billing'" [providerManaged]="sub()?.providerManaged ?? true" />
602
602
  ```
603
603
 
604
+ `Subscription.providerManaged` is false for a subscription the provider does not hold (no provider subscription or customer); the button then hides. Gate an app's cancel and plan-change actions on it too.
605
+
604
606
  ### Usage meters (v1.0.0)
605
607
 
606
- `listUsage()` returns `UsageMeter[]` (`{ resource, used, limit }`, matching keel's `UsageItem`); `<sail-usage-meter>` renders one progress bar per resource (a `limit < 0` shows "Unlimited"):
608
+ `listUsage()` returns `UsageMeter[]` (`{ resource, caption, used, limit }`, matching keel's `UsageItem`); `<sail-usage-meter>` renders one progress bar per resource labelled by `caption` (a `limit < 0` shows "Unlimited"):
607
609
 
608
610
  ```typescript
609
611
  readonly meters = toSignal(this.billing.listUsage(), { initialValue: [] });
@@ -624,11 +626,11 @@ An action with `parameters` (keel v1.2.75 `table_action_parameter`) opens a dial
624
626
 
625
627
  ### Status chip (v1.0.1)
626
628
 
627
- `<sail-status-chip>` resolves a code to its caption from the client-cache `constant_value` dictionary — no per-app `@switch`. It defaults to the `SUBSCRIPTION_STATUS` domain (so `T` trialing / `X` past-due render with no app code) and works for any constant domain via `[domain]`. The code is exposed as `[attr.data-status]` so the app can colour the chip:
629
+ `<sail-status-chip>` resolves a code to its caption from the client-cache `constant_value` dictionary — no per-app `@switch`. It defaults to the `subscription_status` domain (so `T` trialing / `X` past-due render with no app code) and works for any constant domain via `[domain]`. The code is exposed as `[attr.data-status]` so the app can colour the chip:
628
630
 
629
631
  ```html
630
632
  <sail-status-chip [value]="sub()?.status"></sail-status-chip>
631
- <sail-status-chip [value]="invoice.status" domain="INVOICE_STATUS"></sail-status-chip>
633
+ <sail-status-chip [value]="client.status" domain="agency_client_status"></sail-status-chip>
632
634
  ```
633
635
 
634
636
  > Replaces a downstream app's hardcoded status `@switch` (e.g. seo's): captions are backend-authoritative — when a dictionary row is absent the raw code shows, never a fabricated label.
@@ -666,7 +668,7 @@ onPlanPicked(planId: string) {
666
668
  `<sail-dunning-banner>` shows only when `Subscription.status === 'X'` (past-due / keel `SetDunningState`), prompting the partner to update their card and deep-linking to the provider portal via an embedded `<sail-portal-button>`:
667
669
 
668
670
  ```html
669
- <sail-dunning-banner [status]="sub()?.status"></sail-dunning-banner>
671
+ <sail-dunning-banner [status]="sub()?.status" [providerManaged]="sub()?.providerManaged ?? true"></sail-dunning-banner>
670
672
  ```
671
673
 
672
674
  ### Compose a full Billing screen
@@ -683,7 +685,7 @@ A complete screen is the pieces above assembled over `BillingService` — no met
683
685
  <sail-checkout-button [priceId]="offer()!.priceId!" [quantity]="seats()" [successUrl]="okUrl" [cancelUrl]="backUrl"></sail-checkout-button>
684
686
  <sail-usage-meter [meters]="usage()"></sail-usage-meter>
685
687
  <sail-payment-methods></sail-payment-methods>
686
- <sail-portal-button></sail-portal-button>
688
+ <sail-portal-button [providerManaged]="sub()?.providerManaged ?? true" />
687
689
  <!-- invoices: render listInvoices() rows directly -->
688
690
  ```
689
691
 
@@ -1252,7 +1254,8 @@ then test / disconnect. Presentational and CSS-free; the app owns the provider
1252
1254
  catalog and any routing via the outputs.
1253
1255
 
1254
1256
  - **`OAuthConnectionPanelComponent`** (`sail-oauth-connection-panel`) — renders a card per
1255
- `[providers]: OAuthProviderDescriptor[]` with connect / test / reconnect / disconnect.
1257
+ `[providers]: OAuthProviderDescriptor[]` with connect / reconnect / disconnect, plus test for `authType: 'oauth'`
1258
+ (keel serves no test route for an API key).
1256
1259
  Providers with `fields` or a `connectNote` open an inline form/consent step first (API-key
1257
1260
  credentials, a Shopify `shop`, or a "you'll be redirected to…" note); a bare field-less OAuth
1258
1261
  provider redirects straight to consent. `[entityId]` scopes per-business connections and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nauticana/sail",
3
- "version": "1.1.38",
3
+ "version": "1.1.39",
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": {
@@ -1,6 +1,6 @@
1
1
  @if (isPastDue()) {
2
2
  <div class="dunning-banner" role="alert">
3
3
  <span class="dunning-message">{{ message() }}</span>
4
- <sail-portal-button label="Update your card"></sail-portal-button>
4
+ <sail-portal-button label="Update your card" [providerManaged]="providerManaged()" />
5
5
  </div>
6
6
  }
@@ -17,6 +17,8 @@ import { PortalButtonComponent } from './portal_button';
17
17
  export class DunningBannerComponent {
18
18
  /** Subscription.status — the banner shows when this is 'X' (past-due). */
19
19
  readonly status = input<string | undefined>(undefined);
20
+ /** Subscription.providerManaged; the portal button hides when false. */
21
+ readonly providerManaged = input(true);
20
22
  readonly message = input('Your payment is past due — please update your card to keep your subscription active.');
21
23
 
22
24
  readonly isPastDue = computed(() => this.status() === 'X');
@@ -1,10 +1,12 @@
1
- <div class="portal-button">
2
- <button matButton="elevated" class="primary" type="button"
3
- [disabled]="loading()"
4
- (click)="openPortal()">
5
- @if (loading()) { Opening… } @else { {{ label() }} }
6
- </button>
7
- @if (errorMessage()) {
8
- <div class="auth-error" role="alert">{{ errorMessage() }}</div>
9
- }
10
- </div>
1
+ @if (providerManaged()) {
2
+ <div class="portal-button">
3
+ <button matButton="elevated" class="primary" type="button"
4
+ [disabled]="loading()"
5
+ (click)="openPortal()">
6
+ @if (loading()) { Opening… } @else { {{ label() }} }
7
+ </button>
8
+ @if (errorMessage()) {
9
+ <div class="auth-error" role="alert">{{ errorMessage() }}</div>
10
+ }
11
+ </div>
12
+ }
@@ -22,6 +22,8 @@ export class PortalButtonComponent extends BaseAsync {
22
22
  private readonly billing = inject(BillingService);
23
23
 
24
24
  readonly label = input('Manage billing');
25
+ /** Subscription.providerManaged; the button hides when false. */
26
+ readonly providerManaged = input(true);
25
27
 
26
28
  openPortal(): void {
27
29
  this.run(
@@ -5,8 +5,7 @@ import { BaseAuthService } from '../../service/auth.service';
5
5
  /**
6
6
  * Status chip/label that resolves a code to its caption from the client cache
7
7
  * `constant_value` dictionary — no per-app `@switch`. Defaults to the
8
- * `SUBSCRIPTION_STATUS` domain (A active / T trialing / X past-due / …) but
9
- * works for any constant domain via `[domain]`.
8
+ * `subscription_status` domain but works for any constant domain via `[domain]`.
10
9
  *
11
10
  * The caption is backend-authoritative (read from the dictionary); when the
12
11
  * domain row is absent the raw code is shown rather than a fabricated label.
@@ -25,7 +24,7 @@ export class StatusChipComponent {
25
24
  /** The code to resolve (e.g. a Subscription.status value). */
26
25
  readonly value = input<string>('');
27
26
  /** Constant-value domain to look the caption up in. */
28
- readonly domain = input<string>('SUBSCRIPTION_STATUS');
27
+ readonly domain = input<string>('subscription_status');
29
28
 
30
29
  // Recompute when the cache (app data) loads or the inputs change.
31
30
  private readonly appData = toSignal(this.auth.getAppData());
@@ -1,10 +1,10 @@
1
1
  <div class="usage-meter">
2
2
  @for (r of rows(); track r.resource) {
3
3
  <div class="usage-row">
4
- <div class="usage-label">{{ r.resource }}</div>
4
+ <div class="usage-label">{{ r.caption }}</div>
5
5
  @if (r.hasBar) {
6
6
  <div class="usage-bar" role="progressbar"
7
- [attr.aria-label]="r.resource"
7
+ [attr.aria-label]="r.caption"
8
8
  [attr.aria-valuemin]="0"
9
9
  [attr.aria-valuemax]="r.ariaMax"
10
10
  [attr.aria-valuenow]="r.ariaNow"
@@ -4,6 +4,7 @@ import { UsageMeter } from '../../model/appdata';
4
4
  /** Per-row view model derived from a UsageMeter for accessible rendering. */
5
5
  interface UsageRow {
6
6
  resource: string;
7
+ caption: string;
7
8
  hasBar: boolean; // limit > 0 → determinate progress bar; else text-only
8
9
  percent: number; // fill width 0..100 (clamped)
9
10
  ariaMax: number; // limit (only meaningful when hasBar)
@@ -33,6 +34,7 @@ export class UsageMeterComponent {
33
34
  const hasBar = m.limit > 0;
34
35
  return {
35
36
  resource: m.resource,
37
+ caption: m.caption || m.resource,
36
38
  hasBar,
37
39
  percent: hasBar ? Math.min(100, Math.max(0, Math.round((m.used / m.limit) * 100))) : 0,
38
40
  ariaMax: m.limit,
@@ -35,7 +35,9 @@
35
35
  Connect
36
36
  </button>
37
37
  } @else {
38
- <button matButton [disabled]="busy() === v.provider.code" (click)="test(v)">Test</button>
38
+ @if (v.provider.authType === 'oauth') {
39
+ <button matButton [disabled]="busy() === v.provider.code" (click)="test(v)">Test</button>
40
+ }
39
41
  <button matButton [disabled]="busy() === v.provider.code" (click)="connect(v)">Reconnect</button>
40
42
  <button matButton [disabled]="busy() === v.provider.code" (click)="disconnect(v)">Disconnect</button>
41
43
  }
@@ -12,7 +12,8 @@ import { OAuthConnectionStatus, OAuthConnectionView, PartnerCredential, OAuthPro
12
12
 
13
13
  /**
14
14
  * ConnectionPanel — drop-in UI for keel oauth/connect. Renders a card per
15
- * app-supplied OAuthProviderDescriptor with connect / disconnect / test. Providers
15
+ * app-supplied OAuthProviderDescriptor with connect / disconnect, and test for
16
+ * OAuth providers (keel serves no test route for an API key). Providers
16
17
  * that need inputs (API-key credentials, Shopify's shop) open an inline form;
17
18
  * field-less OAuth providers redirect on one click. Ships no CSS — style the
18
19
  * documented `.connection-*` hooks. `entityId` scopes per-business connections
@@ -344,6 +344,7 @@ export interface PortalResponse {
344
344
  /** One resource's current-period usage vs its plan limit (matches keel UsageItem). */
345
345
  export interface UsageMeter {
346
346
  resource: string;
347
+ caption: string;
347
348
  used: number;
348
349
  limit: number;
349
350
  }
@@ -390,7 +391,7 @@ export interface PageRequest {
390
391
  export interface Subscription {
391
392
  planId: string;
392
393
  caption?: string; // plan display name
393
- status: string; // 'A' active, 'P' pending-payment, 'C' cancelled, 'T' trialing, 'X' past-due — caption via SUBSCRIPTION_STATUS dict
394
+ status: string; // 'A' active, 'P' pending-payment, 'C' cancelled, 'T' trialing, 'X' past-due — caption via subscription_status
394
395
  begda: string;
395
396
  endda?: string;
396
397
  installmentMinor: number;
@@ -403,6 +404,8 @@ export interface Subscription {
403
404
  trialEnd?: string;
404
405
  // Seat quantity on the active/trial sub; absent for non-seat plans. Drives `<sail-seat-selector>`.
405
406
  seats?: number;
407
+ // False when the provider holds no subscription or customer: hide provider actions (portal, cancel, plan change).
408
+ providerManaged: boolean;
406
409
  }
407
410
 
408
411
  // Mirrors keel `billing.Invoice`; `status` is keel's one-character invoice status code.