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
}