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

**Guideline:** 4.0 · **Store:** Apple App Store · **Risk:** low · **Difficulty:** easy · **Typical turnaround:** 1-2 hours

Canonical URL: https://appstorereject.com/rejections/apple/4/guideline-40-design-web-page-opens-in-safari-before-returning-to-app

## Description

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.

## Common variations

- OAuth login flow opens Safari and redirects back via URL scheme
- Payment or checkout page opens in Safari before returning to app
- Terms of service or privacy policy opens in Safari instead of in-app
- Email verification link opens Safari, then deep links back to the app
- App opens Safari for a form or survey, then returns via universal link

## Example rejection email

```
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.
```

## Resolution steps

## How to Fix Safari Bounce-Back Flow

1. **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()
   ```

2. **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)
   ```

3. **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.

4. **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).

5. **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:)`.

6. **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.

7. **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.

## Appeal guidance

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.

## Before / after examples

**Before:** // 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:** // 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()
}
**Why it works:** 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:** // Opens terms in Safari — unnecessary app exit
Button("Terms of Service") {
    UIApplication.shared.open(
        URL(string: "https://example.com/terms")!
    )
}
**After:** // 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) {}
}
**Why it works:** 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.

## Common questions

**Can you appeal a 4.0 rejection?**

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.

**How long does this typically take to fix?**

Typical turnaround is 1-2 hours (difficulty: easy). After resubmission, most re-reviews complete within 24-48 hours.

---
*Machine-readable source: https://api.appstorereject.com/api/rejections/detail?slug=guideline-40-design-web-page-opens-in-safari-before-returning-to-app*