@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
- * Supports two auth flows:
28
- * 1. App-switch via Spotify's AuthorizationClient (when Spotify app is installed)
29
- * 2. Web auth via AuthorizationClient's WebView fallback (built into the Spotify auth-lib)
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
- * Uses Spotify's AuthorizationClient which handles both app-switch (when Spotify is installed)
198
- * and WebView fallback automatically.
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 activity with REQUEST_CODE=$REQUEST_CODE")
259
+ secureLog("Opening Spotify authorization in browser")
266
260
 
267
- // === ENHANCED DEBUG LOGGING ===
261
+ // === SPOTIFY AUTH DEBUG ===
268
262
  Log.d(TAG, "=== SPOTIFY AUTH DEBUG ===")
269
- Log.d(TAG, "Auth flow type: ${if (spotifyInstalled) "APP_SWITCH" else "WEBVIEW"}")
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
- // AuthorizationClient.openLoginActivity handles both flows:
313
- // - If Spotify is installed: app-switch auth
314
- // - If Spotify is not installed: opens a WebView with Spotify login
315
- AuthorizationClient.openLoginActivity(activity, REQUEST_CODE, request)
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 activity: ${e.message}", e)
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 web auth session.
709
- * On Android, this clears cookies and notifies JS of cancellation.
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
- val activity = appContext.currentActivity
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 ===")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@superfan-app/spotify-auth",
3
- "version": "0.1.74",
3
+ "version": "0.1.75",
4
4
  "description": "Spotify OAuth module for Expo",
5
5
  "main": "src/index.tsx",
6
6
  "types": "build/index.d.ts",