@nauticana/sail 1.1.38 → 1.1.40
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 +13 -9
- package/package.json +1 -1
- package/src/component/billing/dunning_banner.html +1 -1
- package/src/component/billing/dunning_banner.ts +2 -0
- package/src/component/billing/portal_button.html +12 -10
- package/src/component/billing/portal_button.ts +2 -0
- package/src/component/billing/status_chip.ts +2 -3
- package/src/component/billing/usage_meter.html +2 -2
- package/src/component/billing/usage_meter.ts +2 -0
- package/src/component/oauth_connection/oauth_connection_panel.html +3 -1
- package/src/component/oauth_connection/oauth_connection_panel.ts +4 -3
- package/src/model/appdata.ts +4 -1
- package/src/service/oauth_connection.service.ts +5 -5
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 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
|
|
|
@@ -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'"
|
|
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 `
|
|
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]="
|
|
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
|
|
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 /
|
|
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
|
|
@@ -1274,7 +1277,8 @@ catalog and any routing via the outputs.
|
|
|
1274
1277
|
`FrontendReturnURL` must require sign-in before the panel runs.**
|
|
1275
1278
|
- **`OAuthConnectionService`** — `startOAuth(provider, extra)` → consent URL,
|
|
1276
1279
|
`completeOAuth(provider, ticket)` → `{status, provider}`, `testConnection`,
|
|
1277
|
-
`list(entityId)`
|
|
1280
|
+
`list(entityId)` (generic `partner_credential` REST), `disconnect(provider, entityId)`
|
|
1281
|
+
(POSTs `/api/oauth/disconnect`, so keel announces the change), and
|
|
1278
1282
|
`saveApiKey` (POSTs the sealed `/api/oauth/apikey`).
|
|
1279
1283
|
|
|
1280
1284
|
```html
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nauticana/sail",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.40",
|
|
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"
|
|
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
|
-
|
|
2
|
-
<
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
* `
|
|
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>('
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
@@ -244,9 +245,9 @@ export class OAuthConnectionPanelComponent implements OnInit {
|
|
|
244
245
|
}
|
|
245
246
|
|
|
246
247
|
disconnect(v: OAuthConnectionView): void {
|
|
247
|
-
if (!v.connection
|
|
248
|
+
if (!v.connection) return;
|
|
248
249
|
this.busy.set(v.provider.code);
|
|
249
|
-
this.svc.disconnect(v.
|
|
250
|
+
this.svc.disconnect(v.provider.code, this.entityId()).subscribe({
|
|
250
251
|
next: () => {
|
|
251
252
|
this.busy.set('');
|
|
252
253
|
this.disconnected.emit(v.provider.code);
|
package/src/model/appdata.ts
CHANGED
|
@@ -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
|
|
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.
|
|
@@ -8,8 +8,8 @@ import { PartnerCredential } from '../model/oauth_connection';
|
|
|
8
8
|
/**
|
|
9
9
|
* OAuthConnectionService — frontend client for keel oauth/connect.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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
|
|
53
|
-
disconnect(
|
|
54
|
-
return this.
|
|
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). */
|