@superfan-app/spotify-auth 0.1.42 → 0.1.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ios/.swiftlint.yml +27 -0
- package/ios/SpotifyAuthAuth.swift +48 -67
- package/ios/SpotifyAuthModule.swift +6 -6
- package/ios/SpotifyOAuthView.swift +72 -53
- package/package.json +3 -2
- package/scripts/check-ios.sh +19 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
disabled_rules:
|
|
2
|
+
- trailing_whitespace
|
|
3
|
+
- line_length
|
|
4
|
+
- function_body_length
|
|
5
|
+
- file_length
|
|
6
|
+
- type_body_length
|
|
7
|
+
|
|
8
|
+
opt_in_rules:
|
|
9
|
+
- force_unwrapping
|
|
10
|
+
- empty_count
|
|
11
|
+
- empty_string
|
|
12
|
+
|
|
13
|
+
included:
|
|
14
|
+
- .
|
|
15
|
+
|
|
16
|
+
excluded:
|
|
17
|
+
- Pods
|
|
18
|
+
- Carthage
|
|
19
|
+
- vendor
|
|
20
|
+
|
|
21
|
+
analyzer_rules:
|
|
22
|
+
- explicit_self
|
|
23
|
+
- unused_import
|
|
24
|
+
- unused_declaration
|
|
25
|
+
|
|
26
|
+
force_cast: warning
|
|
27
|
+
force_try: warning
|
|
@@ -313,7 +313,7 @@ final class SpotifyAuthAuth: NSObject, SPTSessionManagerDelegate, SpotifyOAuthVi
|
|
|
313
313
|
let bodyString = params.map { "\($0)=\($1)" }.joined(separator: "&")
|
|
314
314
|
request.httpBody = bodyString.data(using: .utf8)
|
|
315
315
|
|
|
316
|
-
let task = URLSession.shared.dataTask(with: request) { [weak self] data,
|
|
316
|
+
let task = URLSession.shared.dataTask(with: request) { [weak self] data, _, error in
|
|
317
317
|
if let error = error {
|
|
318
318
|
self?.handleError(error, context: "token_refresh")
|
|
319
319
|
return
|
|
@@ -550,50 +550,29 @@ final class SpotifyAuthAuth: NSObject, SPTSessionManagerDelegate, SpotifyOAuthVi
|
|
|
550
550
|
// MARK: - Helpers
|
|
551
551
|
|
|
552
552
|
private func stringToScope(scopeString: String) -> SPTScope? {
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
case "user-top-read":
|
|
577
|
-
return .userTopRead
|
|
578
|
-
case "ugc-image-upload":
|
|
579
|
-
return .ugcImageUpload
|
|
580
|
-
case "streaming":
|
|
581
|
-
return .streaming
|
|
582
|
-
case "app-remote-control":
|
|
583
|
-
return .appRemoteControl
|
|
584
|
-
case "user-read-playback-state":
|
|
585
|
-
return .userReadPlaybackState
|
|
586
|
-
case "user-modify-playback-state":
|
|
587
|
-
return .userModifyPlaybackState
|
|
588
|
-
case "user-read-currently-playing":
|
|
589
|
-
return .userReadCurrentlyPlaying
|
|
590
|
-
case "user-read-recently-played":
|
|
591
|
-
return .userReadRecentlyPlayed
|
|
592
|
-
case "openid":
|
|
593
|
-
return .openid
|
|
594
|
-
default:
|
|
595
|
-
return nil
|
|
596
|
-
}
|
|
553
|
+
let scopeMapping: [String: SPTScope] = [
|
|
554
|
+
"playlist-read-private": .playlistReadPrivate,
|
|
555
|
+
"playlist-read-collaborative": .playlistReadCollaborative,
|
|
556
|
+
"playlist-modify-public": .playlistModifyPublic,
|
|
557
|
+
"playlist-modify-private": .playlistModifyPrivate,
|
|
558
|
+
"user-follow-read": .userFollowRead,
|
|
559
|
+
"user-follow-modify": .userFollowModify,
|
|
560
|
+
"user-library-read": .userLibraryRead,
|
|
561
|
+
"user-library-modify": .userLibraryModify,
|
|
562
|
+
"user-read-birthdate": .userReadBirthDate,
|
|
563
|
+
"user-read-email": .userReadEmail,
|
|
564
|
+
"user-read-private": .userReadPrivate,
|
|
565
|
+
"user-top-read": .userTopRead,
|
|
566
|
+
"ugc-image-upload": .ugcImageUpload,
|
|
567
|
+
"streaming": .streaming,
|
|
568
|
+
"app-remote-control": .appRemoteControl,
|
|
569
|
+
"user-read-playback-state": .userReadPlaybackState,
|
|
570
|
+
"user-modify-playback-state": .userModifyPlaybackState,
|
|
571
|
+
"user-read-currently-playing": .userReadCurrentlyPlaying,
|
|
572
|
+
"user-read-recently-played": .userReadRecentlyPlayed,
|
|
573
|
+
"openid": .openid
|
|
574
|
+
]
|
|
575
|
+
return scopeMapping[scopeString]
|
|
597
576
|
}
|
|
598
577
|
|
|
599
578
|
private func handleError(_ error: Error, context: String) {
|
|
@@ -674,27 +653,29 @@ final class SpotifyAuthAuth: NSObject, SPTSessionManagerDelegate, SpotifyOAuthVi
|
|
|
674
653
|
extension SPTScope {
|
|
675
654
|
/// Converts an SPTScope value into an array of scope strings.
|
|
676
655
|
func scopesToStringArray() -> [String] {
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
656
|
+
let scopeMapping: [(SPTScope, String)] = [
|
|
657
|
+
(.playlistReadPrivate, "playlist-read-private"),
|
|
658
|
+
(.playlistReadCollaborative, "playlist-read-collaborative"),
|
|
659
|
+
(.playlistModifyPublic, "playlist-modify-public"),
|
|
660
|
+
(.playlistModifyPrivate, "playlist-modify-private"),
|
|
661
|
+
(.userFollowRead, "user-follow-read"),
|
|
662
|
+
(.userFollowModify, "user-follow-modify"),
|
|
663
|
+
(.userLibraryRead, "user-library-read"),
|
|
664
|
+
(.userLibraryModify, "user-library-modify"),
|
|
665
|
+
(.userReadBirthDate, "user-read-birthdate"),
|
|
666
|
+
(.userReadEmail, "user-read-email"),
|
|
667
|
+
(.userReadPrivate, "user-read-private"),
|
|
668
|
+
(.userTopRead, "user-top-read"),
|
|
669
|
+
(.ugcImageUpload, "ugc-image-upload"),
|
|
670
|
+
(.streaming, "streaming"),
|
|
671
|
+
(.appRemoteControl, "app-remote-control"),
|
|
672
|
+
(.userReadPlaybackState, "user-read-playback-state"),
|
|
673
|
+
(.userModifyPlaybackState, "user-modify-playback-state"),
|
|
674
|
+
(.userReadCurrentlyPlaying, "user-read-currently-playing"),
|
|
675
|
+
(.userReadRecentlyPlayed, "user-read-recently-played"),
|
|
676
|
+
(.openid, "openid")
|
|
677
|
+
]
|
|
678
|
+
|
|
679
|
+
return scopeMapping.filter { contains($0.0) }.map { $0.1 }
|
|
699
680
|
}
|
|
700
681
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import ExpoModulesCore
|
|
2
2
|
import SpotifyiOS
|
|
3
3
|
|
|
4
|
-
let
|
|
4
|
+
let spotifyAuthorizationEventName = "onSpotifyAuth"
|
|
5
5
|
|
|
6
6
|
#if DEBUG
|
|
7
7
|
func secureLog(_ message: String, sensitive: Bool = false) {
|
|
@@ -44,11 +44,11 @@ public class SpotifyAuthModule: Module {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
Constants([
|
|
47
|
-
"AuthEventName":
|
|
47
|
+
"AuthEventName": spotifyAuthorizationEventName
|
|
48
48
|
])
|
|
49
49
|
|
|
50
50
|
// Defines event names that the module can send to JavaScript.
|
|
51
|
-
Events(
|
|
51
|
+
Events(spotifyAuthorizationEventName)
|
|
52
52
|
|
|
53
53
|
// Called when JS starts observing the event.
|
|
54
54
|
OnStartObserving {
|
|
@@ -114,7 +114,7 @@ public class SpotifyAuthModule: Module {
|
|
|
114
114
|
"token": token,
|
|
115
115
|
"error": NSNull() // Use NSNull() instead of nil.
|
|
116
116
|
]
|
|
117
|
-
sendEvent(
|
|
117
|
+
sendEvent(spotifyAuthorizationEventName, eventData)
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
@objc
|
|
@@ -125,7 +125,7 @@ public class SpotifyAuthModule: Module {
|
|
|
125
125
|
"token": NSNull(), // Use NSNull() instead of nil.
|
|
126
126
|
"error": NSNull() // Use NSNull() instead of nil.
|
|
127
127
|
]
|
|
128
|
-
sendEvent(
|
|
128
|
+
sendEvent(spotifyAuthorizationEventName, eventData)
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
@objc
|
|
@@ -157,7 +157,7 @@ public class SpotifyAuthModule: Module {
|
|
|
157
157
|
"token": NSNull(),
|
|
158
158
|
"error": errorData
|
|
159
159
|
]
|
|
160
|
-
sendEvent(
|
|
160
|
+
sendEvent(spotifyAuthorizationEventName, eventData)
|
|
161
161
|
}
|
|
162
162
|
|
|
163
163
|
private func mapSpotifyError(_ error: SpotifyAuthError) -> [String: Any] {
|
|
@@ -41,7 +41,8 @@ class SpotifyOAuthView: ExpoView {
|
|
|
41
41
|
private var isAuthenticating = false
|
|
42
42
|
private var expectedRedirectScheme: String?
|
|
43
43
|
private var authTimeout: Timer?
|
|
44
|
-
private static let
|
|
44
|
+
private static let authTimeoutInterval: TimeInterval = 300 // 5 minutes
|
|
45
|
+
private var observerToken: NSKeyValueObservation?
|
|
45
46
|
|
|
46
47
|
required init(appContext: AppContext? = nil) {
|
|
47
48
|
// Generate a random state string for CSRF protection
|
|
@@ -56,50 +57,68 @@ class SpotifyOAuthView: ExpoView {
|
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
private func setupWebView() {
|
|
60
|
+
// Ensure we're on the main thread for UI setup
|
|
61
|
+
guard Thread.isMainThread else {
|
|
62
|
+
DispatchQueue.main.async { [weak self] in
|
|
63
|
+
self?.setupWebView()
|
|
64
|
+
}
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
|
|
59
68
|
// Create a configuration that prevents data persistence
|
|
60
|
-
let config =
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
let dataStore = WKWebsiteDataStore.nonPersistent()
|
|
67
|
-
config.websiteDataStore = dataStore
|
|
68
|
-
|
|
69
|
-
webView = WKWebView(frame: .zero, configuration: config)
|
|
70
|
-
webView.navigationDelegate = self
|
|
71
|
-
webView.allowsBackForwardNavigationGestures = true
|
|
72
|
-
webView.customUserAgent = "SpotifyAuth-iOS/1.0" // Custom UA to identify our app
|
|
73
|
-
|
|
74
|
-
// Add loading indicator
|
|
75
|
-
let activityIndicator = UIActivityIndicatorView(style: .medium)
|
|
76
|
-
activityIndicator.translatesAutoresizingMaskIntoConstraints = false
|
|
77
|
-
activityIndicator.hidesWhenStopped = true
|
|
78
|
-
|
|
79
|
-
addSubview(webView)
|
|
80
|
-
addSubview(activityIndicator)
|
|
81
|
-
|
|
82
|
-
// Setup constraints
|
|
83
|
-
webView.translatesAutoresizingMaskIntoConstraints = false
|
|
84
|
-
NSLayoutConstraint.activate([
|
|
85
|
-
webView.topAnchor.constraint(equalTo: topAnchor),
|
|
86
|
-
webView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
87
|
-
webView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
88
|
-
webView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
69
|
+
let config: WKWebViewConfiguration = {
|
|
70
|
+
let configuration = WKWebViewConfiguration()
|
|
71
|
+
configuration.processPool = WKProcessPool() // Create a new process pool
|
|
72
|
+
let prefs = WKWebpagePreferences()
|
|
73
|
+
prefs.allowsContentJavaScript = true
|
|
74
|
+
configuration.defaultWebpagePreferences = prefs
|
|
89
75
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
76
|
+
// Ensure cookies and data are not persisted
|
|
77
|
+
let dataStore = WKWebsiteDataStore.nonPersistent()
|
|
78
|
+
configuration.websiteDataStore = dataStore
|
|
79
|
+
return configuration
|
|
80
|
+
}()
|
|
81
|
+
|
|
82
|
+
// Initialize webview on main thread with error handling
|
|
83
|
+
do {
|
|
84
|
+
webView = WKWebView(frame: .zero, configuration: config)
|
|
85
|
+
webView.navigationDelegate = self
|
|
86
|
+
webView.allowsBackForwardNavigationGestures = true
|
|
87
|
+
webView.customUserAgent = "SpotifyAuth-iOS/1.0" // Custom UA to identify our app
|
|
88
|
+
|
|
89
|
+
// Add loading indicator
|
|
90
|
+
let activityIndicator = UIActivityIndicatorView(style: .medium)
|
|
91
|
+
activityIndicator.translatesAutoresizingMaskIntoConstraints = false
|
|
92
|
+
activityIndicator.hidesWhenStopped = true
|
|
93
|
+
|
|
94
|
+
addSubview(webView)
|
|
95
|
+
addSubview(activityIndicator)
|
|
96
|
+
|
|
97
|
+
// Setup constraints
|
|
98
|
+
webView.translatesAutoresizingMaskIntoConstraints = false
|
|
99
|
+
NSLayoutConstraint.activate([
|
|
100
|
+
webView.topAnchor.constraint(equalTo: topAnchor),
|
|
101
|
+
webView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
102
|
+
webView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
103
|
+
webView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
104
|
+
|
|
105
|
+
activityIndicator.centerXAnchor.constraint(equalTo: centerXAnchor),
|
|
106
|
+
activityIndicator.centerYAnchor.constraint(equalTo: centerYAnchor)
|
|
107
|
+
])
|
|
108
|
+
|
|
109
|
+
// Setup modern KVO observation
|
|
110
|
+
observerToken = webView.observe(\.isLoading, options: [.new]) { [weak self] _, _ in
|
|
111
|
+
if let activityIndicator = self?.subviews.first(where: { $0 is UIActivityIndicatorView }) as? UIActivityIndicatorView {
|
|
112
|
+
if self?.webView.isLoading == true {
|
|
113
|
+
activityIndicator.startAnimating()
|
|
114
|
+
} else {
|
|
115
|
+
activityIndicator.stopAnimating()
|
|
116
|
+
}
|
|
117
|
+
}
|
|
102
118
|
}
|
|
119
|
+
} catch {
|
|
120
|
+
secureLog("Failed to setup WebView: \(error.localizedDescription)")
|
|
121
|
+
delegate?.oauthView(self, didFailWithError: SpotifyOAuthError.authorizationError("Failed to initialize web view"))
|
|
103
122
|
}
|
|
104
123
|
}
|
|
105
124
|
|
|
@@ -122,20 +141,20 @@ class SpotifyOAuthView: ExpoView {
|
|
|
122
141
|
WKWebsiteDataStore.default().removeData(
|
|
123
142
|
ofTypes: [WKWebsiteDataTypeCookies, WKWebsiteDataTypeSessionStorage],
|
|
124
143
|
modifiedSince: Date(timeIntervalSince1970: 0)
|
|
125
|
-
) {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
144
|
+
) { }
|
|
145
|
+
|
|
146
|
+
self.initiateAuthRequest(
|
|
147
|
+
clientId: clientId,
|
|
148
|
+
redirectUri: redirectUri,
|
|
149
|
+
scopes: scopes,
|
|
150
|
+
showDialog: showDialog,
|
|
151
|
+
campaign: campaign
|
|
152
|
+
)
|
|
134
153
|
}
|
|
135
154
|
|
|
136
155
|
private func startAuthTimeout() {
|
|
137
156
|
authTimeout?.invalidate()
|
|
138
|
-
authTimeout = Timer.scheduledTimer(withTimeInterval: Self.
|
|
157
|
+
authTimeout = Timer.scheduledTimer(withTimeInterval: Self.authTimeoutInterval, repeats: false) { [weak self] _ in
|
|
139
158
|
self?.handleTimeout()
|
|
140
159
|
}
|
|
141
160
|
}
|
|
@@ -192,7 +211,7 @@ class SpotifyOAuthView: ExpoView {
|
|
|
192
211
|
}
|
|
193
212
|
|
|
194
213
|
deinit {
|
|
195
|
-
|
|
214
|
+
observerToken?.invalidate()
|
|
196
215
|
authTimeout?.invalidate()
|
|
197
216
|
}
|
|
198
217
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@superfan-app/spotify-auth",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.44",
|
|
4
4
|
"description": "Spotify OAuth module for Expo",
|
|
5
5
|
"main": "src/index.tsx",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"test": "expo-module test",
|
|
13
13
|
"prepare": "expo-module prepare",
|
|
14
14
|
"prepublishOnly": "expo-module prepublishOnly",
|
|
15
|
-
"expo-module": "expo-module"
|
|
15
|
+
"expo-module": "expo-module",
|
|
16
|
+
"check-ios": "./scripts/check-ios.sh"
|
|
16
17
|
},
|
|
17
18
|
"keywords": [
|
|
18
19
|
"react-native",
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# Exit on error
|
|
4
|
+
set -e
|
|
5
|
+
|
|
6
|
+
echo "Checking iOS code..."
|
|
7
|
+
|
|
8
|
+
# Install SwiftLint if not installed
|
|
9
|
+
if ! command -v swiftlint &> /dev/null; then
|
|
10
|
+
echo "SwiftLint not found. Installing..."
|
|
11
|
+
brew install swiftlint
|
|
12
|
+
fi
|
|
13
|
+
|
|
14
|
+
# Run SwiftLint
|
|
15
|
+
cd ios
|
|
16
|
+
echo "Running SwiftLint..."
|
|
17
|
+
swiftlint lint --quiet
|
|
18
|
+
|
|
19
|
+
echo "iOS checks completed successfully!"
|