@superfan-app/spotify-auth 0.1.74 → 0.1.75
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.
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
package expo.modules.spotifyauth
|
|
2
2
|
|
|
3
|
+
import android.app.Activity
|
|
3
4
|
import android.content.Intent
|
|
4
|
-
import android.content.pm.ActivityInfo
|
|
5
5
|
import android.content.pm.PackageManager
|
|
6
6
|
import android.net.Uri
|
|
7
7
|
import android.os.Handler
|
|
@@ -24,9 +24,9 @@ import java.util.concurrent.Executors
|
|
|
24
24
|
/**
|
|
25
25
|
* Core Spotify authentication logic for Android, mirroring the iOS SpotifyAuthAuth class.
|
|
26
26
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
27
|
+
* Uses browser-based auth via Spotify's AuthorizationClient.openLoginInBrowser().
|
|
28
|
+
* On Android, app-switch is disabled regardless of whether the Spotify app is installed.
|
|
29
|
+
* Auth results are delivered via onNewIntent (handled by handleNewIntent()).
|
|
30
30
|
*
|
|
31
31
|
* Token exchange and refresh are handled via the backend token swap/refresh URLs,
|
|
32
32
|
* matching the iOS implementation.
|
|
@@ -193,9 +193,9 @@ class SpotifyAuthAuth private constructor(private val appContext: AppContext) {
|
|
|
193
193
|
// region Authentication Flow
|
|
194
194
|
|
|
195
195
|
/**
|
|
196
|
-
* Initiate the Spotify authorization flow.
|
|
197
|
-
*
|
|
198
|
-
*
|
|
196
|
+
* Initiate the Spotify authorization flow via the system browser.
|
|
197
|
+
* On Android, app-switch is always bypassed. The auth result is delivered
|
|
198
|
+
* via onNewIntent, handled by handleNewIntent().
|
|
199
199
|
*/
|
|
200
200
|
fun initAuth(config: AuthorizeConfig) {
|
|
201
201
|
secureLog("initAuth called with showDialog=${config.showDialog}")
|
|
@@ -224,13 +224,7 @@ class SpotifyAuthAuth private constructor(private val appContext: AppContext) {
|
|
|
224
224
|
val redirectUri = redirectURL
|
|
225
225
|
val scopeArray = scopes.toTypedArray()
|
|
226
226
|
|
|
227
|
-
val spotifyInstalled = isSpotifyInstalled()
|
|
228
227
|
secureLog("Configuration - ClientID: ${clientId.take(8)}..., RedirectURI: $redirectUri, Scopes: ${scopeArray.size}")
|
|
229
|
-
Log.d(TAG, "Spotify app installed: $spotifyInstalled (will use ${if (spotifyInstalled) "app-switch" else "WebView"} auth)")
|
|
230
|
-
|
|
231
|
-
if (!spotifyInstalled) {
|
|
232
|
-
Log.w(TAG, "Spotify app not detected. Will use WebView fallback. If WebView fails, check package visibility in AndroidManifest (<queries> tag)")
|
|
233
|
-
}
|
|
234
228
|
|
|
235
229
|
if (scopeArray.isEmpty()) {
|
|
236
230
|
Log.e(TAG, "No valid scopes found in configuration")
|
|
@@ -262,40 +256,19 @@ class SpotifyAuthAuth private constructor(private val appContext: AppContext) {
|
|
|
262
256
|
|
|
263
257
|
val request = builder.build()
|
|
264
258
|
|
|
265
|
-
secureLog("Opening Spotify authorization
|
|
259
|
+
secureLog("Opening Spotify authorization in browser")
|
|
266
260
|
|
|
267
|
-
// ===
|
|
261
|
+
// === SPOTIFY AUTH DEBUG ===
|
|
268
262
|
Log.d(TAG, "=== SPOTIFY AUTH DEBUG ===")
|
|
269
|
-
Log.d(TAG, "Auth flow type:
|
|
263
|
+
Log.d(TAG, "Auth flow type: BROWSER (app-switch disabled on Android)")
|
|
270
264
|
Log.d(TAG, "Client ID: ${clientId.take(10)}...")
|
|
271
265
|
Log.d(TAG, "Redirect URI: $redirectUri")
|
|
272
266
|
Log.d(TAG, "Response Type: CODE")
|
|
273
267
|
Log.d(TAG, "Scopes: ${scopeArray.joinToString(",")}")
|
|
274
268
|
Log.d(TAG, "Package name: ${appContext.reactContext?.packageName}")
|
|
275
269
|
Log.d(TAG, "Activity: ${activity.javaClass.name}")
|
|
276
|
-
val launchMode = activity.packageManager.getActivityInfo(activity.componentName, 0).launchMode
|
|
277
|
-
Log.d(TAG, "Activity launchMode: $launchMode")
|
|
278
270
|
Log.d(TAG, "========================")
|
|
279
271
|
|
|
280
|
-
if (launchMode == ActivityInfo.LAUNCH_SINGLE_TASK || launchMode == ActivityInfo.LAUNCH_SINGLE_INSTANCE) {
|
|
281
|
-
val modeName = if (launchMode == ActivityInfo.LAUNCH_SINGLE_TASK) "singleTask" else "singleInstance"
|
|
282
|
-
Log.e(TAG, "")
|
|
283
|
-
Log.e(TAG, "╔══════════════════════════════════════════════════════════════╗")
|
|
284
|
-
Log.e(TAG, "║ SPOTIFY APP-SWITCH AUTH WILL LIKELY FAIL ║")
|
|
285
|
-
Log.e(TAG, "╠══════════════════════════════════════════════════════════════╣")
|
|
286
|
-
Log.e(TAG, "║ MainActivity has android:launchMode=\"$modeName\"")
|
|
287
|
-
Log.e(TAG, "║ ║")
|
|
288
|
-
Log.e(TAG, "║ Android 7+ does not properly deliver startActivityForResult ║")
|
|
289
|
-
Log.e(TAG, "║ results to singleTask/singleInstance activities. The Spotify║")
|
|
290
|
-
Log.e(TAG, "║ auth dialog will flash briefly and immediately dismiss. ║")
|
|
291
|
-
Log.e(TAG, "║ ║")
|
|
292
|
-
Log.e(TAG, "║ FIX (client-side): In your app's AndroidManifest.xml, ║")
|
|
293
|
-
Log.e(TAG, "║ change MainActivity's android:launchMode to \"singleTop\" ║")
|
|
294
|
-
Log.e(TAG, "║ or remove the launchMode attribute entirely. ║")
|
|
295
|
-
Log.e(TAG, "╚══════════════════════════════════════════════════════════════╝")
|
|
296
|
-
Log.e(TAG, "")
|
|
297
|
-
}
|
|
298
|
-
|
|
299
272
|
// Set a timeout to detect if the auth flow doesn't complete
|
|
300
273
|
authTimeoutHandler = Runnable {
|
|
301
274
|
if (isAuthenticating) {
|
|
@@ -309,13 +282,12 @@ class SpotifyAuthAuth private constructor(private val appContext: AppContext) {
|
|
|
309
282
|
mainHandler.postDelayed(authTimeoutHandler!!, AUTH_TIMEOUT_MS)
|
|
310
283
|
|
|
311
284
|
try {
|
|
312
|
-
//
|
|
313
|
-
//
|
|
314
|
-
|
|
315
|
-
AuthorizationClient.
|
|
316
|
-
secureLog("AuthorizationClient.openLoginActivity called successfully")
|
|
285
|
+
// Use browser-based auth on Android (bypasses Spotify app-switch).
|
|
286
|
+
// The auth result is delivered via onNewIntent, handled by handleNewIntent().
|
|
287
|
+
AuthorizationClient.openLoginInBrowser(activity, request)
|
|
288
|
+
secureLog("AuthorizationClient.openLoginInBrowser called successfully")
|
|
317
289
|
} catch (e: Exception) {
|
|
318
|
-
Log.e(TAG, "Failed to open authorization
|
|
290
|
+
Log.e(TAG, "Failed to open browser authorization: ${e.message}", e)
|
|
319
291
|
throw SpotifyAuthException.AuthenticationFailed("Failed to open Spotify authorization: ${e.message}")
|
|
320
292
|
}
|
|
321
293
|
|
|
@@ -457,6 +429,78 @@ class SpotifyAuthAuth private constructor(private val appContext: AppContext) {
|
|
|
457
429
|
}
|
|
458
430
|
}
|
|
459
431
|
|
|
432
|
+
/**
|
|
433
|
+
* Handle the Spotify auth callback delivered via onNewIntent (browser-based flow).
|
|
434
|
+
* Called by the module's OnNewIntent handler after openLoginInBrowser() completes.
|
|
435
|
+
*/
|
|
436
|
+
fun handleNewIntent(intent: Intent) {
|
|
437
|
+
Log.d(TAG, "handleNewIntent called - action=${intent.action}, data=${intent.data}")
|
|
438
|
+
|
|
439
|
+
if (!isAuthenticating) {
|
|
440
|
+
Log.d(TAG, "Ignoring new intent - not currently authenticating")
|
|
441
|
+
return
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Cancel the timeout
|
|
445
|
+
authTimeoutHandler?.let {
|
|
446
|
+
mainHandler.removeCallbacks(it)
|
|
447
|
+
secureLog("Auth timeout cancelled")
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
isAuthenticating = false
|
|
451
|
+
secureLog("Setting isAuthenticating to false")
|
|
452
|
+
|
|
453
|
+
if (module == null) {
|
|
454
|
+
Log.e(TAG, "CRITICAL: Module is null in handleNewIntent - cannot send events to JS")
|
|
455
|
+
return
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
try {
|
|
459
|
+
val response = AuthorizationClient.getResponse(Activity.RESULT_OK, intent)
|
|
460
|
+
Log.d(TAG, "Spotify response type: ${response.type}")
|
|
461
|
+
|
|
462
|
+
when (response.type) {
|
|
463
|
+
AuthorizationResponse.Type.CODE -> {
|
|
464
|
+
val code = response.code
|
|
465
|
+
secureLog("Authorization code received, length=${code?.length ?: 0}")
|
|
466
|
+
if (code != null) {
|
|
467
|
+
exchangeCodeForToken(code)
|
|
468
|
+
} else {
|
|
469
|
+
Log.e(TAG, "Authorization code was null despite CODE response type")
|
|
470
|
+
module?.onAuthorizationError(
|
|
471
|
+
SpotifyAuthException.AuthenticationFailed("No authorization code received")
|
|
472
|
+
)
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
AuthorizationResponse.Type.ERROR -> {
|
|
476
|
+
val errorMsg = response.error ?: "Unknown error"
|
|
477
|
+
Log.e(TAG, "Spotify authorization error: $errorMsg")
|
|
478
|
+
if (errorMsg.contains("access_denied", ignoreCase = true) ||
|
|
479
|
+
errorMsg.contains("cancelled", ignoreCase = true)) {
|
|
480
|
+
module?.onAuthorizationError(SpotifyAuthException.UserCancelled())
|
|
481
|
+
} else {
|
|
482
|
+
module?.onAuthorizationError(SpotifyAuthException.AuthorizationError(errorMsg))
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
AuthorizationResponse.Type.EMPTY -> {
|
|
486
|
+
Log.w(TAG, "Browser auth returned EMPTY - user likely cancelled")
|
|
487
|
+
module?.onAuthorizationError(SpotifyAuthException.UserCancelled())
|
|
488
|
+
}
|
|
489
|
+
else -> {
|
|
490
|
+
Log.e(TAG, "Unexpected Spotify response type: ${response.type}")
|
|
491
|
+
module?.onAuthorizationError(
|
|
492
|
+
SpotifyAuthException.AuthenticationFailed("Unexpected response type: ${response.type}")
|
|
493
|
+
)
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
} catch (e: Exception) {
|
|
497
|
+
Log.e(TAG, "Exception in handleNewIntent: ${e.message}", e)
|
|
498
|
+
module?.onAuthorizationError(
|
|
499
|
+
SpotifyAuthException.AuthenticationFailed("Error processing auth result: ${e.message}")
|
|
500
|
+
)
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
460
504
|
// endregion
|
|
461
505
|
|
|
462
506
|
// region Token Exchange
|
|
@@ -705,16 +749,14 @@ class SpotifyAuthAuth private constructor(private val appContext: AppContext) {
|
|
|
705
749
|
// region Web Auth Cancellation
|
|
706
750
|
|
|
707
751
|
/**
|
|
708
|
-
* Cancel an in-progress
|
|
709
|
-
*
|
|
752
|
+
* Cancel an in-progress auth session.
|
|
753
|
+
* For browser-based auth the system browser cannot be closed programmatically,
|
|
754
|
+
* so this clears internal state and notifies JS of cancellation.
|
|
710
755
|
*/
|
|
711
756
|
fun cancelWebAuth() {
|
|
712
|
-
|
|
713
|
-
if (activity != null) {
|
|
714
|
-
AuthorizationClient.stopLoginActivity(activity, REQUEST_CODE)
|
|
715
|
-
}
|
|
716
|
-
module?.onAuthorizationError(SpotifyAuthException.UserCancelled())
|
|
757
|
+
authTimeoutHandler?.let { mainHandler.removeCallbacks(it) }
|
|
717
758
|
isAuthenticating = false
|
|
759
|
+
module?.onAuthorizationError(SpotifyAuthException.UserCancelled())
|
|
718
760
|
}
|
|
719
761
|
|
|
720
762
|
// endregion
|
|
@@ -62,6 +62,11 @@ class SpotifyAuthModule : Module() {
|
|
|
62
62
|
spotifyAuth.cancelWebAuth()
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
OnNewIntent { intent ->
|
|
66
|
+
secureLog("OnNewIntent received in module - action=${intent.action}, data=${intent.data}")
|
|
67
|
+
spotifyAuth.handleNewIntent(intent)
|
|
68
|
+
}
|
|
69
|
+
|
|
65
70
|
OnActivityResult { _, payload ->
|
|
66
71
|
secureLog("OnActivityResult received in module - requestCode=${payload.requestCode}, resultCode=${payload.resultCode}")
|
|
67
72
|
android.util.Log.d("SpotifyAuth", "=== MODULE ACTIVITY RESULT ===")
|