mirror of
https://github.com/block/buzz.git
synced 2026-11-04 00:12:02 +00:00
fix(mobile): show the Join prompt when an invite launches Buzz (#8970)
## Summary Opening an invite while Buzz is closed can leave the Join prompt invisible and block later attempts. Native confirmation can attach to a temporary system dialog that then closes, leaving the join result pending. This fix uses Flutter confirmation when the native presenter is occupied. If UIKit rejects presentation, it clears the pending session so the user can try again. ### Related issue Related invite handoff: https://github.com/block/buzz/pull/8601. ### Testing The native regression fails before the fix and passes afterward. It covers rejected presentation, a second attempt, and an existing temporary dialog. The isolated PR build passed a fresh-user journey from Safari consent through launching Buzz, visible confirmation, Join, starter channels and seeded history. ### Screenshots Visible Join prompt after Safari opens Buzz, from this PR build:  ### Remaining work Finish Dart validation and review before readiness. Signed-off-by: John Tennant <jtennant@block.xyz> Co-authored-by: Zack <04f47b290b155b3b8b782b11501cdb15f1e21e82dfceefd01ff0ed405459d927@buzz.block.builderlab.xyz>
This commit is contained in:
committed by
GitHub
parent
205ff47744
commit
1c143a40b7
@@ -24,10 +24,9 @@ final class NativeCommunityEntryCoordinator: NSObject, UIAdaptivePresentationCon
|
||||
let root = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }
|
||||
.filter { $0.activationState == .foregroundActive }.flatMap(\.windows)
|
||||
.first(where: \.isKeyWindow)?.rootViewController
|
||||
guard var presenter = root else {
|
||||
guard let presenter = root, presenter.presentedViewController == nil else {
|
||||
result(FlutterError(code: "presentation_failed", message: nil, details: nil)); return
|
||||
}
|
||||
while let presented = presenter.presentedViewController { presenter = presented }
|
||||
let controller = NativeCommunityEntryController(configuration: args)
|
||||
controller.action = { [weak self] method, payload, completion in
|
||||
guard let self, self.session == session else { return }
|
||||
@@ -47,6 +46,10 @@ final class NativeCommunityEntryCoordinator: NSObject, UIAdaptivePresentationCon
|
||||
self.navigation = navigation
|
||||
controller.configureDetents()
|
||||
presenter.present(navigation, animated: !UIAccessibility.isReduceMotionEnabled)
|
||||
guard presenter.presentedViewController === navigation else {
|
||||
self.complete(FlutterError(code: "presentation_failed", message: nil, details: nil))
|
||||
return
|
||||
}
|
||||
navigation.presentationController?.delegate = self
|
||||
}
|
||||
}
|
||||
@@ -71,12 +74,12 @@ final class NativeCommunityEntryCoordinator: NSObject, UIAdaptivePresentationCon
|
||||
}
|
||||
}
|
||||
|
||||
private func complete(_ joined: Bool) {
|
||||
private func complete(_ value: Any?) {
|
||||
let result = pending
|
||||
pending = nil
|
||||
navigation = nil
|
||||
session = nil
|
||||
result?(joined)
|
||||
result?(value)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,6 +239,7 @@ private final class NativeCommunityEntryController: UIViewController, UITextFiel
|
||||
submit = addButton("Join community", primary: true) { [weak self] in
|
||||
self?.request("join", [:])
|
||||
}
|
||||
submit?.accessibilityIdentifier = "community-invite-join"
|
||||
} else {
|
||||
for choice in choices {
|
||||
addButton("\(choice["label"] ?? "Saved identity")\n\(choice["npub"] ?? "")") { [weak self] in
|
||||
|
||||
@@ -8,6 +8,39 @@ import XCTest
|
||||
|
||||
class RunnerTests: XCTestCase {
|
||||
|
||||
@MainActor
|
||||
func testRejectedCommunityEntryPresentationCompletesAndAllowsRetry() throws {
|
||||
let messenger = NavigationTestMessenger()
|
||||
let presenter = RejectedCommunityEntryPresenter()
|
||||
let window = UIWindow(frame: UIScreen.main.bounds)
|
||||
window.windowScene = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }.first
|
||||
window.rootViewController = presenter
|
||||
window.makeKeyAndVisible()
|
||||
defer { window.isHidden = true }
|
||||
let coordinator = NativeCommunityEntryCoordinator(messenger: messenger)
|
||||
for session in [1, 2] {
|
||||
var reply: Any?
|
||||
messenger.invoke("present", arguments: ["session": session, "name": "Local test", "invite": true]) { reply = $0 }
|
||||
XCTAssertEqual((reply as? FlutterError)?.code, "presentation_failed")
|
||||
XCTAssertEqual(presenter.attempts, session)
|
||||
}
|
||||
let root = UIViewController()
|
||||
window.rootViewController = root
|
||||
window.makeKeyAndVisible()
|
||||
let temporary = UIViewController()
|
||||
let presented = expectation(description: "Temporary native modal appeared")
|
||||
root.present(temporary, animated: false) { presented.fulfill() }
|
||||
wait(for: [presented], timeout: 2)
|
||||
XCTAssertTrue(root.presentedViewController === temporary)
|
||||
XCTAssertNotNil(temporary.view.window)
|
||||
var reply: Any?
|
||||
messenger.invoke("present", arguments: ["session": 3, "name": "Local test", "invite": true]) { reply = $0 }
|
||||
XCTAssertEqual((reply as? FlutterError)?.code, "presentation_failed")
|
||||
XCTAssertNil(temporary.presentedViewController)
|
||||
root.dismiss(animated: false)
|
||||
withExtendedLifetime(coordinator) {}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func testCustomFlutterHeaderUsesDecorativeSoftEdge() throws {
|
||||
guard #available(iOS 27.0, *) else { return }
|
||||
@@ -2085,3 +2118,11 @@ private final class NavigationTestMessenger: NSObject, FlutterBinaryMessenger {
|
||||
handler?(message) { _ in }
|
||||
}
|
||||
}
|
||||
|
||||
private final class RejectedCommunityEntryPresenter: UIViewController {
|
||||
var attempts = 0
|
||||
override func present(_ viewControllerToPresent: UIViewController, animated flag: Bool, completion: (() -> Void)? = nil) {
|
||||
// Reproduce UIKit declining a presentation during a scene transition.
|
||||
attempts += 1
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user