Skip to content

Commit c5721fe

Browse files
authored
fix(auth): emit local stored session as the initial session (#822)
* fix(auth): emit local stored session as the initial session * fix(auth): refresh token in background if stored session is expired * fix: use trait for allowing users to opt in new behavior
1 parent 70e8c18 commit c5721fe

File tree

3 files changed

+346
-41
lines changed

3 files changed

+346
-41
lines changed

[email protected]

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
// swift-tools-version:6.1
2+
// The swift-tools-version declares the minimum version of Swift required to build this package.
3+
4+
import Foundation
5+
import PackageDescription
6+
7+
let package = Package(
8+
name: "Supabase",
9+
platforms: [
10+
.iOS(.v13),
11+
.macCatalyst(.v13),
12+
.macOS(.v10_15),
13+
.watchOS(.v6),
14+
.tvOS(.v13),
15+
],
16+
products: [
17+
.library(name: "Auth", targets: ["Auth"]),
18+
.library(name: "Functions", targets: ["Functions"]),
19+
.library(name: "PostgREST", targets: ["PostgREST"]),
20+
.library(name: "Realtime", targets: ["Realtime"]),
21+
.library(name: "Storage", targets: ["Storage"]),
22+
.library(
23+
name: "Supabase",
24+
targets: ["Supabase", "Functions", "PostgREST", "Auth", "Realtime", "Storage"]
25+
),
26+
],
27+
traits: [
28+
.init(
29+
name: "EmitLocalSessionAsInitialSession",
30+
description: "Emits the local stored session as the initial session.",
31+
enabledTraits: []
32+
)
33+
],
34+
dependencies: [
35+
.package(url: "https://github.com/apple/swift-crypto.git", "1.0.0"..<"5.0.0"),
36+
.package(url: "https://github.com/apple/swift-http-types.git", from: "1.3.0"),
37+
.package(url: "https://github.com/pointfreeco/swift-clocks", from: "1.0.0"),
38+
.package(url: "https://github.com/pointfreeco/swift-concurrency-extras", from: "1.1.0"),
39+
.package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "1.3.2"),
40+
.package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.17.0"),
41+
.package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.2.2"),
42+
.package(url: "https://github.com/WeTransfer/Mocker", from: "3.0.0"),
43+
],
44+
targets: [
45+
.target(
46+
name: "Helpers",
47+
dependencies: [
48+
.product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"),
49+
.product(name: "HTTPTypes", package: "swift-http-types"),
50+
.product(name: "Clocks", package: "swift-clocks"),
51+
.product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"),
52+
]
53+
),
54+
.testTarget(
55+
name: "HelpersTests",
56+
dependencies: [
57+
.product(name: "CustomDump", package: "swift-custom-dump"),
58+
"Helpers",
59+
]
60+
),
61+
.target(
62+
name: "Auth",
63+
dependencies: [
64+
.product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"),
65+
.product(name: "Crypto", package: "swift-crypto"),
66+
"Helpers",
67+
]
68+
),
69+
.testTarget(
70+
name: "AuthTests",
71+
dependencies: [
72+
.product(name: "CustomDump", package: "swift-custom-dump"),
73+
.product(name: "InlineSnapshotTesting", package: "swift-snapshot-testing"),
74+
.product(name: "SnapshotTesting", package: "swift-snapshot-testing"),
75+
.product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"),
76+
"Auth",
77+
"Helpers",
78+
"TestHelpers",
79+
],
80+
exclude: [
81+
"__Snapshots__"
82+
],
83+
resources: [.process("Resources")]
84+
),
85+
.target(
86+
name: "Functions",
87+
dependencies: [
88+
"Helpers"
89+
]
90+
),
91+
.testTarget(
92+
name: "FunctionsTests",
93+
dependencies: [
94+
.product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"),
95+
.product(name: "InlineSnapshotTesting", package: "swift-snapshot-testing"),
96+
.product(name: "SnapshotTesting", package: "swift-snapshot-testing"),
97+
.product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"),
98+
"Functions",
99+
"Mocker",
100+
"TestHelpers",
101+
]
102+
),
103+
.testTarget(
104+
name: "IntegrationTests",
105+
dependencies: [
106+
.product(name: "CustomDump", package: "swift-custom-dump"),
107+
.product(name: "InlineSnapshotTesting", package: "swift-snapshot-testing"),
108+
.product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"),
109+
"Helpers",
110+
"Supabase",
111+
"TestHelpers",
112+
],
113+
resources: [
114+
.process("Fixtures"),
115+
.process("supabase"),
116+
]
117+
),
118+
.target(
119+
name: "PostgREST",
120+
dependencies: [
121+
.product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"),
122+
"Helpers",
123+
]
124+
),
125+
.testTarget(
126+
name: "PostgRESTTests",
127+
dependencies: [
128+
.product(name: "InlineSnapshotTesting", package: "swift-snapshot-testing"),
129+
.product(name: "SnapshotTesting", package: "swift-snapshot-testing"),
130+
"Helpers",
131+
"Mocker",
132+
"PostgREST",
133+
"TestHelpers",
134+
]
135+
),
136+
.target(
137+
name: "Realtime",
138+
dependencies: [
139+
.product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"),
140+
.product(name: "IssueReporting", package: "xctest-dynamic-overlay"),
141+
"Helpers",
142+
]
143+
),
144+
.testTarget(
145+
name: "RealtimeTests",
146+
dependencies: [
147+
.product(name: "CustomDump", package: "swift-custom-dump"),
148+
.product(name: "InlineSnapshotTesting", package: "swift-snapshot-testing"),
149+
.product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"),
150+
"PostgREST",
151+
"Realtime",
152+
"TestHelpers",
153+
]
154+
),
155+
.target(
156+
name: "Storage",
157+
dependencies: [
158+
"Helpers"
159+
]
160+
),
161+
.testTarget(
162+
name: "StorageTests",
163+
dependencies: [
164+
.product(name: "CustomDump", package: "swift-custom-dump"),
165+
.product(name: "InlineSnapshotTesting", package: "swift-snapshot-testing"),
166+
.product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"),
167+
"Mocker",
168+
"TestHelpers",
169+
"Storage",
170+
],
171+
resources: [
172+
.copy("sadcat.jpg"),
173+
.process("Fixtures"),
174+
]
175+
),
176+
.target(
177+
name: "Supabase",
178+
dependencies: [
179+
.product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"),
180+
.product(name: "IssueReporting", package: "xctest-dynamic-overlay"),
181+
"Auth",
182+
"Functions",
183+
"PostgREST",
184+
"Realtime",
185+
"Storage",
186+
]
187+
),
188+
.testTarget(
189+
name: "SupabaseTests",
190+
dependencies: [
191+
.product(name: "CustomDump", package: "swift-custom-dump"),
192+
.product(name: "InlineSnapshotTesting", package: "swift-snapshot-testing"),
193+
"Supabase",
194+
]
195+
),
196+
.target(
197+
name: "TestHelpers",
198+
dependencies: [
199+
.product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"),
200+
.product(name: "InlineSnapshotTesting", package: "swift-snapshot-testing"),
201+
.product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"),
202+
"Auth",
203+
"Mocker",
204+
]
205+
),
206+
],
207+
swiftLanguageModes: [.v5]
208+
)
209+
210+
for target in package.targets where !target.isTest {
211+
target.swiftSettings = [
212+
.enableUpcomingFeature("ExistentialAny"),
213+
.enableExperimentalFeature("StrictConcurrency"),
214+
]
215+
}

Sources/Auth/AuthClient.swift

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -218,9 +218,8 @@ public actor AuthClient {
218218
/// - Parameter listener: Block that executes when a new event is emitted.
219219
/// - Returns: A handle that can be used to manually unsubscribe.
220220
///
221-
/// - Note: This method blocks execution until the ``AuthChangeEvent/initialSession`` event is
222-
/// emitted. Although this operation is usually fast, in case of the current stored session being
223-
/// invalid, a call to the endpoint is necessary for refreshing the session.
221+
/// - Note: The session emitted in the ``AuthChangeEvent/initialSession`` event may have been expired
222+
/// since last launch, consider checking for ``Session/isExpired``. If this is the case, then expect a ``AuthChangeEvent/tokenRefreshed`` after.
224223
@discardableResult
225224
public func onAuthStateChange(
226225
_ listener: @escaping AuthStateChangeListener
@@ -1393,8 +1392,36 @@ public actor AuthClient {
13931392
}
13941393

13951394
private func emitInitialSession(forToken token: ObservationToken) async {
1396-
let session = try? await session
1397-
eventEmitter.emit(.initialSession, session: session, token: token)
1395+
#if EmitLocalSessionAsInitialSession
1396+
guard let currentSession else {
1397+
eventEmitter.emit(.initialSession, session: nil, token: token)
1398+
return
1399+
}
1400+
1401+
eventEmitter.emit(.initialSession, session: currentSession, token: token)
1402+
1403+
Task {
1404+
if currentSession.isExpired {
1405+
_ = try? await sessionManager.refreshSession(currentSession.refreshToken)
1406+
// No need to emit `tokenRefreshed` nor `signOut` event since the `refreshSession` does it already.
1407+
}
1408+
}
1409+
#else
1410+
let session = try? await session
1411+
eventEmitter.emit(.initialSession, session: session, token: token)
1412+
1413+
logger?.warning(
1414+
"""
1415+
Initial session emitted after attempting to refresh the local stored session.
1416+
This is incorrect behavior and will be fixed in the next major release since it’s a breaking change.
1417+
For now, if you want to opt-in to the new behavior, add the trait `EmitLocalSessionAsInitialSession` to your Package.swift file when importing the Supabase dependency.
1418+
The new behavior ensures that the locally stored session is always emitted, regardless of its validity or expiration.
1419+
If you rely on the initial session to opt users in, you need to add an additional check for `session.isExpired` in the session.
1420+
1421+
Check https://github.com/supabase/supabase-swift/pull/822 for more information.
1422+
"""
1423+
)
1424+
#endif
13981425
}
13991426

14001427
nonisolated private func prepareForPKCE() -> (

0 commit comments

Comments
 (0)