@nauticana/sail 1.1.23 → 1.1.24
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
|
@@ -772,14 +772,20 @@ being summed into a misleading total.
|
|
|
772
772
|
The agency URL defaults use `/api/v1/agency/*`. Apps mounting Keel elsewhere
|
|
773
773
|
must override the `RestURL.agency*URL` values with `configureRestUrls()`.
|
|
774
774
|
|
|
775
|
-
## Off-session SCA / 3DS confirmation (v1.0.x)
|
|
775
|
+
## Off-session SCA / 3DS confirmation (v1.0.x; refusals v1.1.24)
|
|
776
776
|
|
|
777
|
-
When an app charges a rider's **vaulted** card off-session (keel `payment.ChargeClient` / `StripeChargeClient`), the
|
|
777
|
+
When an app charges a rider's **vaulted** card off-session (keel `payment.ChargeClient` / `StripeChargeClient`), the cardholder may have to complete a Strong Customer Authentication (3DS) challenge. keel reports two distinct outcomes on `ChargeResult`, and they need different client operations:
|
|
778
778
|
|
|
779
|
-
|
|
779
|
+
| `status` | What happened | Client operation |
|
|
780
|
+
|---|---|---|
|
|
781
|
+
| `requires_action` | The PaymentIntent carries a `next_action` to run. | Run it — `actionUrl` redirect, or `confirm(clientSecret)`. |
|
|
782
|
+
| `authentication_required` | The issuer refused the off-session confirmation. The intent is back at `requires_payment_method` with **no `next_action`**. | Re-confirm the same intent on-session with the refused method — `confirmWithMethod(clientSecret, paymentMethodId)`. |
|
|
783
|
+
|
|
784
|
+
sail stays provider-agnostic: it ships **no payment-provider SDK** and **no default confirmer**. It provides a port (`ScaConfirmer`), the `SCA_CONFIRMER` injection token, and `<sail-sca-confirm>`, which dispatches on the status:
|
|
780
785
|
|
|
781
|
-
-
|
|
782
|
-
-
|
|
786
|
+
- `authentication_required` → `SCA_CONFIRMER.confirmWithMethod`; else
|
|
787
|
+
- `actionUrl` present → redirect (provider-hosted challenge — no SDK needed); else
|
|
788
|
+
- inline `clientSecret` → `SCA_CONFIRMER.confirm`.
|
|
783
789
|
|
|
784
790
|
### Backend contract (app-owned)
|
|
785
791
|
|
|
@@ -788,6 +794,7 @@ Off-session charging is a worker/handler concern, so the **app** owns the charge
|
|
|
788
794
|
```jsonc
|
|
789
795
|
// POST /api/<app>/charge → 200
|
|
790
796
|
{ "status": "requires_action", "clientSecret": "pi_..._secret_...", "actionUrl": "" }
|
|
797
|
+
// or { "status": "authentication_required", "clientSecret": "pi_..._secret_...", "paymentMethodId": "pm_..." }
|
|
791
798
|
// or { "status": "succeeded", "providerChargeId": "pi_..." }
|
|
792
799
|
// or { "status": "failed", "error": "card_declined" }
|
|
793
800
|
```
|
|
@@ -811,6 +818,13 @@ export class StripeScaConfirmer implements ScaConfirmer {
|
|
|
811
818
|
const { error } = await stripe.handleNextAction({ clientSecret });
|
|
812
819
|
return error ? { outcome: 'failed', error: error.message } : { outcome: 'succeeded' };
|
|
813
820
|
}
|
|
821
|
+
|
|
822
|
+
async confirmWithMethod(clientSecret: string, paymentMethodId: string): Promise<ScaResult> {
|
|
823
|
+
const stripe = await loadStripe(/* publishable key from backend cache */);
|
|
824
|
+
if (!stripe) return { outcome: 'failed', error: 'Stripe.js failed to load' };
|
|
825
|
+
const { error } = await stripe.confirmCardPayment(clientSecret, { payment_method: paymentMethodId });
|
|
826
|
+
return error ? { outcome: 'failed', error: error.message } : { outcome: 'succeeded' };
|
|
827
|
+
}
|
|
814
828
|
}
|
|
815
829
|
```
|
|
816
830
|
|
|
@@ -834,7 +848,7 @@ providers: [
|
|
|
834
848
|
</sail-sca-confirm>
|
|
835
849
|
```
|
|
836
850
|
|
|
837
|
-
`<sail-sca-confirm>` renders nothing for `succeeded`, the decline message for `failed`, and a confirm button for
|
|
851
|
+
`<sail-sca-confirm>` renders nothing for `succeeded`, the decline message for `failed`, and a confirm button for both action states. The publishable key is backend-authoritative — read it from the auth/app-data cache, never hardcode a fallback.
|
|
838
852
|
|
|
839
853
|
## Consent capture
|
|
840
854
|
|
|
@@ -973,6 +987,13 @@ Analytics clients that need this distinction can call the protected
|
|
|
973
987
|
`{ rows, source: { status, id } }`, populated from `X-Data-Source-Status` and
|
|
974
988
|
`X-Data-Source`; the existing `analytic<T>()` remains `Observable<T[]>`.
|
|
975
989
|
|
|
990
|
+
`sourceState(result, sourceName, connectLabel?)` maps an analytic result to
|
|
991
|
+
`{ state, message, cta }` for `[state]`, `[noSourceMessage]`, and `[connectLabel]`.
|
|
992
|
+
`NOT_CONNECTED` offers Connect; `NOT_COLLECTED` explains that collection is not
|
|
993
|
+
live and leaves the CTA empty; `READY` or no header yields `ready` / `empty` from
|
|
994
|
+
the rows. The caller supplies the source name and handles
|
|
995
|
+
the click route.
|
|
996
|
+
|
|
976
997
|
When the app and its API are on different origins the browser hides both headers
|
|
977
998
|
unless the backend sends
|
|
978
999
|
`Access-Control-Expose-Headers: X-Data-Source-Status, X-Data-Source`; without it
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nauticana/sail",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.24",
|
|
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": {
|
|
@@ -4,19 +4,7 @@ import { BaseAsync } from '../abstract/base_async';
|
|
|
4
4
|
import { ChargeResult } from '../../model/appdata';
|
|
5
5
|
import { SCA_CONFIRMER } from '../../service/sca_confirmer';
|
|
6
6
|
|
|
7
|
-
/**
|
|
8
|
-
* Drives the SCA / 3DS confirmation for an off-session `ChargeResult`.
|
|
9
|
-
*
|
|
10
|
-
* Two confirmation paths, mirroring keel's `ChargeResult`:
|
|
11
|
-
* - `actionUrl` present → redirect to the provider-hosted challenge
|
|
12
|
-
* (provider-agnostic; the page navigates away).
|
|
13
|
-
* - otherwise `clientSecret` → delegate to the injected `SCA_CONFIRMER`
|
|
14
|
-
* (the app's provider SDK wrapper). No confirmer is bundled in sail.
|
|
15
|
-
*
|
|
16
|
-
* Renders nothing for a `succeeded` result; shows the decline message for
|
|
17
|
-
* `failed`; shows a confirm button for `requires_action`. Emits `(confirmed)`
|
|
18
|
-
* on success and `(failed)` with a message on any error — never silently.
|
|
19
|
-
*/
|
|
7
|
+
/** Confirms an off-session charge through a redirect or the injected provider. */
|
|
20
8
|
@Component({
|
|
21
9
|
selector: 'sail-sca-confirm',
|
|
22
10
|
templateUrl: './sca_confirm.html',
|
|
@@ -32,23 +20,30 @@ export class ScaConfirmComponent extends BaseAsync {
|
|
|
32
20
|
readonly confirmed = output<void>();
|
|
33
21
|
readonly failed = output<string>();
|
|
34
22
|
|
|
35
|
-
readonly needsAction = computed(() =>
|
|
23
|
+
readonly needsAction = computed(() => {
|
|
24
|
+
const status = this.result().status;
|
|
25
|
+
return status === 'requires_action' || status === 'authentication_required';
|
|
26
|
+
});
|
|
36
27
|
readonly declined = computed(() => this.result().status === 'failed');
|
|
37
28
|
|
|
38
29
|
async confirm(): Promise<void> {
|
|
30
|
+
if (!this.needsAction() || this.loading()) return;
|
|
39
31
|
const res = this.result();
|
|
32
|
+
const reconfirm = res.status === 'authentication_required';
|
|
40
33
|
|
|
41
|
-
|
|
42
|
-
if (res.actionUrl) {
|
|
34
|
+
if (!reconfirm && res.actionUrl) {
|
|
43
35
|
window.location.href = res.actionUrl;
|
|
44
36
|
return;
|
|
45
37
|
}
|
|
46
38
|
|
|
47
|
-
// Inline confirmation branch — delegates to the app's provider impl.
|
|
48
39
|
if (!res.clientSecret) {
|
|
49
40
|
this.fail('Missing client secret for SCA confirmation.');
|
|
50
41
|
return;
|
|
51
42
|
}
|
|
43
|
+
if (reconfirm && !res.paymentMethodId) {
|
|
44
|
+
this.fail('Missing payment method for re-confirmation.');
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
52
47
|
if (!this.confirmer) {
|
|
53
48
|
this.fail('No SCA confirmer is configured. Provide SCA_CONFIRMER (e.g. a Stripe.js wrapper).');
|
|
54
49
|
return;
|
|
@@ -57,7 +52,9 @@ export class ScaConfirmComponent extends BaseAsync {
|
|
|
57
52
|
this.clearMessages();
|
|
58
53
|
this.loading.set(true);
|
|
59
54
|
try {
|
|
60
|
-
const out =
|
|
55
|
+
const out = reconfirm
|
|
56
|
+
? await this.confirmer.confirmWithMethod(res.clientSecret, res.paymentMethodId!)
|
|
57
|
+
: await this.confirmer.confirm(res.clientSecret);
|
|
61
58
|
this.loading.set(false);
|
|
62
59
|
if (out.outcome === 'succeeded') {
|
|
63
60
|
this.confirmed.emit();
|
package/src/model/appdata.ts
CHANGED
|
@@ -325,18 +325,21 @@ export interface UsageMeter {
|
|
|
325
325
|
// returns from its own charge endpoint, mirroring keel `payment.ChargeResult`.
|
|
326
326
|
// 'requires_action' means the rider must complete an SCA / 3DS confirmation
|
|
327
327
|
// (drive it with `<sail-sca-confirm>`).
|
|
328
|
-
|
|
328
|
+
// 'authentication_required' is an off-session refusal: no next_action to run —
|
|
329
|
+
// re-confirm the same intent on-session with paymentMethodId.
|
|
330
|
+
export type ChargeStatus = 'succeeded' | 'requires_action' | 'authentication_required' | 'failed';
|
|
329
331
|
|
|
330
332
|
export interface ChargeResult {
|
|
331
333
|
status: ChargeStatus;
|
|
332
334
|
providerChargeId?: string; // PaymentIntent / charge id (pi_xxx)
|
|
333
|
-
// Provider client secret
|
|
334
|
-
// SCA challenge must be confirmed inline (provider SDK). Mirrors keel
|
|
335
|
-
// ChargeResult.ClientSecret.
|
|
335
|
+
// Provider client secret for inline confirmation or on-session re-confirmation.
|
|
336
336
|
clientSecret?: string;
|
|
337
337
|
// Provider-hosted redirect URL when one exists (often empty for off-session
|
|
338
338
|
// 3DS). When present it's the simplest, provider-agnostic confirmation path.
|
|
339
339
|
actionUrl?: string;
|
|
340
|
+
// The refused stored method, to re-confirm with when
|
|
341
|
+
// status === 'authentication_required'. Mirrors keel ChargeResult.PaymentMethodID.
|
|
342
|
+
paymentMethodId?: string;
|
|
340
343
|
error?: string; // decline / error message when status === 'failed'
|
|
341
344
|
}
|
|
342
345
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
|
2
2
|
import { inject } from '@angular/core';
|
|
3
3
|
import { map, Observable } from 'rxjs';
|
|
4
|
+
import type { DataCardState } from '../model/dashboard';
|
|
4
5
|
import { RestURL } from './rest_url';
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -64,3 +65,20 @@ export interface AnalyticSourceStatus {
|
|
|
64
65
|
status: string | null;
|
|
65
66
|
id: string | null;
|
|
66
67
|
}
|
|
68
|
+
|
|
69
|
+
export interface SourceCardState {
|
|
70
|
+
state: DataCardState;
|
|
71
|
+
message: string;
|
|
72
|
+
cta: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Only keel's two not-ready states gate the card; `READY` or no header falls through to the rows. */
|
|
76
|
+
export function sourceState<T>(result: AnalyticResult<T>, sourceName: string, connectLabel = 'Connect'): SourceCardState {
|
|
77
|
+
if (result.source.status === 'NOT_COLLECTED') {
|
|
78
|
+
return { state: 'no-source', message: `${sourceName} collection is not live yet.`, cta: '' };
|
|
79
|
+
}
|
|
80
|
+
if (result.source.status === 'NOT_CONNECTED') {
|
|
81
|
+
return { state: 'no-source', message: `Connect ${sourceName} to see this.`, cta: connectLabel };
|
|
82
|
+
}
|
|
83
|
+
return { state: result.rows.length ? 'ready' : 'empty', message: '', cta: '' };
|
|
84
|
+
}
|
|
@@ -20,10 +20,17 @@ export interface ScaResult {
|
|
|
20
20
|
* { provide: SCA_CONFIRMER, useClass: StripeScaConfirmer }
|
|
21
21
|
*
|
|
22
22
|
* The `actionUrl` (provider-hosted redirect) branch needs no confirmer — only
|
|
23
|
-
* the inline `clientSecret`
|
|
23
|
+
* the inline `clientSecret` branches delegate here.
|
|
24
24
|
*/
|
|
25
25
|
export interface ScaConfirmer {
|
|
26
|
+
/** Run the intent's `next_action` (Stripe.js `handleNextAction`). */
|
|
26
27
|
confirm(clientSecret: string): Promise<ScaResult>;
|
|
28
|
+
/**
|
|
29
|
+
* Re-confirm the intent on-session with the method the issuer refused
|
|
30
|
+
* off-session (Stripe.js `confirmCardPayment`, stripe-ios
|
|
31
|
+
* `STPPaymentHandler.confirmPayment`) — there is no `next_action` to run.
|
|
32
|
+
*/
|
|
33
|
+
confirmWithMethod(clientSecret: string, paymentMethodId: string): Promise<ScaResult>;
|
|
27
34
|
}
|
|
28
35
|
|
|
29
36
|
export const SCA_CONFIRMER = new InjectionToken<ScaConfirmer>('SailScaConfirmer');
|