fix(web): pre-open the sign-in popup in the click gesture and use popup wording
Opening the verification page from the async poll callback was popup- blocked (the 'click sign-in twice' bug); a placeholder window opened synchronously inside the press is navigated once the URL arrives, and closed on cancel/failure. The wasm login modal now says 'popup window' instead of 'your browser' — the web editor already runs in one.
This commit is contained in:
parent
7d3b66e07b
commit
6a780c7425
4
.github/workflows/nix-check.yml
vendored
4
.github/workflows/nix-check.yml
vendored
|
|
@ -38,10 +38,10 @@ jobs:
|
|||
submodules: recursive
|
||||
|
||||
- name: Install Nix
|
||||
uses: DeterminateSystems/nix-installer-action@main
|
||||
uses: DeterminateSystems/nix-installer-action@ef8a148080ab6020fd15196c2084a2eea5ff2d25 # v22
|
||||
|
||||
- name: Enable Nix cache
|
||||
uses: DeterminateSystems/magic-nix-cache-action@main
|
||||
uses: DeterminateSystems/magic-nix-cache-action@908b263ff629f4cc17666315b7fd3ec127c6244d # v14
|
||||
|
||||
- name: Evaluate flake checks
|
||||
run: nix flake check --no-build --show-trace
|
||||
|
|
|
|||
55
.github/workflows/rust-release.yml
vendored
55
.github/workflows/rust-release.yml
vendored
|
|
@ -85,6 +85,7 @@ on:
|
|||
permissions:
|
||||
contents: write
|
||||
packages: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
version:
|
||||
|
|
@ -842,6 +843,8 @@ jobs:
|
|||
TAP_GITHUB_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: dist
|
||||
|
|
@ -967,9 +970,8 @@ jobs:
|
|||
name: ${{ steps.body.outputs.title }}
|
||||
body_path: release-body.md
|
||||
files: release-files/*
|
||||
- name: Update latest published Nix release manifest
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
- name: Prepare latest published Nix release manifest
|
||||
id: nix-manifest
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
|
@ -983,18 +985,43 @@ jobs:
|
|||
--arg cliHash "$(sri_hash op-cli-linux-x86_64.tar.gz)" \
|
||||
'{version: $version, desktopHash: $desktopHash, cliHash: $cliHash}')"
|
||||
|
||||
git fetch origin main
|
||||
git switch --create update-release-manifest origin/main
|
||||
printf '%s\n' "$manifest" > nix/release-manifest.json
|
||||
git config user.name github-actions[bot]
|
||||
git config user.email 41898282+github-actions[bot]@users.noreply.github.com
|
||||
git add nix/release-manifest.json
|
||||
if git diff --cached --quiet; then
|
||||
echo "release manifest already current"
|
||||
exit 0
|
||||
# Tags do not retain their source branch. Prefer the matching release
|
||||
# branch when it contains the tagged commit, then fall back to the
|
||||
# repository's default branch.
|
||||
release_branch="v${version}"
|
||||
default_branch="${{ github.event.repository.default_branch }}"
|
||||
git fetch --prune origin \
|
||||
"+refs/heads/*:refs/remotes/origin/*"
|
||||
if git show-ref --verify --quiet "refs/remotes/origin/${release_branch}" \
|
||||
&& git merge-base --is-ancestor "$GITHUB_SHA" "origin/${release_branch}"; then
|
||||
target_branch="$release_branch"
|
||||
elif git merge-base --is-ancestor "$GITHUB_SHA" "origin/${default_branch}"; then
|
||||
target_branch="$default_branch"
|
||||
else
|
||||
echo "::error::tagged commit is not contained in ${release_branch} or ${default_branch}"
|
||||
exit 1
|
||||
fi
|
||||
git commit -m "chore(release): update Nix release manifest for v${version}"
|
||||
git push origin HEAD:main
|
||||
|
||||
git switch --force-create "$target_branch" "origin/${target_branch}"
|
||||
printf '%s\n' "$manifest" > nix/release-manifest.json
|
||||
echo "target-branch=$target_branch" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# The release branches are protected, so publish the generated hash
|
||||
# update through a reviewable PR instead of trying to push as the
|
||||
# GitHub Actions bot. Re-running a release updates the same PR branch.
|
||||
- name: Open Nix release manifest update
|
||||
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
||||
with:
|
||||
base: ${{ steps.nix-manifest.outputs.target-branch }}
|
||||
branch: automation/nix-release-manifest-v${{ env.OP_VERSION }}
|
||||
delete-branch: true
|
||||
add-paths: nix/release-manifest.json
|
||||
commit-message: "chore(release): update Nix release manifest for v${{ env.OP_VERSION }}"
|
||||
title: "chore(release): update Nix release manifest for v${{ env.OP_VERSION }}"
|
||||
body: |
|
||||
Updates the pinned Nix prebuilt release metadata after publishing v${{ env.OP_VERSION }}.
|
||||
|
||||
This PR was generated by the Rust release workflow.
|
||||
|
||||
package-managers:
|
||||
name: Update Homebrew tap and Scoop bucket
|
||||
|
|
|
|||
|
|
@ -165,9 +165,11 @@ nix build .#appimage # portable desktop AppImage
|
|||
The flake uses the pinned Rust toolchain from `rust-toolchain.toml` and is
|
||||
currently published for `x86_64-linux`. A Debian package is not produced by
|
||||
the flake yet; use the upstream release artifacts when a `.deb` is required.
|
||||
The `prebuilt` outputs require the upstream GitHub release for the workspace
|
||||
version to publish matching Linux archives; otherwise use the source-built
|
||||
outputs above.
|
||||
The `prebuilt` outputs use the release version and hashes pinned in
|
||||
`nix/release-manifest.json`, independently of the workspace source version.
|
||||
After a release is published, the release workflow opens a PR to refresh that
|
||||
manifest. Until it is merged, the prebuilt outputs continue to use the prior
|
||||
published release; the source-built outputs always build the checked-out source.
|
||||
|
||||
**CLI (`op`):**
|
||||
|
||||
|
|
|
|||
|
|
@ -310,18 +310,25 @@ fn paint_status_note(
|
|||
) {
|
||||
use op_editor_core::{LoginFlowError, LoginFlowStatus};
|
||||
let status = status_rect(panel);
|
||||
// The web build runs IN a browser: the approval happens in a popup
|
||||
// window, so "waiting for your browser" would read as nonsense there.
|
||||
let (waiting_key, approval_key) = if cfg!(target_arch = "wasm32") {
|
||||
("account.openingPopup", "account.approveInPopup")
|
||||
} else {
|
||||
("account.waitingForBrowser", "account.waitingForApproval")
|
||||
};
|
||||
let (icon, text, background, border, color) = if let Some(flow) = flow_status {
|
||||
match flow {
|
||||
LoginFlowStatus::WaitingBrowser => (
|
||||
Icon::Globe,
|
||||
t(locale, "account.waitingForBrowser"),
|
||||
t(locale, waiting_key),
|
||||
theme.row_selected_primary,
|
||||
theme.primary.with_alpha(0.32),
|
||||
theme.foreground,
|
||||
),
|
||||
LoginFlowStatus::WaitingApproval => (
|
||||
Icon::Globe,
|
||||
t(locale, "account.waitingForApproval"),
|
||||
t(locale, approval_key),
|
||||
theme.row_selected_primary,
|
||||
theme.primary.with_alpha(0.32),
|
||||
theme.foreground,
|
||||
|
|
|
|||
|
|
@ -132,7 +132,11 @@ fn drain_actions<C: RepaintContext + 'static>(
|
|||
}
|
||||
})),
|
||||
);
|
||||
if action == PendingAuthAction::CancelLogin {
|
||||
close_login_popup_placeholder();
|
||||
}
|
||||
if !ok && action == PendingAuthAction::BeginLogin {
|
||||
close_login_popup_placeholder();
|
||||
let mut b = inner.borrow_mut();
|
||||
let ui = &mut b.host_mut().editor_state_mut().editor_ui;
|
||||
ui.login_modal_status = Some(LoginFlowStatus::Failed(LoginFlowError::Unavailable));
|
||||
|
|
@ -194,7 +198,7 @@ fn apply_login_status<C: RepaintContext + 'static>(
|
|||
if let Some(url) = parsed["verification_uri"].as_str() {
|
||||
if !url.is_empty() {
|
||||
browser_opened.set(true);
|
||||
open_popup(url);
|
||||
navigate_login_popup(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -217,6 +221,7 @@ fn apply_login_status<C: RepaintContext + 'static>(
|
|||
ui.login_modal_hover = None;
|
||||
}
|
||||
"error" => {
|
||||
close_login_popup_placeholder();
|
||||
ui.login_modal_status = Some(LoginFlowStatus::Failed(
|
||||
match parsed["code"].as_str().unwrap_or_default() {
|
||||
"denied" => LoginFlowError::Denied,
|
||||
|
|
@ -227,7 +232,10 @@ fn apply_login_status<C: RepaintContext + 'static>(
|
|||
}
|
||||
// "idle" / "canceled": another tab or the daemon finished the
|
||||
// flow out from under us — reset the note, keep the modal.
|
||||
_ => ui.login_modal_status = None,
|
||||
_ => {
|
||||
close_login_popup_placeholder();
|
||||
ui.login_modal_status = None;
|
||||
}
|
||||
}
|
||||
if ui.login_modal_status != previous || !ui.login_modal_open {
|
||||
b.host_mut().mark_editor_state_dirty();
|
||||
|
|
@ -235,12 +243,53 @@ fn apply_login_status<C: RepaintContext + 'static>(
|
|||
}
|
||||
}
|
||||
|
||||
/// Open the verification page. A blocked popup falls back to a full
|
||||
/// navigation in a new tab via an anchor click, and if even that is
|
||||
/// unavailable the user can still see the URL-less waiting note (the
|
||||
/// approval also works from any logged-in browser tab).
|
||||
fn open_popup(url: &str) {
|
||||
if let Some(window) = web_sys::window() {
|
||||
let _ = window.open_with_url_and_target(url, "_blank");
|
||||
thread_local! {
|
||||
/// Placeholder popup opened synchronously inside the SignIn click
|
||||
/// (user-activation context — the only moment `window.open` is
|
||||
/// reliably allowed). The poll callback later navigates it to the
|
||||
/// verification page; opening from the async callback instead gets
|
||||
/// popup-blocked, which is exactly the "have to click sign-in
|
||||
/// twice" failure this design removes.
|
||||
static PENDING_POPUP: RefCell<Option<web_sys::Window>> = const { RefCell::new(None) };
|
||||
}
|
||||
|
||||
/// Open the placeholder popup. Must be called synchronously from a
|
||||
/// pointer-event handler (the web sign-in press dispatcher).
|
||||
pub(crate) fn open_login_popup_placeholder() {
|
||||
let opened = web_sys::window()
|
||||
.and_then(|window| {
|
||||
window
|
||||
.open_with_url_and_target("about:blank", "_blank")
|
||||
.ok()
|
||||
})
|
||||
.flatten();
|
||||
PENDING_POPUP.with(|slot| *slot.borrow_mut() = opened);
|
||||
}
|
||||
|
||||
/// Close a still-pending placeholder (flow canceled or failed before
|
||||
/// the verification page was known).
|
||||
pub(crate) fn close_login_popup_placeholder() {
|
||||
PENDING_POPUP.with(|slot| {
|
||||
if let Some(popup) = slot.borrow_mut().take() {
|
||||
let _ = popup.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Point the placeholder at the verification page; when the placeholder
|
||||
/// is missing (blocked, or an older flow) fall back to a direct open —
|
||||
/// it may be blocked outside a gesture, but the approval also works
|
||||
/// from any logged-in browser tab, so the flow still completes.
|
||||
fn navigate_login_popup(url: &str) {
|
||||
let pending = PENDING_POPUP.with(|slot| slot.borrow_mut().take());
|
||||
match pending {
|
||||
Some(popup) => {
|
||||
let _ = popup.location().set_href(url);
|
||||
}
|
||||
None => {
|
||||
if let Some(window) = web_sys::window() {
|
||||
let _ = window.open_with_url_and_target(url, "_blank");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,23 @@ impl WidgetHost {
|
|||
self.editor_state.editor_ui.login_modal_stub_hint_shown = false;
|
||||
}
|
||||
LoginModalHit::SignIn => {
|
||||
// A flow is already running — don't spawn another
|
||||
// placeholder popup or re-begin; the poll owns the modal.
|
||||
if matches!(
|
||||
self.editor_state.editor_ui.login_modal_status,
|
||||
Some(
|
||||
LoginFlowStatus::WaitingBrowser
|
||||
| LoginFlowStatus::WaitingApproval
|
||||
| LoginFlowStatus::Exchanging
|
||||
)
|
||||
) {
|
||||
self.mark_dirty();
|
||||
return;
|
||||
}
|
||||
// Open the placeholder popup NOW, inside the click's
|
||||
// user-activation window — an async `window.open` later
|
||||
// would be popup-blocked (the "click twice" bug).
|
||||
crate::web_auth_sync::open_login_popup_placeholder();
|
||||
// Optimistic status so the modal reacts on this frame;
|
||||
// the daemon poll refines it (or reports the failure).
|
||||
self.editor_state.editor_ui.login_modal_status =
|
||||
|
|
|
|||
|
|
@ -587,6 +587,8 @@ pub fn lookup(key: &str) -> Option<&'static str> {
|
|||
"account.signInComingSoon" => "Browser sign-in is coming soon.",
|
||||
"account.waitingForBrowser" => "Waiting for your browser…",
|
||||
"account.waitingForApproval" => "Approve the sign-in in your browser…",
|
||||
"account.openingPopup" => "Opening the sign-in window…",
|
||||
"account.approveInPopup" => "Finish signing in in the popup window…",
|
||||
"account.signingIn" => "Signing you in…",
|
||||
"account.signInDenied" => "Sign-in was declined in the browser.",
|
||||
"account.signInExpired" => "The sign-in request expired. Try again.",
|
||||
|
|
|
|||
|
|
@ -591,6 +591,8 @@ pub fn lookup(key: &str) -> Option<&'static str> {
|
|||
"account.signInComingSoon" => "浏览器登录即将上线。",
|
||||
"account.waitingForBrowser" => "正在等待浏览器…",
|
||||
"account.waitingForApproval" => "请在浏览器中确认登录…",
|
||||
"account.openingPopup" => "正在打开登录窗口…",
|
||||
"account.approveInPopup" => "请在弹出的窗口中完成登录确认…",
|
||||
"account.signingIn" => "正在登录…",
|
||||
"account.signInDenied" => "登录请求在浏览器中被拒绝。",
|
||||
"account.signInExpired" => "登录请求已过期,请重试。",
|
||||
|
|
|
|||
Loading…
Reference in a new issue