Skip to content

Commit b3c08fc

Browse files
authored
Merge pull request #1 from darrarski/feature/auth
Authorization
2 parents 7306f12 + ee41bf0 commit b3c08fc

File tree

20 files changed

+1234
-18
lines changed

20 files changed

+1234
-18
lines changed

Example/DropboxClientExampleApp/Dependencies.swift

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,25 @@ import Dependencies
22
import DropboxClient
33

44
extension DropboxClient.Client: DependencyKey {
5-
public static let liveValue = Client.live()
6-
public static let previewValue = Client()
5+
public static let liveValue = Client.live(
6+
config: Config(
7+
appKey: "3wmt82ponra7r9v",
8+
redirectURI: "db-3wmt82ponra7r9v://swift-dropbox-example"
9+
)
10+
)
11+
12+
public static let previewValue: Client = {
13+
let isSignedIn = CurrentValueAsyncSequence(false)
14+
return Client(
15+
auth: .init(
16+
isSignedIn: { await isSignedIn.value },
17+
isSignedInStream: { isSignedIn.eraseToStream() },
18+
signIn: { await isSignedIn.setValue(true) },
19+
handleRedirect: { _ in },
20+
signOut: { await isSignedIn.setValue(false) }
21+
)
22+
)
23+
}()
724
}
825

926
extension DependencyValues {

Example/DropboxClientExampleApp/ExampleView.swift

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,56 @@ import SwiftUI
66
struct ExampleView: View {
77
@Dependency(\.dropboxClient) var client
88
let log = Logger(label: Bundle.main.bundleIdentifier!)
9+
@State var isSignedIn = false
910

1011
var body: some View {
11-
Button {
12-
log.error("Not implemented")
13-
} label: {
14-
Text(String(describing: client))
12+
Form {
13+
authSection
14+
}
15+
.textSelection(.enabled)
16+
.navigationTitle("Example")
17+
.task {
18+
for await isSignedIn in client.auth.isSignedInStream() {
19+
self.isSignedIn = isSignedIn
20+
}
21+
}
22+
.onOpenURL { url in
23+
Task<Void, Never> {
24+
do {
25+
try await client.auth.handleRedirect(url)
26+
} catch {
27+
log.error("Auth.HandleRedirect failure", metadata: [
28+
"error": "\(error)",
29+
"localizedDescription": "\(error.localizedDescription)"
30+
])
31+
}
32+
}
33+
}
34+
}
35+
36+
var authSection: some View {
37+
Section("Auth") {
38+
if !isSignedIn {
39+
Text("You are signed out")
40+
41+
Button {
42+
Task {
43+
await client.auth.signIn()
44+
}
45+
} label: {
46+
Text("Sign In")
47+
}
48+
} else {
49+
Text("You are signed in")
50+
51+
Button(role: .destructive) {
52+
Task {
53+
await client.auth.signOut()
54+
}
55+
} label: {
56+
Text("Sign Out")
57+
}
58+
}
1559
}
1660
}
1761
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>CFBundleURLTypes</key>
6+
<array>
7+
<dict>
8+
<key>CFBundleTypeRole</key>
9+
<string>Editor</string>
10+
<key>CFBundleURLSchemes</key>
11+
<array>
12+
<string>db-3wmt82ponra7r9v</string>
13+
</array>
14+
</dict>
15+
</array>
16+
</dict>
17+
</plist>

Example/Example.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
31FB21472A57552E00B9FD8C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
2525
31FB21542A57560A00B9FD8C /* ExampleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExampleView.swift; sourceTree = "<group>"; };
2626
31FB21562A57561D00B9FD8C /* Dependencies.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dependencies.swift; sourceTree = "<group>"; };
27+
31FB21662A57633500B9FD8C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
2728
/* End PBXFileReference section */
2829

2930
/* Begin PBXFrameworksBuildPhase section */
@@ -64,6 +65,7 @@
6465
31FB21432A57552E00B9FD8C /* App.swift */,
6566
31FB21542A57560A00B9FD8C /* ExampleView.swift */,
6667
31FB21562A57561D00B9FD8C /* Dependencies.swift */,
68+
31FB21662A57633500B9FD8C /* Info.plist */,
6769
31FB21472A57552E00B9FD8C /* Assets.xcassets */,
6870
);
6971
path = DropboxClientExampleApp;
@@ -282,6 +284,7 @@
282284
CURRENT_PROJECT_VERSION = 0;
283285
ENABLE_PREVIEWS = YES;
284286
GENERATE_INFOPLIST_FILE = YES;
287+
INFOPLIST_FILE = DropboxClientExampleApp/Info.plist;
285288
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
286289
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
287290
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
@@ -309,6 +312,7 @@
309312
CURRENT_PROJECT_VERSION = 0;
310313
ENABLE_PREVIEWS = YES;
311314
GENERATE_INFOPLIST_FILE = YES;
315+
INFOPLIST_FILE = DropboxClientExampleApp/Info.plist;
312316
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
313317
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
314318
INFOPLIST_KEY_UILaunchScreen_Generation = YES;

README.md

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,69 @@
55

66
Basic Dropbox HTTP API client that does not depend on Dropbox's SDK. No external dependencies.
77

8+
- Authorize access
9+
- ...
10+
811
## 📖 Usage
912

1013
Use [Swift Package Manager](https://swift.org/package-manager/) to add the `DropboxClient` library as a dependency to your project.
1114

12-
*TBD*
15+
Register your application in [Dropbox App Console](https://www.dropbox.com/developers/apps).
16+
17+
Configure your app so that it can handle sign-in redirects. For an iOS app, you can do it by adding or modifying `CFBundleURLTypes` in `Info.plist`:
18+
19+
```xml
20+
<key>CFBundleURLTypes</key>
21+
<array>
22+
<dict>
23+
<key>CFBundleTypeRole</key>
24+
<string>Editor</string>
25+
<key>CFBundleURLName</key>
26+
<string></string>
27+
<key>CFBundleURLSchemes</key>
28+
<array>
29+
<string>db-abcd1234</string>
30+
</array>
31+
</dict>
32+
</array>
33+
```
34+
35+
Create the client:
36+
37+
```swift
38+
import DropboxClient
39+
40+
let client = DropboxClient.Client.live(
41+
config: .init(
42+
appKey: "abcd1234",
43+
redirectURI: "db-abcd1234://my-app"
44+
)
45+
)
46+
```
47+
48+
Make sure the `redirectURI` contains the scheme defined earlier.
49+
50+
The package provides a basic implementation for storing vulnerable data securely in the keychain. Optionally, you can provide your own, custom implementation of a keychain, instead of using the default one.
51+
52+
```swift
53+
import DropboxClient
54+
55+
let keychain = DropboxClient.Keychain(
56+
loadCredentials: { () async -> DropboxClient.Credentials? in
57+
// load from secure storage and return
58+
},
59+
saveCredentials: { (DropboxClient.Credentials) async -> Void in
60+
// save in secure storage
61+
},
62+
deleteCredentials: { () async -> Void in
63+
// delete from secure storage
64+
}
65+
)
66+
let client = DropboxClient.Client.live(
67+
config: .init(...),
68+
keychain: keychain
69+
)
70+
```
1371

1472
### ▶️ Example
1573

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
/// MIT License
2+
///
3+
/// Copyright (c) 2022 Point-Free, Inc.
4+
///
5+
/// Permission is hereby granted, free of charge, to any person obtaining a copy
6+
/// of this software and associated documentation files (the "Software"), to deal
7+
/// in the Software without restriction, including without limitation the rights
8+
/// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
/// copies of the Software, and to permit persons to whom the Software is
10+
/// furnished to do so, subject to the following conditions:
11+
///
12+
/// The above copyright notice and this permission notice shall be included in all
13+
/// copies or substantial portions of the Software.
14+
///
15+
/// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
/// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
/// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
/// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
/// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
/// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
/// SOFTWARE.
22+
23+
/// A generic wrapper for isolating a mutable value to an actor.
24+
///
25+
/// This type is most useful when writing tests for when you want to inspect what happens inside an
26+
/// effect. For example, suppose you have a feature such that when a button is tapped you track some
27+
/// analytics:
28+
///
29+
/// ```swift
30+
/// class FeatureModel: ObservableObject {
31+
/// @Dependency(\.analytics) var analytics
32+
/// // ...
33+
/// func buttonTapped() {
34+
/// // ...
35+
/// await self.analytics.track("Button tapped")
36+
/// }
37+
/// }
38+
/// ```
39+
///
40+
/// Then, in tests we can construct an analytics client that appends events to a mutable array
41+
/// rather than actually sending events to an analytics server. However, in order to do this in a
42+
/// safe way we should use an actor, and `ActorIsolated` makes this easy:
43+
///
44+
/// ```swift
45+
/// func testAnalytics() async {
46+
/// let events = ActorIsolated<[String]>([])
47+
/// let model = withDependencies {
48+
/// $0.analytics = AnalyticsClient(
49+
/// track: { event in await events.withValue { $0.append(event) } }
50+
/// )
51+
/// } operation: {
52+
/// FeatureModel()
53+
/// }
54+
///
55+
/// model.buttonTapped()
56+
/// await events.withValue {
57+
/// XCTAssertEqual($0, ["Button tapped"])
58+
/// }
59+
/// }
60+
/// ```
61+
///
62+
/// To synchronously isolate a value, see ``LockIsolated``.
63+
@dynamicMemberLookup
64+
final actor ActorIsolated<Value> {
65+
/// The actor-isolated value.
66+
var value: Value
67+
68+
/// Initializes actor-isolated state around a value.
69+
///
70+
/// - Parameter value: A value to isolate in an actor.
71+
init(_ value: @autoclosure @Sendable () throws -> Value) rethrows {
72+
self.value = try value()
73+
}
74+
75+
subscript<Subject>(dynamicMember keyPath: KeyPath<Value, Subject>) -> Subject {
76+
self.value[keyPath: keyPath]
77+
}
78+
79+
/// Perform an operation with isolated access to the underlying value.
80+
///
81+
/// Useful for modifying a value in a single transaction.
82+
///
83+
/// ```swift
84+
/// // Isolate an integer for concurrent read/write access:
85+
/// let count = ActorIsolated(0)
86+
///
87+
/// func increment() async {
88+
/// // Safely increment it:
89+
/// await self.count.withValue { $0 += 1 }
90+
/// }
91+
/// ```
92+
///
93+
/// > Tip: Because XCTest assertions don't play nicely with Swift concurrency, `withValue` also
94+
/// > provides a handy interface to peek at an actor-isolated value and assert against it:
95+
/// >
96+
/// > ```swift
97+
/// > let didOpenSettings = ActorIsolated(false)
98+
/// > let model = withDependencies {
99+
/// > $0.openSettings = { await didOpenSettings.setValue(true) }
100+
/// > } operation: {
101+
/// > FeatureModel()
102+
/// > }
103+
/// > await model.settingsButtonTapped()
104+
/// > await didOpenSettings.withValue { XCTAssertTrue($0) }
105+
/// > ```
106+
///
107+
/// - Parameters: operation: An operation to be performed on the actor with the underlying value.
108+
/// - Returns: The result of the operation.
109+
func withValue<T>(
110+
_ operation: @Sendable (inout Value) throws -> T
111+
) rethrows -> T {
112+
var value = self.value
113+
defer { self.value = value }
114+
return try operation(&value)
115+
}
116+
117+
/// Overwrite the isolated value with a new value.
118+
///
119+
/// ```swift
120+
/// // Isolate an integer for concurrent read/write access:
121+
/// let count = ActorIsolated(0)
122+
///
123+
/// func reset() async {
124+
/// // Reset it:
125+
/// await self.count.setValue(0)
126+
/// }
127+
/// ```
128+
///
129+
/// > Tip: Use ``withValue(_:)-805p`` instead of `setValue` if the value being set is derived from
130+
/// > the current value. This isolates the entire transaction and avoids data races between
131+
/// > reading and writing the value.
132+
///
133+
/// - Parameter newValue: The value to replace the current isolated value with.
134+
func setValue(_ newValue: @autoclosure @Sendable () throws -> Value) rethrows {
135+
self.value = try newValue()
136+
}
137+
}

0 commit comments

Comments
 (0)