diff --git a/messages/de.json b/messages/de.json
index b02ed86da..b0f681548 100644
--- a/messages/de.json
+++ b/messages/de.json
@@ -4636,6 +4636,7 @@
"remainingMany": "Noch {count} Messungen bis zum Trend."
},
"settings": {
+ "oauthCallbackPreflightItem": "{provider}: Die Callback-Adresse {configuredOrigin} unterscheidet sich von der App-Adresse {currentOrigin}.",
"shell": {
"sectionsNav": "Einstellungs-Bereiche",
"groups": {
diff --git a/messages/en.json b/messages/en.json
index a7472a8de..e9b91967f 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -4636,6 +4636,7 @@
"remainingMany": "{count} more readings to go."
},
"settings": {
+ "oauthCallbackPreflightItem": "{provider}: callback address {configuredOrigin} differs from this app address {currentOrigin}.",
"shell": {
"sectionsNav": "Settings sections",
"groups": {
diff --git a/src/components/settings/integrations/fitbit-card.tsx b/src/components/settings/integrations/fitbit-card.tsx
index 351ef2dd2..98e7c4056 100644
--- a/src/components/settings/integrations/fitbit-card.tsx
+++ b/src/components/settings/integrations/fitbit-card.tsx
@@ -65,8 +65,10 @@ import {
type SyncOutcomeState,
} from "./sync-outcome";
import {
+ CallbackMismatchNotice,
IntegrationCardDescription,
IntegrationRedirectGuide,
+ integrationCallbackUrl,
} from "./setup-guide-link";
export function FitbitCard({
@@ -521,6 +523,10 @@ export function FitbitCard({
)}
+
);
}
diff --git a/src/components/settings/integrations/google-health-card.tsx b/src/components/settings/integrations/google-health-card.tsx
index c76e11e57..99b127409 100644
--- a/src/components/settings/integrations/google-health-card.tsx
+++ b/src/components/settings/integrations/google-health-card.tsx
@@ -80,7 +80,11 @@ import {
type SyncOutcomeResource,
type SyncOutcomeState,
} from "./sync-outcome";
-import { IntegrationCardDescription } from "./setup-guide-link";
+import {
+ CallbackMismatchNotice,
+ IntegrationCardDescription,
+ integrationCallbackUrl,
+} from "./setup-guide-link";
export function GoogleHealthCard({
viewModel,
@@ -758,6 +762,10 @@ export function GoogleHealthCard({
)}
+
);
}
diff --git a/src/components/settings/integrations/oauth-provider-card.tsx b/src/components/settings/integrations/oauth-provider-card.tsx
index 3b15226f4..147fcc79a 100644
--- a/src/components/settings/integrations/oauth-provider-card.tsx
+++ b/src/components/settings/integrations/oauth-provider-card.tsx
@@ -65,9 +65,11 @@ import {
pillStateForVerdict,
} from "./shared";
import {
+ CallbackMismatchNotice,
IntegrationCardDescription,
IntegrationRedirectGuide,
type IntegrationDocsProvider,
+ integrationCallbackUrl,
} from "./setup-guide-link";
export interface OAuthProviderStatus {
@@ -522,6 +524,10 @@ export function OAuthProviderCard({
)}
+
);
}
diff --git a/src/components/settings/integrations/setup-guide-link.tsx b/src/components/settings/integrations/setup-guide-link.tsx
index 96ac12b49..23889a30a 100644
--- a/src/components/settings/integrations/setup-guide-link.tsx
+++ b/src/components/settings/integrations/setup-guide-link.tsx
@@ -11,6 +11,7 @@
* destination now so a user who is mid-setup always knows where to go.
*/
+import { useSyncExternalStore } from "react";
import { ExternalLink } from "lucide-react";
import { useTranslations } from "@/lib/i18n/context";
@@ -51,6 +52,44 @@ export function integrationCallbackUrl(
return `${base}/api/${provider}/callback`;
}
+/** Shows a compact warning when the callback origin differs from the app. */
+export function CallbackMismatchNotice({
+ provider,
+ callbackUrl,
+}: {
+ provider: string;
+ callbackUrl: string;
+}) {
+ // Get the current origin of the app
+ const { t } = useTranslations();
+ const currentOrigin = useSyncExternalStore(
+ () => () => {},
+ () => window.location.origin,
+ () => null,
+ );
+ // Get the origin of the callback URL
+ const callbackOrigin = (() => {
+ try {
+ return new URL(callbackUrl).origin;
+ } catch {
+ return null;
+ }
+ })();
+ if (!currentOrigin || !callbackOrigin || callbackOrigin === currentOrigin) {
+ return null;
+ }
+ // If the callback origin differs from the app, show a warning
+ return (
+
+ {t("settings.oauthCallbackPreflightItem", {
+ provider,
+ configuredOrigin: callbackOrigin,
+ currentOrigin,
+ })}
+
+ );
+}
+
/**
* v1.29.x (UX audit H2) — compact 3-step BYO-developer-app guide for the
* six providers that require a self-hoster to register their own vendor
diff --git a/src/components/settings/integrations/whoop-card.tsx b/src/components/settings/integrations/whoop-card.tsx
index 6abcec564..db02bd914 100644
--- a/src/components/settings/integrations/whoop-card.tsx
+++ b/src/components/settings/integrations/whoop-card.tsx
@@ -56,8 +56,10 @@ import {
type SyncOutcomeState,
} from "./sync-outcome";
import {
+ CallbackMismatchNotice,
IntegrationCardDescription,
IntegrationRedirectGuide,
+ integrationCallbackUrl,
} from "./setup-guide-link";
export function WhoopCard({
@@ -503,6 +505,10 @@ export function WhoopCard({
)}
+
);
}
diff --git a/src/components/settings/integrations/withings-card.tsx b/src/components/settings/integrations/withings-card.tsx
index faf143687..b85c8b03b 100644
--- a/src/components/settings/integrations/withings-card.tsx
+++ b/src/components/settings/integrations/withings-card.tsx
@@ -55,8 +55,10 @@ import {
type SyncOutcomeState,
} from "./sync-outcome";
import {
+ CallbackMismatchNotice,
IntegrationCardDescription,
IntegrationRedirectGuide,
+ integrationCallbackUrl,
} from "./setup-guide-link";
export function WithingsCard({
@@ -552,6 +554,10 @@ export function WithingsCard({
)}
+
);
}