diff --git a/.changeset/fix-link-button-mobile.md b/.changeset/fix-link-button-mobile.md
new file mode 100644
index 00000000..ad5be4cc
--- /dev/null
+++ b/.changeset/fix-link-button-mobile.md
@@ -0,0 +1,5 @@
+---
+"@frigade/react": patch
+---
+
+Fix mobile popup-blocker swallowing primary/secondary button link clicks. When a step exposes `primaryButton.uri` (or the legacy `primaryButtonUri`) and the consumer hasn't overridden the `navigate` prop, the button now renders as a native `` so the browser handles navigation directly. Previously the click triggered `window.open` after an awaited `step.complete`, which iOS Safari and Chrome Android silently block as a popup because the user-gesture context was lost. Buttons without a URI, and buttons under a custom `navigate` handler, are unchanged. Visual styling is identical.
diff --git a/apps/smithy/src/stories/Announcement/Announcement.stories.tsx b/apps/smithy/src/stories/Announcement/Announcement.stories.tsx
index 1e99149e..9f36327b 100644
--- a/apps/smithy/src/stories/Announcement/Announcement.stories.tsx
+++ b/apps/smithy/src/stories/Announcement/Announcement.stories.tsx
@@ -1,4 +1,11 @@
-import { Announcement, Tour, useFlow, useFrigade } from "@frigade/react";
+import {
+ Announcement,
+ FrigadeJS,
+ Provider,
+ Tour,
+ useFlow,
+ useFrigade,
+} from "@frigade/react";
import { useEffect } from "react";
export default {
@@ -15,6 +22,88 @@ export const Default = {
},
};
+// TEMP verification harness for the mobile popup-blocker fix. Uses __readOnly
+// + __flowStateOverrides to mock an Announcement whose primary CTA opens a
+// URL in a new tab. With the fix in place, the primary button renders as
+// `` so mobile browsers
+// don't block the popup.
+const MOCK_FLOW_ID = "flow_mock_link_button";
+const linkButtonFlowOverride = {
+ [MOCK_FLOW_ID]: {
+ flowSlug: MOCK_FLOW_ID,
+ flowName: "Link Button Repro",
+ flowType: FrigadeJS.FlowType.ANNOUNCEMENT,
+ data: {
+ steps: [
+ {
+ id: "step-one",
+ title: "Payment links are here",
+ subtitle: "Now you can create an order and send your customers a link to pay through multiplate.",
+ primaryButton: {
+ title: "Learn more",
+ uri: "https://example.com/learn-more",
+ target: "_blank",
+ },
+ secondaryButton: { title: "Dismiss" },
+ $state: {
+ completed: false,
+ started: false,
+ visible: true,
+ blocked: false,
+ skipped: false,
+ },
+ },
+ ],
+ },
+ $state: {
+ currentStepId: "step-one",
+ visible: true,
+ started: false,
+ completed: false,
+ skipped: false,
+ currentStepIndex: 0,
+ },
+ },
+};
+
+export const PrimaryButtonAsLink = {
+ args: { flowId: MOCK_FLOW_ID, modal: true, dismissible: true },
+ decorators: [
+ (Story, { args }) => (
+
+
+
+ ),
+ ],
+};
+
+// Same mock flow, but with a custom navigate handler — should fall back to
+// rendering as