diff --git a/apps/compliance-portal/src/_locales/en-US.json b/apps/compliance-portal/src/_locales/en-US.json
index de6d9c8bd..eae17b626 100644
--- a/apps/compliance-portal/src/_locales/en-US.json
+++ b/apps/compliance-portal/src/_locales/en-US.json
@@ -65,8 +65,7 @@
"magicLinkFailed": "Couldn't send the magic link. Please try again.",
"verifyFailed": "Couldn't verify the link. Please try again.",
"fullNameFailed": "Couldn't save your name. Please try again.",
- "requestFailed": "Couldn't complete your access request. Please try again.",
- "ndaRequired": "You need to sign the NDA before requesting access."
+ "requestFailed": "Couldn't complete your access request. Please try again."
}
},
"home": {
diff --git a/apps/compliance-portal/src/_locales/fr-FR.json b/apps/compliance-portal/src/_locales/fr-FR.json
index 2b072b175..e6b17aecc 100644
--- a/apps/compliance-portal/src/_locales/fr-FR.json
+++ b/apps/compliance-portal/src/_locales/fr-FR.json
@@ -65,8 +65,7 @@
"magicLinkFailed": "Impossible d'envoyer le lien magique. Veuillez réessayer.",
"verifyFailed": "Impossible de vérifier le lien. Veuillez réessayer.",
"fullNameFailed": "Impossible d'enregistrer votre nom. Veuillez réessayer.",
- "requestFailed": "Impossible de finaliser votre demande d'accès. Veuillez réessayer.",
- "ndaRequired": "Vous devez signer l'accord de confidentialité avant de demander l'accès."
+ "requestFailed": "Impossible de finaliser votre demande d'accès. Veuillez réessayer."
}
},
"home": {
diff --git a/apps/compliance-portal/src/components/errors/resolveGateRedirect.ts b/apps/compliance-portal/src/components/errors/resolveGateRedirect.ts
index c38424dc4..10bb13d19 100644
--- a/apps/compliance-portal/src/components/errors/resolveGateRedirect.ts
+++ b/apps/compliance-portal/src/components/errors/resolveGateRedirect.ts
@@ -18,7 +18,7 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
-import { FullNameRequiredError, NDASignatureRequiredError } from "@probo/relay";
+import { gateRedirectPath } from "#/lib/auth/continueUrl";
// Maps a caught gate error to the route that resolves it, carrying the current
// URL as a `continue` target so the user returns here once the gate is cleared.
@@ -26,15 +26,5 @@ import { FullNameRequiredError, NDASignatureRequiredError } from "@probo/relay";
// page validates the continue URL via getSafeContinueUrl. Returns null for any
// other error so the boundary can fall through to its normal error UI.
export function resolveGateRedirect(error: unknown): string | null {
- const continueUrl = encodeURIComponent(window.location.href);
-
- if (error instanceof FullNameRequiredError) {
- return `/full-name?continue=${continueUrl}`;
- }
-
- if (error instanceof NDASignatureRequiredError) {
- return `/nda?continue=${continueUrl}`;
- }
-
- return null;
+ return gateRedirectPath(error, window.location.href);
}
diff --git a/apps/compliance-portal/src/lib/auth/continueUrl.ts b/apps/compliance-portal/src/lib/auth/continueUrl.ts
index eb72d9227..a5e0aa792 100644
--- a/apps/compliance-portal/src/lib/auth/continueUrl.ts
+++ b/apps/compliance-portal/src/lib/auth/continueUrl.ts
@@ -18,6 +18,8 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
+import { FullNameRequiredError, NDASignatureRequiredError } from "@probo/relay";
+
import { getPathPrefix } from "#/lib/http/pathPrefix";
// Markers appended to a post-auth `continue` URL so the portal fires the pending
@@ -70,3 +72,18 @@ export function buildRequestAccessContinueUrl(param: string, id: string): string
url.searchParams.set(param, id);
return url.toString();
}
+
+// Maps a caught auth-gate error to the route that resolves it, carrying the
+// given `continueUrl` so the user returns here (and any deferred request
+// resumes) once the gate is cleared. Returns null for non-gate errors. Shared
+// by the route boundaries and the request-access flows so all gate handling
+// stays in one place.
+export function gateRedirectPath(error: unknown, continueUrl: string): string | null {
+ if (error instanceof FullNameRequiredError) {
+ return `/full-name?continue=${encodeURIComponent(continueUrl)}`;
+ }
+ if (error instanceof NDASignatureRequiredError) {
+ return `/nda?continue=${encodeURIComponent(continueUrl)}`;
+ }
+ return null;
+}
diff --git a/apps/compliance-portal/src/lib/auth/useResumeAccessRequest.ts b/apps/compliance-portal/src/lib/auth/useResumeAccessRequest.ts
index 32e28cb87..d0176729d 100644
--- a/apps/compliance-portal/src/lib/auth/useResumeAccessRequest.ts
+++ b/apps/compliance-portal/src/lib/auth/useResumeAccessRequest.ts
@@ -19,7 +19,6 @@
// SOFTWARE.
import { Toast } from "@base-ui/react/toast";
-import { FullNameRequiredError, NDASignatureRequiredError } from "@probo/relay";
import { useEffect, useRef } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate, useSearchParams } from "react-router";
@@ -29,6 +28,7 @@ import { graphql } from "relay-runtime";
import {
buildRequestAccessContinueUrl,
buildRequestAllContinueUrl,
+ gateRedirectPath,
REQUEST_ALL_PARAM,
REQUEST_DOCUMENT_PARAM,
REQUEST_FILE_PARAM,
@@ -143,9 +143,9 @@ export function useResumeAccessRequest(isAuthenticated: boolean) {
firedRef.current = true;
// Shared outcome handling. The full-name and NDA gates are thrown by the
- // fetch layer, so they arrive in `onError`: full-name deep-links to its gate
- // (preserving the marker so the request resumes), NDA is a toast (its
- // primary path is the query-load boundary), failures toast, success confirms.
+ // fetch layer, so they arrive in `onError` and deep-link to their gate page,
+ // preserving the marker so the request resumes once cleared. Other failures
+ // toast; success confirms.
const makeHandlers = (continueUrl: string) => ({
onCompleted: (_response: unknown, errors: PayloadError[] | null) => {
if (errors && errors.length > 0) {
@@ -155,12 +155,9 @@ export function useResumeAccessRequest(isAuthenticated: boolean) {
toast.add({ title: t("auth.requestAccess.success"), type: "success" });
},
onError: (error: Error) => {
- if (error instanceof FullNameRequiredError) {
- void navigate(`/full-name?continue=${encodeURIComponent(continueUrl)}`);
- return;
- }
- if (error instanceof NDASignatureRequiredError) {
- toast.add({ title: t("auth.errors.ndaRequired"), type: "error" });
+ const gatePath = gateRedirectPath(error, continueUrl);
+ if (gatePath) {
+ void navigate(gatePath);
return;
}
toast.add({ title: t("auth.errors.requestFailed"), type: "error" });
@@ -203,10 +200,11 @@ export function useResumeAccessRequest(isAuthenticated: boolean) {
return;
}
+ const allContinueUrl = buildRequestAllContinueUrl();
clear(REQUEST_ALL_PARAM);
void requestAllAccesses({
variables: {},
- ...makeHandlers(buildRequestAllContinueUrl()),
+ ...makeHandlers(allContinueUrl),
}).catch(() => {});
}, [
isAuthenticated,
diff --git a/apps/compliance-portal/src/pages/auth/VerifyMagicLinkPage.tsx b/apps/compliance-portal/src/pages/auth/VerifyMagicLinkPage.tsx
index fc627d41e..803d6d94a 100644
--- a/apps/compliance-portal/src/pages/auth/VerifyMagicLinkPage.tsx
+++ b/apps/compliance-portal/src/pages/auth/VerifyMagicLinkPage.tsx
@@ -68,7 +68,9 @@ export default function VerifyMagicLinkPage() {
const code = (errors?.[0] as GraphQLError | undefined)?.extensions?.code;
if (code === "ALREADY_AUTHENTICATED") {
- window.location.href = getSafeContinueUrl(null);
+ // Already signed in: honor a `continue` on the URL if present so a
+ // deferred access request still resumes, instead of always going home.
+ window.location.href = getSafeContinueUrl(searchParams.get("continue"));
return;
}
if (code === "TOKEN_EXPIRED") {
diff --git a/apps/compliance-portal/src/pages/documents/_lib/useAccessRequest.ts b/apps/compliance-portal/src/pages/documents/_lib/useAccessRequest.ts
index 3bfd6e864..e2b6d420a 100644
--- a/apps/compliance-portal/src/pages/documents/_lib/useAccessRequest.ts
+++ b/apps/compliance-portal/src/pages/documents/_lib/useAccessRequest.ts
@@ -19,11 +19,7 @@
// SOFTWARE.
import { Toast } from "@base-ui/react/toast";
-import {
- FullNameRequiredError,
- NDASignatureRequiredError,
- UnAuthenticatedError,
-} from "@probo/relay";
+import { UnAuthenticatedError } from "@probo/relay";
import { useCallback, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router";
@@ -32,6 +28,7 @@ import { graphql } from "relay-runtime";
import {
buildRequestAccessContinueUrl,
+ gateRedirectPath,
REQUEST_DOCUMENT_PARAM,
REQUEST_FILE_PARAM,
REQUEST_REPORT_PARAM,
@@ -98,10 +95,9 @@ const fileMutation = graphql`
// Shared success / error handling for a single access request. The auth,
// full-name, and NDA gates are thrown by the fetch layer, so they surface in
-// `onError` (not `onError`'s GraphQL-errors argument): unauthenticated opens the
-// sign-in dialog, full-name deep-links to its gate (both deferring the request
-// via the continue URL), NDA is a toast (its primary path is the query-load
-// boundary), and everything else is a generic toast.
+// `onError`: unauthenticated opens the sign-in dialog, while full-name and NDA
+// deep-link to their gate page — all deferring the request via the continue URL
+// so it resumes once the gate is cleared. Everything else is a generic toast.
function useAccessRequestHandlers(param: string, id: string) {
const { openSignIn } = useSignInDialog();
const navigate = useNavigate();
@@ -118,23 +114,19 @@ function useAccessRequestHandlers(param: string, id: string) {
toast.add({ title: t("auth.requestAccess.success"), type: "success" });
},
onError: (error: Error) => {
+ const continueUrl = buildRequestAccessContinueUrl(param, id);
+
// Not signed in: open the dialog, deferring this request until the user
// lands back authenticated (see useResumeAccessRequest).
if (error instanceof UnAuthenticatedError) {
- openSignIn({ continueTo: buildRequestAccessContinueUrl(param, id) });
+ openSignIn({ continueTo: continueUrl });
return;
}
- // Missing profile name: send them to the full-name gate, preserving the
+ // Full-name / NDA gate: deep-link to the gate page, preserving the
// marker so the request resumes afterwards.
- if (error instanceof FullNameRequiredError) {
- const continueUrl = buildRequestAccessContinueUrl(param, id);
- void navigate(`/full-name?continue=${encodeURIComponent(continueUrl)}`);
- return;
- }
- // NDA is enforced at query load (the route boundary redirects to /nda);
- // here we only inform, matching the trust app.
- if (error instanceof NDASignatureRequiredError) {
- toast.add({ title: t("auth.errors.ndaRequired"), type: "error" });
+ const gatePath = gateRedirectPath(error, continueUrl);
+ if (gatePath) {
+ void navigate(gatePath);
return;
}
toast.add({ title: t("auth.errors.requestFailed"), type: "error" });
diff --git a/apps/compliance-portal/src/pages/nda/NDAPage.tsx b/apps/compliance-portal/src/pages/nda/NDAPage.tsx
index 3e53a5b1a..bb00bf051 100644
--- a/apps/compliance-portal/src/pages/nda/NDAPage.tsx
+++ b/apps/compliance-portal/src/pages/nda/NDAPage.tsx
@@ -189,14 +189,22 @@ export function NDAPage({ queryRef }: NDAPageProps) {
}).catch(() => {});
}
- void recordSigningEvent({
- variables: { input: { signatureId: signature.id, eventType: "CONSENT_GIVEN" } },
- onCompleted: () => {
- void acceptSignature({
- variables: { input: { signatureId: signature.id } },
- }).catch(() => {});
+ // Consent + acceptance are the critical steps: surface failures (via the
+ // default mutation error toast) so the user can retry, instead of leaving
+ // the sign button apparently inert. The fire-and-forget events above stay
+ // silent (errorToast: false on the hook).
+ void recordSigningEvent(
+ {
+ variables: { input: { signatureId: signature.id, eventType: "CONSENT_GIVEN" } },
+ onCompleted: () => {
+ void acceptSignature(
+ { variables: { input: { signatureId: signature.id } } },
+ { errorToast: true },
+ ).catch(() => {});
+ },
},
- }).catch(() => {});
+ { errorToast: true },
+ ).catch(() => {});
};
const movePage = (direction: 1 | -1) => {
@@ -209,10 +217,16 @@ export function NDAPage({ queryRef }: NDAPageProps) {
return ;
}
- if (!nda || !signature || isCompleted) {
+ if (!nda || !signature) {
return ;
}
+ // Signature already sealed: the effect above redirects to the continue URL;
+ // render nothing meanwhile so we don't flash the sign UI or the home page.
+ if (isCompleted) {
+ return null;
+ }
+
const slots = ndaPage();
return (
diff --git a/pkg/server/api/trust/v1/auth_resolvers.go b/pkg/server/api/trust/v1/auth_resolvers.go
index b80314ac9..1ded8d77f 100644
--- a/pkg/server/api/trust/v1/auth_resolvers.go
+++ b/pkg/server/api/trust/v1/auth_resolvers.go
@@ -161,10 +161,15 @@ func (r *mutationResolver) UpdateFullName(ctx context.Context, input types.Updat
profile, err := r.iam.OrganizationService.GetProfileForIdentityAndOrganization(ctx, identity.ID, compliancePage.OrganizationID)
if err != nil {
- if _, ok := errors.AsType[*iam.ErrProfileNotFound](err); !ok {
- r.logger.ErrorCtx(ctx, "cannot get profile", log.Error(err))
- return nil, gqlutils.Internal(ctx)
+ // External trust-center visitors have no organization profile; updating
+ // the identity's full name above is all that is needed for them.
+ if _, ok := errors.AsType[*iam.ErrProfileNotFound](err); ok {
+ return &types.UpdateFullNamePayload{Success: true}, nil
}
+
+ r.logger.ErrorCtx(ctx, "cannot get profile", log.Error(err))
+
+ return nil, gqlutils.Internal(ctx)
}
if profile.Source == coredata.ProfileSourceManual {