@superfan-app/spotify-auth 0.1.73 → 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,5 +1,6 @@
|
|
|
1
1
|
package expo.modules.spotifyauth
|
|
2
2
|
|
|
3
|
+
import android.app.Activity
|
|
3
4
|
import android.content.Intent
|
|
4
5
|
import android.content.pm.PackageManager
|
|
5
6
|
import android.net.Uri
|
|
@@ -23,9 +24,9 @@ import java.util.concurrent.Executors
|
|
|
23
24
|
/**
|
|
24
25
|
* Core Spotify authentication logic for Android, mirroring the iOS SpotifyAuthAuth class.
|
|
25
26
|
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
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()).
|
|
29
30
|
*
|
|
30
31
|
* Token exchange and refresh are handled via the backend token swap/refresh URLs,
|
|
31
32
|
* matching the iOS implementation.
|
|
@@ -192,9 +193,9 @@ class SpotifyAuthAuth private constructor(private val appContext: AppContext) {
|
|
|
192
193
|
// region Authentication Flow
|
|
193
194
|
|
|
194
195
|
/**
|
|
195
|
-
* Initiate the Spotify authorization flow.
|
|
196
|
-
*
|
|
197
|
-
*
|
|
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().
|
|
198
199
|
*/
|
|
199
200
|
fun initAuth(config: AuthorizeConfig) {
|
|
200
201
|
secureLog("initAuth called with showDialog=${config.showDialog}")
|
|
@@ -223,13 +224,7 @@ class SpotifyAuthAuth private constructor(private val appContext: AppContext) {
|
|
|
223
224
|
val redirectUri = redirectURL
|
|
224
225
|
val scopeArray = scopes.toTypedArray()
|
|
225
226
|
|
|
226
|
-
val spotifyInstalled = isSpotifyInstalled()
|
|
227
227
|
secureLog("Configuration - ClientID: ${clientId.take(8)}..., RedirectURI: $redirectUri, Scopes: ${scopeArray.size}")
|
|
228
|
-
Log.d(TAG, "Spotify app installed: $spotifyInstalled (will use ${if (spotifyInstalled) "app-switch" else "WebView"} auth)")
|
|
229
|
-
|
|
230
|
-
if (!spotifyInstalled) {
|
|
231
|
-
Log.w(TAG, "Spotify app not detected. Will use WebView fallback. If WebView fails, check package visibility in AndroidManifest (<queries> tag)")
|
|
232
|
-
}
|
|
233
228
|
|
|
234
229
|
if (scopeArray.isEmpty()) {
|
|
235
230
|
Log.e(TAG, "No valid scopes found in configuration")
|
|
@@ -261,18 +256,17 @@ class SpotifyAuthAuth private constructor(private val appContext: AppContext) {
|
|
|
261
256
|
|
|
262
257
|
val request = builder.build()
|
|
263
258
|
|
|
264
|
-
secureLog("Opening Spotify authorization
|
|
259
|
+
secureLog("Opening Spotify authorization in browser")
|
|
265
260
|
|
|
266
|
-
// ===
|
|
261
|
+
// === SPOTIFY AUTH DEBUG ===
|
|
267
262
|
Log.d(TAG, "=== SPOTIFY AUTH DEBUG ===")
|
|
268
|
-
Log.d(TAG, "Auth flow type:
|
|
263
|
+
Log.d(TAG, "Auth flow type: BROWSER (app-switch disabled on Android)")
|
|
269
264
|
Log.d(TAG, "Client ID: ${clientId.take(10)}...")
|
|
270
265
|
Log.d(TAG, "Redirect URI: $redirectUri")
|
|
271
266
|
Log.d(TAG, "Response Type: CODE")
|
|
272
267
|
Log.d(TAG, "Scopes: ${scopeArray.joinToString(",")}")
|
|
273
268
|
Log.d(TAG, "Package name: ${appContext.reactContext?.packageName}")
|
|
274
269
|
Log.d(TAG, "Activity: ${activity.javaClass.name}")
|
|
275
|
-
Log.d(TAG, "Activity launchMode: ${activity.packageManager.getActivityInfo(activity.componentName, 0).launchMode}")
|
|
276
270
|
Log.d(TAG, "========================")
|
|
277
271
|
|
|
278
272
|
// Set a timeout to detect if the auth flow doesn't complete
|
|
@@ -288,13 +282,12 @@ class SpotifyAuthAuth private constructor(private val appContext: AppContext) {
|
|
|
288
282
|
mainHandler.postDelayed(authTimeoutHandler!!, AUTH_TIMEOUT_MS)
|
|
289
283
|
|
|
290
284
|
try {
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
|
|
294
|
-
AuthorizationClient.
|
|
295
|
-
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")
|
|
296
289
|
} catch (e: Exception) {
|
|
297
|
-
Log.e(TAG, "Failed to open authorization
|
|
290
|
+
Log.e(TAG, "Failed to open browser authorization: ${e.message}", e)
|
|
298
291
|
throw SpotifyAuthException.AuthenticationFailed("Failed to open Spotify authorization: ${e.message}")
|
|
299
292
|
}
|
|
300
293
|
|
|
@@ -392,7 +385,33 @@ class SpotifyAuthAuth private constructor(private val appContext: AppContext) {
|
|
|
392
385
|
}
|
|
393
386
|
}
|
|
394
387
|
AuthorizationResponse.Type.EMPTY -> {
|
|
395
|
-
|
|
388
|
+
val spotifyInstalled = isSpotifyInstalled()
|
|
389
|
+
if (spotifyInstalled) {
|
|
390
|
+
Log.e(TAG, "")
|
|
391
|
+
Log.e(TAG, "╔══════════════════════════════════════════════════════════════╗")
|
|
392
|
+
Log.e(TAG, "║ SPOTIFY APP-SWITCH AUTH: EMPTY RESPONSE ║")
|
|
393
|
+
Log.e(TAG, "╠══════════════════════════════════════════════════════════════╣")
|
|
394
|
+
Log.e(TAG, "║ Spotify is installed but auth returned empty immediately. ║")
|
|
395
|
+
Log.e(TAG, "║ The auth dialog flashed and dismissed without going to ║")
|
|
396
|
+
Log.e(TAG, "║ Spotify. Common client-side causes: ║")
|
|
397
|
+
Log.e(TAG, "║ ║")
|
|
398
|
+
Log.e(TAG, "║ 1. MainActivity launchMode is singleTask (most likely). ║")
|
|
399
|
+
Log.e(TAG, "║ Change to singleTop in your AndroidManifest.xml. ║")
|
|
400
|
+
Log.e(TAG, "║ ║")
|
|
401
|
+
Log.e(TAG, "║ 2. Redirect URI '${redirectURL.take(30)}...' not registered ║")
|
|
402
|
+
Log.e(TAG, "║ in Spotify Developer Dashboard. ║")
|
|
403
|
+
Log.e(TAG, "║ ║")
|
|
404
|
+
Log.e(TAG, "║ 3. manifestPlaceholders redirectHostName in build.gradle ║")
|
|
405
|
+
Log.e(TAG, "║ does not match the host portion of your redirect URI. ║")
|
|
406
|
+
Log.e(TAG, "║ (Run the Expo config plugin to regenerate.) ║")
|
|
407
|
+
Log.e(TAG, "║ ║")
|
|
408
|
+
Log.e(TAG, "║ 4. Installed Spotify version is too old to support ║")
|
|
409
|
+
Log.e(TAG, "║ app-switch auth. ║")
|
|
410
|
+
Log.e(TAG, "╚══════════════════════════════════════════════════════════════╝")
|
|
411
|
+
Log.e(TAG, "")
|
|
412
|
+
} else {
|
|
413
|
+
Log.w(TAG, "Authorization returned EMPTY - user likely cancelled")
|
|
414
|
+
}
|
|
396
415
|
module?.onAuthorizationError(SpotifyAuthException.UserCancelled())
|
|
397
416
|
}
|
|
398
417
|
else -> {
|
|
@@ -410,6 +429,78 @@ class SpotifyAuthAuth private constructor(private val appContext: AppContext) {
|
|
|
410
429
|
}
|
|
411
430
|
}
|
|
412
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
|
+
|
|
413
504
|
// endregion
|
|
414
505
|
|
|
415
506
|
// region Token Exchange
|
|
@@ -658,16 +749,14 @@ class SpotifyAuthAuth private constructor(private val appContext: AppContext) {
|
|
|
658
749
|
// region Web Auth Cancellation
|
|
659
750
|
|
|
660
751
|
/**
|
|
661
|
-
* Cancel an in-progress
|
|
662
|
-
*
|
|
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.
|
|
663
755
|
*/
|
|
664
756
|
fun cancelWebAuth() {
|
|
665
|
-
|
|
666
|
-
if (activity != null) {
|
|
667
|
-
AuthorizationClient.stopLoginActivity(activity, REQUEST_CODE)
|
|
668
|
-
}
|
|
669
|
-
module?.onAuthorizationError(SpotifyAuthException.UserCancelled())
|
|
757
|
+
authTimeoutHandler?.let { mainHandler.removeCallbacks(it) }
|
|
670
758
|
isAuthenticating = false
|
|
759
|
+
module?.onAuthorizationError(SpotifyAuthException.UserCancelled())
|
|
671
760
|
}
|
|
672
761
|
|
|
673
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
package/plugin/build/index.js
CHANGED
|
@@ -3,6 +3,16 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
const config_plugins_1 = require("@expo/config-plugins");
|
|
5
5
|
const pkg = require('../../package.json');
|
|
6
|
+
/**
|
|
7
|
+
* Splits a callback value (e.g. "auth" or "auth/spotify") into its host and optional path components.
|
|
8
|
+
* Android intent filter `android:host` must not include a path segment.
|
|
9
|
+
*/
|
|
10
|
+
function parseCallback(callback) {
|
|
11
|
+
const slashIdx = callback.indexOf('/');
|
|
12
|
+
if (slashIdx === -1)
|
|
13
|
+
return { host: callback, path: null };
|
|
14
|
+
return { host: callback.slice(0, slashIdx), path: callback.slice(slashIdx) };
|
|
15
|
+
}
|
|
6
16
|
function validateSpotifyConfig(config) {
|
|
7
17
|
if (!config.clientID)
|
|
8
18
|
throw new Error("Spotify clientID is required");
|
|
@@ -77,9 +87,12 @@ const withSpotifyManifestPlaceholders = (config, props) => {
|
|
|
77
87
|
let buildGradle = config.modResults.contents;
|
|
78
88
|
// Escape values for Gradle string literals (escape backslashes and quotes)
|
|
79
89
|
const scheme = String(props.scheme).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
const
|
|
90
|
+
// Split callback into host and optional path — android:host must not contain a slash
|
|
91
|
+
const { host: callbackHost, path: callbackPath } = parseCallback(props.callback);
|
|
92
|
+
const host = String(callbackHost).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
93
|
+
// If the callback has a path component (e.g. "auth/spotify"), prefix the pathPattern
|
|
94
|
+
// so SpotifyAuthorizationActivity's intent filter correctly matches the return URI.
|
|
95
|
+
const pathPattern = callbackPath ? `${callbackPath}.*` : '.*';
|
|
83
96
|
const placeholdersBlock = ` manifestPlaceholders = [
|
|
84
97
|
redirectSchemeName: "${scheme}",
|
|
85
98
|
redirectHostName: "${host}",
|
|
@@ -109,6 +122,8 @@ const withSpotifyAndroidManifest = (config, props) => {
|
|
|
109
122
|
const mainApplication = config_plugins_1.AndroidConfig.Manifest.getMainApplicationOrThrow(config.modResults);
|
|
110
123
|
// Construct the redirect URL from scheme and callback
|
|
111
124
|
const redirectUrl = `${props.scheme}://${props.callback}`;
|
|
125
|
+
// Split callback into host and optional path for correct intent filter construction
|
|
126
|
+
const { host: callbackHost, path: callbackPath } = parseCallback(props.callback);
|
|
112
127
|
// Add Spotify configuration as meta-data elements
|
|
113
128
|
const metaDataEntries = [
|
|
114
129
|
{ name: 'SpotifyClientID', value: props.clientID },
|
|
@@ -137,22 +152,24 @@ const withSpotifyAndroidManifest = (config, props) => {
|
|
|
137
152
|
mainActivity['intent-filter'] = [];
|
|
138
153
|
}
|
|
139
154
|
// Check if we already have a Spotify redirect intent filter
|
|
140
|
-
const hasSpotifyIntentFilter = mainActivity['intent-filter'].some((filter) => filter.data?.some((d) => d.$?.['android:scheme'] === props.scheme && d.$?.['android:host'] ===
|
|
155
|
+
const hasSpotifyIntentFilter = mainActivity['intent-filter'].some((filter) => filter.data?.some((d) => d.$?.['android:scheme'] === props.scheme && d.$?.['android:host'] === callbackHost));
|
|
141
156
|
if (!hasSpotifyIntentFilter) {
|
|
157
|
+
// Build the data element: android:host must be the hostname only (no path).
|
|
158
|
+
// If the callback has a path component (e.g. "auth/spotify"), add android:pathPrefix.
|
|
159
|
+
const dataAttrs = {
|
|
160
|
+
'android:scheme': props.scheme,
|
|
161
|
+
'android:host': callbackHost,
|
|
162
|
+
};
|
|
163
|
+
if (callbackPath) {
|
|
164
|
+
dataAttrs['android:pathPrefix'] = callbackPath;
|
|
165
|
+
}
|
|
142
166
|
mainActivity['intent-filter'].push({
|
|
143
167
|
action: [{ $: { 'android:name': 'android.intent.action.VIEW' } }],
|
|
144
168
|
category: [
|
|
145
169
|
{ $: { 'android:name': 'android.intent.category.DEFAULT' } },
|
|
146
170
|
{ $: { 'android:name': 'android.intent.category.BROWSABLE' } },
|
|
147
171
|
],
|
|
148
|
-
data: [
|
|
149
|
-
{
|
|
150
|
-
$: {
|
|
151
|
-
'android:scheme': props.scheme,
|
|
152
|
-
'android:host': props.callback,
|
|
153
|
-
},
|
|
154
|
-
},
|
|
155
|
-
],
|
|
172
|
+
data: [{ $: dataAttrs }],
|
|
156
173
|
});
|
|
157
174
|
}
|
|
158
175
|
return config;
|
package/plugin/src/index.ts
CHANGED
|
@@ -5,6 +5,16 @@ import { SpotifyConfig } from './types.js'
|
|
|
5
5
|
|
|
6
6
|
const pkg = require('../../package.json');
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Splits a callback value (e.g. "auth" or "auth/spotify") into its host and optional path components.
|
|
10
|
+
* Android intent filter `android:host` must not include a path segment.
|
|
11
|
+
*/
|
|
12
|
+
function parseCallback(callback: string): { host: string; path: string | null } {
|
|
13
|
+
const slashIdx = callback.indexOf('/')
|
|
14
|
+
if (slashIdx === -1) return { host: callback, path: null }
|
|
15
|
+
return { host: callback.slice(0, slashIdx), path: callback.slice(slashIdx) }
|
|
16
|
+
}
|
|
17
|
+
|
|
8
18
|
function validateSpotifyConfig(config: SpotifyConfig) {
|
|
9
19
|
if (!config.clientID) throw new Error("Spotify clientID is required")
|
|
10
20
|
if (!config.scheme) throw new Error("URL scheme is required")
|
|
@@ -86,9 +96,12 @@ const withSpotifyManifestPlaceholders: ConfigPlugin<SpotifyConfig> = (config, pr
|
|
|
86
96
|
|
|
87
97
|
// Escape values for Gradle string literals (escape backslashes and quotes)
|
|
88
98
|
const scheme = String(props.scheme).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const
|
|
99
|
+
// Split callback into host and optional path — android:host must not contain a slash
|
|
100
|
+
const { host: callbackHost, path: callbackPath } = parseCallback(props.callback)
|
|
101
|
+
const host = String(callbackHost).replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
102
|
+
// If the callback has a path component (e.g. "auth/spotify"), prefix the pathPattern
|
|
103
|
+
// so SpotifyAuthorizationActivity's intent filter correctly matches the return URI.
|
|
104
|
+
const pathPattern = callbackPath ? `${callbackPath}.*` : '.*';
|
|
92
105
|
|
|
93
106
|
const placeholdersBlock = ` manifestPlaceholders = [
|
|
94
107
|
redirectSchemeName: "${scheme}",
|
|
@@ -131,6 +144,8 @@ const withSpotifyAndroidManifest: ConfigPlugin<SpotifyConfig> = (config, props)
|
|
|
131
144
|
|
|
132
145
|
// Construct the redirect URL from scheme and callback
|
|
133
146
|
const redirectUrl = `${props.scheme}://${props.callback}`;
|
|
147
|
+
// Split callback into host and optional path for correct intent filter construction
|
|
148
|
+
const { host: callbackHost, path: callbackPath } = parseCallback(props.callback)
|
|
134
149
|
|
|
135
150
|
// Add Spotify configuration as meta-data elements
|
|
136
151
|
const metaDataEntries = [
|
|
@@ -170,25 +185,28 @@ const withSpotifyAndroidManifest: ConfigPlugin<SpotifyConfig> = (config, props)
|
|
|
170
185
|
const hasSpotifyIntentFilter = mainActivity['intent-filter'].some(
|
|
171
186
|
(filter: any) =>
|
|
172
187
|
filter.data?.some(
|
|
173
|
-
(d: any) => d.$?.['android:scheme'] === props.scheme && d.$?.['android:host'] ===
|
|
188
|
+
(d: any) => d.$?.['android:scheme'] === props.scheme && d.$?.['android:host'] === callbackHost
|
|
174
189
|
)
|
|
175
190
|
);
|
|
176
191
|
|
|
177
192
|
if (!hasSpotifyIntentFilter) {
|
|
193
|
+
// Build the data element: android:host must be the hostname only (no path).
|
|
194
|
+
// If the callback has a path component (e.g. "auth/spotify"), add android:pathPrefix.
|
|
195
|
+
const dataAttrs: Record<string, string> = {
|
|
196
|
+
'android:scheme': props.scheme,
|
|
197
|
+
'android:host': callbackHost,
|
|
198
|
+
}
|
|
199
|
+
if (callbackPath) {
|
|
200
|
+
dataAttrs['android:pathPrefix'] = callbackPath
|
|
201
|
+
}
|
|
202
|
+
|
|
178
203
|
mainActivity['intent-filter'].push({
|
|
179
204
|
action: [{ $: { 'android:name': 'android.intent.action.VIEW' } }],
|
|
180
205
|
category: [
|
|
181
206
|
{ $: { 'android:name': 'android.intent.category.DEFAULT' } },
|
|
182
207
|
{ $: { 'android:name': 'android.intent.category.BROWSABLE' } },
|
|
183
208
|
],
|
|
184
|
-
data: [
|
|
185
|
-
{
|
|
186
|
-
$: {
|
|
187
|
-
'android:scheme': props.scheme,
|
|
188
|
-
'android:host': props.callback,
|
|
189
|
-
},
|
|
190
|
-
},
|
|
191
|
-
],
|
|
209
|
+
data: [{ $: dataAttrs }],
|
|
192
210
|
});
|
|
193
211
|
}
|
|
194
212
|
|