Guideline 4.0

Guideline 4.0 - Design: Web Page Opens in Safari Before Returning to App

Low RiskEasyTypical Fix: 1-2 hours0 Reports
Also known as:OAuth login flow opens Safari and redirects back via URL schemePayment or checkout page opens in Safari before returning to appTerms of service or privacy policy opens in Safari instead of in-appEmail verification link opens Safari, then deep links back to the appApp opens Safari for a form or survey, then returns via universal link

Our Take

Apple rejected your app because it opens a web page in Mobile Safari as part of a normal in-app flow, then bounces the user back to the app. This typically happens during OAuth login flows, payment processing, or terms-of-service viewing where the app calls UIApplication.shared.open(url) instead of presenting the web content within the app. The Safari round-trip is disorienting and creates a poor user experience. Apple expects web content to be displayed within the app using SFSafariViewController, ASWebAuthenticationSession, or WKWebView.

Resolution Guide

01

**Use ASWebAuthenticationSession for OAuth** — This is the recommended way to handle web-based login flows. It presents a system-managed web view and handles the redirect automatically:

```swift

import AuthenticationServices


let session = ASWebAuthenticationSession(

url: authURL,

callbackURLScheme: "myapp"

) { callbackURL, error in

// Handle the OAuth callback

}

session.presentationContextProvider = self

session.prefersEphemeralWebBrowserSession = true

session.start()

```

02

**Use SFSafariViewController for general web content** — For terms of service, privacy policy, help pages, and other web content that doesn't need deep app integration:

```swift

import SafariServices


let safari = SFSafariViewController(url: termsURL)

present(safari, animated: true)

```

03

**Use WKWebView for deeply integrated web content** — If you need to inject JavaScript, intercept navigation, or customize the web experience, embed a WKWebView directly in your view hierarchy.


04

**Remove all UIApplication.shared.open() calls for in-app flows** — Search your codebase for `UIApplication.shared.open` and `openURL`. Replace any that are part of the normal app flow (not external links the user explicitly requests).


05

**Handle OAuth redirects in-app** — If using ASWebAuthenticationSession, the callback is handled automatically. If using a custom approach, register your URL scheme in Info.plist and handle it in `application(_:open:options:)`.


06

**Test the full authentication flow** — Sign out, then sign in again and verify the entire flow stays within the app. The user should never see Safari's UI during normal app usage.


07

**Keep "Open in Safari" for external links** — It's fine to open external links (e.g., "Visit our website") in Safari with a share button or explicit "Open in Safari" option. The rejection is about automatic bouncing, not user-initiated navigation.


Example Rejection Email

From:Apple App Review Team
Subject:Guideline 4.0 - Design: Web Page Opens in Safari Before
Upon launching the app, a web page in mobile Safari opens first before returning the user to the app. Specifically, when the user taps 'Sign In,' the app opens a login page in Safari, and after authentication the user is redirected back to the app via a URL scheme. This creates a disruptive experience. In-app web content should be displayed using SFSafariViewController or WKWebView to keep users within your app. Next Steps: Please revise your app to present web content within the app and resubmit.

Consider Appealing

Do not appeal. This is a clear UX issue with a direct fix. Replace the Safari redirect with ASWebAuthenticationSession (for auth) or SFSafariViewController (for content) and resubmit. The fix takes less time than waiting for an appeal response.

Generate Appeal

Before & After

Before — Rejected

// Opens Safari for login — user bounces out and back

func signIn() {

let authURL = URL(string: "https://auth.example.com/login?redirect=myapp://callback")!

UIApplication.shared.open(authURL) // opens Safari!

}


// In AppDelegate:

func application(_ app: UIApplication, open url: URL, options: ...) -> Bool {

if url.scheme == "myapp" { handleAuthCallback(url) }

return true

}

After — Approved

// Stays in-app using ASWebAuthenticationSession

import AuthenticationServices


func signIn() {

let authURL = URL(string: "https://auth.example.com/login")!

let session = ASWebAuthenticationSession(

url: authURL,

callbackURLScheme: "myapp"

) { callbackURL, error in

guard let url = callbackURL else { return }

self.handleAuthCallback(url)

}

session.presentationContextProvider = self

session.prefersEphemeralWebBrowserSession = true

session.start()

}

What changed: ASWebAuthenticationSession presents a secure in-app browser sheet for authentication. The user never leaves the app, and the callback is handled seamlessly. Setting prefersEphemeralWebBrowserSession to true avoids sharing Safari cookies.

Before — Rejected

// Opens terms in Safari — unnecessary app exit

Button("Terms of Service") {

UIApplication.shared.open(

URL(string: "https://example.com/terms")!

)

}

After — Approved

// Opens terms in-app with SFSafariViewController

@State private var showTerms = false


Button("Terms of Service") { showTerms = true }

.sheet(isPresented: $showTerms) {

SafariView(url: URL(string: "https://example.com/terms")!)

}


// Helper wrapper for SwiftUI

struct SafariView: UIViewControllerRepresentable {

let url: URL

func makeUIViewController(context: Context) -> SFSafariViewController {

SFSafariViewController(url: url)

}

func updateUIViewController(_ vc: SFSafariViewController, context: Context) {}

}

What changed: SFSafariViewController displays web content in a modal sheet within the app. The user can read the terms and dismiss the sheet to return to exactly where they were, with no Safari bounce.

Community Solutions · 0

Sign in to share your solution.

More Guideline 4 (Design) rejections

View all Guideline 4 rejections