@superfan-app/spotify-auth 0.1.72 → 0.1.74
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,6 +1,7 @@
|
|
|
1
1
|
package expo.modules.spotifyauth
|
|
2
2
|
|
|
3
3
|
import android.content.Intent
|
|
4
|
+
import android.content.pm.ActivityInfo
|
|
4
5
|
import android.content.pm.PackageManager
|
|
5
6
|
import android.net.Uri
|
|
6
7
|
import android.os.Handler
|
|
@@ -103,9 +104,16 @@ class SpotifyAuthAuth private constructor(private val appContext: AppContext) {
|
|
|
103
104
|
private fun isSpotifyInstalled(): Boolean {
|
|
104
105
|
val context = appContext.reactContext ?: return false
|
|
105
106
|
return try {
|
|
106
|
-
context.packageManager.getPackageInfo("com.spotify.music", 0)
|
|
107
|
+
val packageInfo = context.packageManager.getPackageInfo("com.spotify.music", 0)
|
|
108
|
+
Log.d(TAG, "Spotify app detected: com.spotify.music (version: ${packageInfo.versionName})")
|
|
107
109
|
true
|
|
110
|
+
} catch (e: PackageManager.NameNotFoundException) {
|
|
111
|
+
Log.d(TAG, "Spotify app NOT detected: ${e.message}")
|
|
112
|
+
Log.d(TAG, "If Spotify IS installed, this may be a package visibility issue (Android 11+)")
|
|
113
|
+
Log.d(TAG, "Ensure <queries><package android:name=\"com.spotify.music\"/></queries> is in merged manifest")
|
|
114
|
+
false
|
|
108
115
|
} catch (e: Exception) {
|
|
116
|
+
Log.e(TAG, "Error checking for Spotify app: ${e.message}", e)
|
|
109
117
|
false
|
|
110
118
|
}
|
|
111
119
|
}
|
|
@@ -256,6 +264,38 @@ class SpotifyAuthAuth private constructor(private val appContext: AppContext) {
|
|
|
256
264
|
|
|
257
265
|
secureLog("Opening Spotify authorization activity with REQUEST_CODE=$REQUEST_CODE")
|
|
258
266
|
|
|
267
|
+
// === ENHANCED DEBUG LOGGING ===
|
|
268
|
+
Log.d(TAG, "=== SPOTIFY AUTH DEBUG ===")
|
|
269
|
+
Log.d(TAG, "Auth flow type: ${if (spotifyInstalled) "APP_SWITCH" else "WEBVIEW"}")
|
|
270
|
+
Log.d(TAG, "Client ID: ${clientId.take(10)}...")
|
|
271
|
+
Log.d(TAG, "Redirect URI: $redirectUri")
|
|
272
|
+
Log.d(TAG, "Response Type: CODE")
|
|
273
|
+
Log.d(TAG, "Scopes: ${scopeArray.joinToString(",")}")
|
|
274
|
+
Log.d(TAG, "Package name: ${appContext.reactContext?.packageName}")
|
|
275
|
+
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
|
+
Log.d(TAG, "========================")
|
|
279
|
+
|
|
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
|
+
|
|
259
299
|
// Set a timeout to detect if the auth flow doesn't complete
|
|
260
300
|
authTimeoutHandler = Runnable {
|
|
261
301
|
if (isAuthenticating) {
|
|
@@ -300,6 +340,28 @@ class SpotifyAuthAuth private constructor(private val appContext: AppContext) {
|
|
|
300
340
|
fun handleActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
301
341
|
Log.d(TAG, "handleActivityResult called - requestCode=$requestCode, resultCode=$resultCode, hasData=${data != null}")
|
|
302
342
|
|
|
343
|
+
// === ENHANCED DEBUG LOGGING FOR ACTIVITY RESULT ===
|
|
344
|
+
if (data != null) {
|
|
345
|
+
Log.d(TAG, "Intent data URI: ${data.data}")
|
|
346
|
+
Log.d(TAG, "Intent action: ${data.action}")
|
|
347
|
+
Log.d(TAG, "Intent extras keys: ${data.extras?.keySet()?.joinToString() ?: "none"}")
|
|
348
|
+
data.extras?.let { extras ->
|
|
349
|
+
for (key in extras.keySet()) {
|
|
350
|
+
val value = extras.get(key)
|
|
351
|
+
if (key.contains("token", ignoreCase = true) ||
|
|
352
|
+
key.contains("code", ignoreCase = true) ||
|
|
353
|
+
key.contains("secret", ignoreCase = true)) {
|
|
354
|
+
Log.d(TAG, " $key: [REDACTED]")
|
|
355
|
+
} else {
|
|
356
|
+
Log.d(TAG, " $key: $value")
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
} else {
|
|
361
|
+
Log.w(TAG, "Intent data is NULL - callback may not have fired correctly")
|
|
362
|
+
Log.w(TAG, "This often indicates an intent filter configuration issue")
|
|
363
|
+
}
|
|
364
|
+
|
|
303
365
|
if (requestCode != REQUEST_CODE) {
|
|
304
366
|
Log.d(TAG, "Ignoring activity result - wrong request code (expected $REQUEST_CODE, got $requestCode)")
|
|
305
367
|
return
|
|
@@ -351,7 +413,33 @@ class SpotifyAuthAuth private constructor(private val appContext: AppContext) {
|
|
|
351
413
|
}
|
|
352
414
|
}
|
|
353
415
|
AuthorizationResponse.Type.EMPTY -> {
|
|
354
|
-
|
|
416
|
+
val spotifyInstalled = isSpotifyInstalled()
|
|
417
|
+
if (spotifyInstalled) {
|
|
418
|
+
Log.e(TAG, "")
|
|
419
|
+
Log.e(TAG, "╔══════════════════════════════════════════════════════════════╗")
|
|
420
|
+
Log.e(TAG, "║ SPOTIFY APP-SWITCH AUTH: EMPTY RESPONSE ║")
|
|
421
|
+
Log.e(TAG, "╠══════════════════════════════════════════════════════════════╣")
|
|
422
|
+
Log.e(TAG, "║ Spotify is installed but auth returned empty immediately. ║")
|
|
423
|
+
Log.e(TAG, "║ The auth dialog flashed and dismissed without going to ║")
|
|
424
|
+
Log.e(TAG, "║ Spotify. Common client-side causes: ║")
|
|
425
|
+
Log.e(TAG, "║ ║")
|
|
426
|
+
Log.e(TAG, "║ 1. MainActivity launchMode is singleTask (most likely). ║")
|
|
427
|
+
Log.e(TAG, "║ Change to singleTop in your AndroidManifest.xml. ║")
|
|
428
|
+
Log.e(TAG, "║ ║")
|
|
429
|
+
Log.e(TAG, "║ 2. Redirect URI '${redirectURL.take(30)}...' not registered ║")
|
|
430
|
+
Log.e(TAG, "║ in Spotify Developer Dashboard. ║")
|
|
431
|
+
Log.e(TAG, "║ ║")
|
|
432
|
+
Log.e(TAG, "║ 3. manifestPlaceholders redirectHostName in build.gradle ║")
|
|
433
|
+
Log.e(TAG, "║ does not match the host portion of your redirect URI. ║")
|
|
434
|
+
Log.e(TAG, "║ (Run the Expo config plugin to regenerate.) ║")
|
|
435
|
+
Log.e(TAG, "║ ║")
|
|
436
|
+
Log.e(TAG, "║ 4. Installed Spotify version is too old to support ║")
|
|
437
|
+
Log.e(TAG, "║ app-switch auth. ║")
|
|
438
|
+
Log.e(TAG, "╚══════════════════════════════════════════════════════════════╝")
|
|
439
|
+
Log.e(TAG, "")
|
|
440
|
+
} else {
|
|
441
|
+
Log.w(TAG, "Authorization returned EMPTY - user likely cancelled")
|
|
442
|
+
}
|
|
355
443
|
module?.onAuthorizationError(SpotifyAuthException.UserCancelled())
|
|
356
444
|
}
|
|
357
445
|
else -> {
|
|
@@ -64,6 +64,16 @@ class SpotifyAuthModule : Module() {
|
|
|
64
64
|
|
|
65
65
|
OnActivityResult { _, payload ->
|
|
66
66
|
secureLog("OnActivityResult received in module - requestCode=${payload.requestCode}, resultCode=${payload.resultCode}")
|
|
67
|
+
android.util.Log.d("SpotifyAuth", "=== MODULE ACTIVITY RESULT ===")
|
|
68
|
+
android.util.Log.d("SpotifyAuth", "Request code: ${payload.requestCode}")
|
|
69
|
+
android.util.Log.d("SpotifyAuth", "Result code: ${payload.resultCode}")
|
|
70
|
+
android.util.Log.d("SpotifyAuth", "Has data: ${payload.data != null}")
|
|
71
|
+
if (payload.data != null) {
|
|
72
|
+
android.util.Log.d("SpotifyAuth", "Data scheme: ${payload.data?.scheme}")
|
|
73
|
+
android.util.Log.d("SpotifyAuth", "Data host: ${payload.data?.data?.host}")
|
|
74
|
+
android.util.Log.d("SpotifyAuth", "Data path: ${payload.data?.data?.path}")
|
|
75
|
+
}
|
|
76
|
+
android.util.Log.d("SpotifyAuth", "============================")
|
|
67
77
|
spotifyAuth.handleActivityResult(payload.requestCode, payload.resultCode, payload.data)
|
|
68
78
|
}
|
|
69
79
|
|
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
|
|