add sign in with apple example

Seto committed Feb 7, 2026 at 15:01 UTC 1e90ac71d0b3265984ce1ac1a9e1e6a2c53479e9
7 files changed +281 -55
examples/tauri-app/package.json
+6 -6
@@ -10,13 +10,13 @@
10 "tauri": "tauri"
11 },
12 "dependencies": {
13 - "@tauri-apps/api": "^2.0.0",
14 - "tauri-plugin-ios-window-api": "file:../../"
13 + "@tauri-apps/api": "^2.10.1",
14 + "tauri-plugin-ios-window-api": "file:../.."
15 },
16 "devDependencies": {
17 - "@sveltejs/vite-plugin-svelte": "^6.0.0",
18 - "svelte": "^5.0.0",
19 - "vite": "^7.0.0",
20 - "@tauri-apps/cli": "^2.0.0"
17 + "@sveltejs/vite-plugin-svelte": "^6.2.4",
18 + "@tauri-apps/cli": "^2.10.0",
19 + "svelte": "^5.50.0",
20 + "vite": "^7.3.1"
21 }
22 }
examples/tauri-app/pnpm-lock.yaml
+6 -6
@@ -9,23 +9,23 @@ importers:
9 .:
10 dependencies:
11 '@tauri-apps/api':
12 - specifier: ^2.0.0
12 + specifier: ^2.10.1
13 version: 2.10.1
14 tauri-plugin-ios-window-api:
15 - specifier: file:../../
15 + specifier: file:../..
16 version: file:../..
17 devDependencies:
18 '@sveltejs/vite-plugin-svelte':
19 - specifier: ^6.0.0
19 + specifier: ^6.2.4
20 version: 6.2.4(svelte@5.50.0)(vite@7.3.1)
21 '@tauri-apps/cli':
22 - specifier: ^2.0.0
22 + specifier: ^2.10.0
23 version: 2.10.0
24 svelte:
25 - specifier: ^5.0.0
25 + specifier: ^5.50.0
26 version: 5.50.0
27 vite:
28 - specifier: ^7.0.0
28 + specifier: ^7.3.1
29 version: 7.3.1
30
31 packages:
examples/tauri-app/src/App.svelte
+93 -19
@@ -2,8 +2,21 @@
2 import Greet from "./lib/Greet.svelte";
3 import { open } from "tauri-plugin-ios-window-api";
4
5 + // Environment configuration
6 + let environment = $state("production"); // Toggle between "production" and "development"
7 +
8 + $effect(() => {
9 + console.log(`Environment: ${environment}`);
10 + });
11 +
12 + function getBaseUrl() {
13 + return environment === "production"
14 + ? "https://splitfire.ai"
15 + : "https://splitfire-ai-local.com:3333";
16 + }
17 +
18 function openTauriWebsite() {
6 - open("https://tauri.app")
19 + open("https://tauri.app", "Tauri")
20 .then(() => {
21 console.log("Tauri website opened successfully");
22 })
@@ -12,24 +25,50 @@
25 });
26 }
27
15 - function signInWithApple() {
16 - // Apple ID authentication URL
17 - // In production, you would use proper OAuth flow with your client ID and redirect URI
18 - const appleAuthUrl =
19 - "https://appleid.apple.com/auth/authorize?" +
20 - "response_type=code&" +
21 - "response_mode=form_post&" +
22 - "client_id=YOUR_CLIENT_ID&" +
23 - "redirect_uri=YOUR_REDIRECT_URI&" +
24 - "scope=name%20email";
28 + async function signInWithApple() {
29 + try {
30 + const baseUrl = getBaseUrl();
31 +
32 + // OAuth callback configuration
33 + // Option 1: Custom URL scheme (recommended for mobile)
34 + // const redirectUri = "splitfire://oauth/callback";
35 +
36 + // Option 2: Localhost server (for testing/desktop)
37 + const redirectUri = "http://localhost:8080/oauth/callback";
38 +
39 + // Build the OAuth URL following SplitFire pattern
40 + const redirectUrl = `${baseUrl}/synch-apple/auth?redirect_uri=${encodeURIComponent(redirectUri)}`;
41 +
42 + console.log(`Opening Apple OAuth: ${redirectUrl}`);
43 + console.log(`Callback will be sent to: ${redirectUri}`);
44 +
45 + // Open the OAuth window
46 + await open(redirectUrl, "Sign in with Apple");
47 + console.log("Apple OAuth window opened successfully");
48 +
49 + // TODO: Implement OAuth callback handling
50 + // 1. Set up a listener for the redirect URI (deep link or local server)
51 + // 2. Parse the authorization code/token from the callback
52 + // 3. Send to your backend for validation
53 + // 4. Store the user session
54 +
55 + // Example callback handler (pseudo-code):
56 + // listen("oauth-callback", (event) => {
57 + // const { code, state } = event.payload;
58 + // // Exchange code for access token via backend
59 + // fetch(`${baseUrl}/api/oauth/apple/token`, {
60 + // method: "POST",
61 + // body: JSON.stringify({ code, redirect_uri: redirectUri })
62 + // });
63 + // });
64 + } catch (error) {
65 + console.error("Failed to open Apple OAuth:", error);
66 + }
67 + }
68
26 - open(appleAuthUrl)
27 - .then(() => {
28 - console.log("Sign in with Apple opened successfully");
29 - })
30 - .catch((error) => {
31 - console.error("Failed to open Sign in with Apple:", error);
32 - });
69 + function toggleEnvironment() {
70 + environment =
71 + environment === "production" ? "development" : "production";
72 }
73 </script>
74
@@ -55,6 +94,14 @@
94 </div>
95
96 <div class="button-container">
97 + <!-- Environment Toggle -->
98 + <div class="environment-toggle">
99 + <button onclick={toggleEnvironment} class="btn btn-secondary">
100 + Environment: {environment}
101 + </button>
102 + <p class="environment-url">{getBaseUrl()}</p>
103 + </div>
104 +
105 <button onclick={openTauriWebsite} class="btn btn-primary">
106 Open Tauri Website
107 </button>
@@ -65,7 +112,7 @@
112 d="M17.05 20.28c-.98.95-2.05.8-3.08.35-1.09-.46-2.09-.48-3.24 0-1.44.62-2.2.44-3.06-.35C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09l.01-.01zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z"
113 />
114 </svg>
68 - Sign in with Apple
115 + Sign in with Apple (SplitFire)
116 </button>
117 </div>
118 </main>
@@ -128,4 +175,31 @@
175 .btn:active {
176 transform: translateY(0);
177 }
178 +
179 + .btn-secondary {
180 + background: #6c757d;
181 + color: white;
182 + }
183 +
184 + .btn-secondary:hover {
185 + background: #5a6268;
186 + transform: translateY(-2px);
187 + box-shadow: 0 4px 12px rgba(108, 117, 125, 0.4);
188 + }
189 +
190 + .environment-toggle {
191 + display: flex;
192 + flex-direction: column;
193 + gap: 0.5rem;
194 + align-items: center;
195 + padding-bottom: 1rem;
196 + border-bottom: 1px solid rgba(255, 255, 255, 0.1);
197 + margin-bottom: 1rem;
198 + }
199 +
200 + .environment-url {
201 + font-size: 0.875rem;
202 + color: #888;
203 + margin: 0;
204 + }
205 </style>
guest-js/index.ts
+2 -1
@@ -1,9 +1,10 @@
1 import { invoke } from "@tauri-apps/api/core";
2
3 -export async function open(url: string): Promise<void> {
3 +export async function open(url: string, title?: string): Promise<void> {
4 await invoke("plugin:ios-window|open", {
5 payload: {
6 url,
7 + title,
8 },
9 });
10 }
ios/Sources/IosWindow.swift
+2
@@ -5,6 +5,7 @@ import WebKit
5
6 class OpenArgs: Decodable {
7 let url: String
8 + let title: String?
9 }
10
11 class IosWindow: Plugin {
@@ -25,6 +26,7 @@ class IosWindow: Plugin {
26 // Create the new view controller
27 let newVC = NewViewController()
28 newVC.url = args.url
29 + newVC.windowTitle = args.title ?? "Sign in"
30
31 // Wrap in navigation controller for proper presentation
32 let navController = UINavigationController(rootViewController: newVC)
ios/Sources/NewViewController.swift
+171 -23
@@ -11,62 +11,210 @@ import WebKit
11 final class NewViewController: UIViewController {
12
13 var url: String = ""
14 + var windowTitle: String = "Sign in"
15
16 private var webView: WKWebView!
17 + private var progressView: UIProgressView!
18 + private var navigationBar: UIView!
19 + private var titleLabel: UILabel!
20 + private var loadingIndicator: UIActivityIndicatorView!
21
22 override func viewDidLoad() {
23 super.viewDidLoad()
19 - title = "New Window"
24 view.backgroundColor = .systemBackground
25
22 - setupUI()
26 + setupNavigationBar()
27 + setupWebView()
28 + setupProgressIndicator()
29 loadURL()
30 }
31
26 - private func setupUI() {
27 - // Configure WebView
28 - let webConfiguration = WKWebViewConfiguration()
29 - webView = WKWebView(frame: .zero, configuration: webConfiguration)
30 - webView.translatesAutoresizingMaskIntoConstraints = false
31 - webView.allowsBackForwardNavigationGestures = true
32 - view.addSubview(webView)
32 + private func setupNavigationBar() {
33 + // Navigation bar container
34 + navigationBar = UIView()
35 + navigationBar.backgroundColor = .systemBackground
36 + navigationBar.translatesAutoresizingMaskIntoConstraints = false
37 + view.addSubview(navigationBar)
38 +
39 + // Add subtle bottom border
40 + let borderView = UIView()
41 + borderView.backgroundColor = .separator.withAlphaComponent(0.3)
42 + borderView.translatesAutoresizingMaskIntoConstraints = false
43 + navigationBar.addSubview(borderView)
44
34 - // Close button
45 + // Close button (leading position per Apple HIG for modal presentations)
46 let closeButton = UIButton(type: .system)
47 closeButton.setTitle("Close", for: .normal)
37 - closeButton.titleLabel?.font = .systemFont(ofSize: 17, weight: .semibold)
38 - closeButton.backgroundColor = .systemBackground.withAlphaComponent(0.9)
39 - closeButton.layer.cornerRadius = 8
40 - closeButton.contentEdgeInsets = UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 16)
48 + closeButton.titleLabel?.font = .systemFont(ofSize: 17, weight: .regular)
49 closeButton.translatesAutoresizingMaskIntoConstraints = false
50 closeButton.addTarget(self, action: #selector(closeButtonTapped), for: .touchUpInside)
43 - view.addSubview(closeButton)
51 + navigationBar.addSubview(closeButton)
52 +
53 + // Title label (centered)
54 + titleLabel = UILabel()
55 + titleLabel.text = windowTitle
56 + titleLabel.font = .systemFont(ofSize: 17, weight: .semibold)
57 + titleLabel.textColor = .label
58 + titleLabel.textAlignment = .center
59 + titleLabel.translatesAutoresizingMaskIntoConstraints = false
60 + navigationBar.addSubview(titleLabel)
61 +
62 + // Loading indicator (trailing position)
63 + loadingIndicator = UIActivityIndicatorView(style: .medium)
64 + loadingIndicator.hidesWhenStopped = true
65 + loadingIndicator.translatesAutoresizingMaskIntoConstraints = false
66 + navigationBar.addSubview(loadingIndicator)
67 +
68 + NSLayoutConstraint.activate([
69 + // Navigation bar
70 + navigationBar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
71 + navigationBar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
72 + navigationBar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
73 + navigationBar.heightAnchor.constraint(equalToConstant: 44),
74 +
75 + // Border view
76 + borderView.leadingAnchor.constraint(equalTo: navigationBar.leadingAnchor),
77 + borderView.trailingAnchor.constraint(equalTo: navigationBar.trailingAnchor),
78 + borderView.bottomAnchor.constraint(equalTo: navigationBar.bottomAnchor),
79 + borderView.heightAnchor.constraint(equalToConstant: 0.5),
80 +
81 + // Close button (leading, per Apple HIG)
82 + closeButton.leadingAnchor.constraint(
83 + equalTo: navigationBar.leadingAnchor, constant: 16),
84 + closeButton.centerYAnchor.constraint(equalTo: navigationBar.centerYAnchor),
85 +
86 + // Title label (centered)
87 + titleLabel.centerXAnchor.constraint(equalTo: navigationBar.centerXAnchor),
88 + titleLabel.centerYAnchor.constraint(equalTo: navigationBar.centerYAnchor),
89 + titleLabel.leadingAnchor.constraint(
90 + greaterThanOrEqualTo: closeButton.trailingAnchor, constant: 16),
91 + titleLabel.trailingAnchor.constraint(
92 + lessThanOrEqualTo: loadingIndicator.leadingAnchor, constant: -16),
93 +
94 + // Loading indicator (trailing)
95 + loadingIndicator.trailingAnchor.constraint(
96 + equalTo: navigationBar.trailingAnchor, constant: -16),
97 + loadingIndicator.centerYAnchor.constraint(equalTo: navigationBar.centerYAnchor),
98 + ])
99 + }
100 +
101 + private func setupWebView() {
102 + // Configure WebView with optimized settings
103 + let webConfiguration = WKWebViewConfiguration()
104 + webConfiguration.allowsInlineMediaPlayback = true
105 + webConfiguration.mediaTypesRequiringUserActionForPlayback = []
106 +
107 + webView = WKWebView(frame: .zero, configuration: webConfiguration)
108 + webView.translatesAutoresizingMaskIntoConstraints = false
109 + webView.allowsBackForwardNavigationGestures = true
110 + webView.scrollView.contentInsetAdjustmentBehavior = .never
111 + webView.navigationDelegate = self
112 +
113 + // Improve rendering
114 + webView.isOpaque = false
115 + webView.backgroundColor = .systemBackground
116 +
117 + view.addSubview(webView)
118
119 NSLayoutConstraint.activate([
46 - // WebView constraints
47 - webView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
120 + webView.topAnchor.constraint(equalTo: navigationBar.bottomAnchor),
121 webView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
122 webView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
123 webView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
124 + ])
125 + }
126 +
127 + private func setupProgressIndicator() {
128 + // Modern progress bar
129 + progressView = UIProgressView(progressViewStyle: .bar)
130 + progressView.translatesAutoresizingMaskIntoConstraints = false
131 + progressView.progressTintColor = .systemBlue
132 + progressView.trackTintColor = .clear
133 + progressView.alpha = 0
134 + view.addSubview(progressView)
135
52 - // Close button constraints
53 - closeButton.topAnchor.constraint(
54 - equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 12),
55 - closeButton.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -12),
136 + NSLayoutConstraint.activate([
137 + progressView.topAnchor.constraint(equalTo: navigationBar.bottomAnchor),
138 + progressView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
139 + progressView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
140 + progressView.heightAnchor.constraint(equalToConstant: 2),
141 ])
142 +
143 + // Observe loading progress
144 + webView.addObserver(
145 + self, forKeyPath: #keyPath(WKWebView.estimatedProgress), options: .new, context: nil)
146 }
147
148 private func loadURL() {
149 guard let urlToLoad = URL(string: url) else {
61 - print("Invalid URL: \(url)")
150 + showErrorState()
151 return
152 }
153
65 - let request = URLRequest(url: urlToLoad)
154 + var request = URLRequest(url: urlToLoad)
155 + request.cachePolicy = .reloadIgnoringLocalCacheData
156 webView.load(request)
157 }
158
159 + private func showErrorState() {
160 + let errorLabel = UILabel()
161 + errorLabel.text = "Unable to load page"
162 + errorLabel.textColor = .secondaryLabel
163 + errorLabel.font = .systemFont(ofSize: 17, weight: .medium)
164 + errorLabel.textAlignment = .center
165 + errorLabel.translatesAutoresizingMaskIntoConstraints = false
166 + view.addSubview(errorLabel)
167 +
168 + NSLayoutConstraint.activate([
169 + errorLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
170 + errorLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
171 + ])
172 + }
173 +
174 + override func observeValue(
175 + forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?,
176 + context: UnsafeMutableRawPointer?
177 + ) {
178 + if keyPath == "estimatedProgress" {
179 + let progress = Float(webView.estimatedProgress)
180 +
181 + UIView.animate(withDuration: 0.2) {
182 + self.progressView.alpha = progress < 1.0 ? 1.0 : 0.0
183 + self.progressView.setProgress(progress, animated: true)
184 + }
185 + }
186 + }
187 +
188 @objc private func closeButtonTapped() {
189 dismiss(animated: true)
190 }
191 +
192 + deinit {
193 + webView.removeObserver(self, forKeyPath: #keyPath(WKWebView.estimatedProgress))
194 + }
195 +}
196 +
197 +// MARK: - WKNavigationDelegate
198 +extension NewViewController: WKNavigationDelegate {
199 +
200 + func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
201 + loadingIndicator.startAnimating()
202 + }
203 +
204 + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
205 + loadingIndicator.stopAnimating()
206 + }
207 +
208 + func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) {
209 + loadingIndicator.stopAnimating()
210 + print("Navigation failed: \(error.localizedDescription)")
211 + }
212 +
213 + func webView(
214 + _ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!,
215 + withError error: Error
216 + ) {
217 + loadingIndicator.stopAnimating()
218 + showErrorState()
219 + }
220 }
src/models.rs
+1
@@ -4,4 +4,5 @@ use serde::{Deserialize, Serialize};
4 #[serde(rename_all = "camelCase")]
5 pub struct OpenRequest {
6 pub url: String,
7 + pub title: Option<String>,
8 }