diff --git a/deploy/compose.yml b/deploy/compose.yml
index fe08b30..cddfe5b 100644
--- a/deploy/compose.yml
+++ b/deploy/compose.yml
@@ -259,27 +259,38 @@ services:
fi
echo "==> native app (KMP client: desktop loopback + mobile custom scheme)"
+ # Loopback (desktop) and a reverse-domain custom scheme (Android/iOS,
+ # RFC 8252 §7.1) are both valid for a NATIVE app without devMode, so
+ # dev and prod share the same config. Unlike the other apps, an
+ # existing one is also updated, so a change here reaches instances
+ # created before it.
NATIVE_APP_NAME="Musify Native"
+ NATIVE_CONFIG='{
+ "redirectUris": ["http://127.0.0.1:17453/callback", "cat.ikerdemo.musify:/callback"],
+ "postLogoutRedirectUris": ["http://127.0.0.1:17453/logout", "cat.ikerdemo.musify:/logout"],
+ "responseTypes": ["OIDC_RESPONSE_TYPE_CODE"],
+ "grantTypes": ["OIDC_GRANT_TYPE_AUTHORIZATION_CODE", "OIDC_GRANT_TYPE_REFRESH_TOKEN"],
+ "appType": "OIDC_APP_TYPE_NATIVE",
+ "authMethodType": "OIDC_AUTH_METHOD_TYPE_NONE",
+ "devMode": false,
+ "accessTokenType": "OIDC_TOKEN_TYPE_JWT",
+ "accessTokenRoleAssertion": true,
+ "idTokenRoleAssertion": true,
+ "idTokenUserinfoAssertion": true
+ }'
NATIVE_APP_ID=$$(find_app "$$NATIVE_APP_NAME")
if [ -n "$$NATIVE_APP_ID" ]; then
NATIVE_CLIENT_ID=$$(api_call GET "/projects/$$PROJECT_ID/apps/$$NATIVE_APP_ID" | jq -r '.app.oidcConfig.clientId')
- echo " reused (clientId $$NATIVE_CLIENT_ID)"
+ # Zitadel answers 400 "No changes" when the config already matches.
+ RESULT=$$(curl -sS -X PUT "$$MGMT/projects/$$PROJECT_ID/apps/$$NATIVE_APP_ID/oidc_config" \
+ -H "Authorization: Bearer $$PAT" -H "Content-Type: application/json" -d "$$NATIVE_CONFIG")
+ case "$$RESULT" in
+ *'"details"'*'"changeDate"'*) echo " reused (clientId $$NATIVE_CLIENT_ID, config updated)" ;;
+ *COMMAND-1m88i*) echo " reused (clientId $$NATIVE_CLIENT_ID)" ;;
+ *) echo "could not update the native app config: $$RESULT" >&2; exit 1 ;;
+ esac
else
- NATIVE_CLIENT_ID=$$(api_call POST "/projects/$$PROJECT_ID/apps/oidc" '{
- "name": "Musify Native",
- "redirectUris": ["http://127.0.0.1:17453/callback", "musify://callback"],
- "postLogoutRedirectUris": ["http://127.0.0.1:17453/logout", "musify://logout"],
- "responseTypes": ["OIDC_RESPONSE_TYPE_CODE"],
- "grantTypes": ["OIDC_GRANT_TYPE_AUTHORIZATION_CODE", "OIDC_GRANT_TYPE_REFRESH_TOKEN"],
- "appType": "OIDC_APP_TYPE_NATIVE",
- "authMethodType": "OIDC_AUTH_METHOD_TYPE_NONE",
- "version": "OIDC_VERSION_1_0",
- "devMode": true,
- "accessTokenType": "OIDC_TOKEN_TYPE_JWT",
- "accessTokenRoleAssertion": true,
- "idTokenRoleAssertion": true,
- "idTokenUserinfoAssertion": true
- }' | jq -r '.clientId')
+ NATIVE_CLIENT_ID=$$(api_call POST "/projects/$$PROJECT_ID/apps/oidc" "$$(echo "$$NATIVE_CONFIG" | jq --arg name "$$NATIVE_APP_NAME" '. + {name: $$name, version: "OIDC_VERSION_1_0"}')" | jq -r '.clientId')
echo " created (clientId $$NATIVE_CLIENT_ID)"
fi
diff --git a/mobile/README.md b/mobile/README.md
index bb91d38..69d1217 100644
--- a/mobile/README.md
+++ b/mobile/README.md
@@ -54,7 +54,7 @@ al patrón que ya usa Scalar para probar la API — ver `deploy/compose.yml` (`z
Redirect URIs registrados:
- Desktop: `http://127.0.0.1:17453/callback` (RFC 8252, loopback con puerto fijo).
-- Android / iOS: `musify://callback` (custom scheme).
+- Android / iOS: `cat.ikerdemo.musify:/callback` (custom scheme con dominio invertido, RFC 8252 §7.1).
`mobile/gradle.properties` controla a qué backend apunta Android/Desktop:
@@ -125,13 +125,13 @@ Kotlin/Native (`iosMain/`) sigue las APIs estándar de interop (`AVFoundation`,
1. Genera un proyecto Xcode con el asistente "Kotlin Multiplatform" de Android Studio, o crea uno vacío
e importa `composeApp` como framework (`./gradlew :composeApp:embedAndSignAppleFrameworkForXcode`).
-2. Registra el scheme `musify` en `Info.plist`:
+2. Registra el scheme `cat.ikerdemo.musify` en `Info.plist`:
```xml
CFBundleURLTypes
CFBundleURLSchemes
- musify
+ cat.ikerdemo.musify
```
diff --git a/mobile/composeApp/src/androidMain/AndroidManifest.xml b/mobile/composeApp/src/androidMain/AndroidManifest.xml
index 6a9094a..baafc80 100644
--- a/mobile/composeApp/src/androidMain/AndroidManifest.xml
+++ b/mobile/composeApp/src/androidMain/AndroidManifest.xml
@@ -18,7 +18,9 @@
-
-
+
diff --git a/mobile/composeApp/src/androidMain/kotlin/com/musify/app/Config.android.kt b/mobile/composeApp/src/androidMain/kotlin/com/musify/app/Config.android.kt
index aaad041..5ed1152 100644
--- a/mobile/composeApp/src/androidMain/kotlin/com/musify/app/Config.android.kt
+++ b/mobile/composeApp/src/androidMain/kotlin/com/musify/app/Config.android.kt
@@ -9,5 +9,5 @@ actual object AppConfig {
// Matches the AuthCallbackActivity intent-filter in AndroidManifest.xml and
// the redirect URI registered for the "Musify Native" app in Zitadel.
- actual val nativeRedirectUri: String = "musify://callback"
+ actual val nativeRedirectUri: String = "cat.ikerdemo.musify:/callback"
}
diff --git a/mobile/composeApp/src/androidMain/kotlin/com/musify/app/auth/AuthCallbackActivity.kt b/mobile/composeApp/src/androidMain/kotlin/com/musify/app/auth/AuthCallbackActivity.kt
index 3fc4bc0..d1ace95 100644
--- a/mobile/composeApp/src/androidMain/kotlin/com/musify/app/auth/AuthCallbackActivity.kt
+++ b/mobile/composeApp/src/androidMain/kotlin/com/musify/app/auth/AuthCallbackActivity.kt
@@ -5,7 +5,7 @@ import android.content.Intent
import android.os.Bundle
/**
- * Declared in AndroidManifest.xml with an intent-filter for `musify://callback`.
+ * Declared in AndroidManifest.xml with an intent-filter for `cat.ikerdemo.musify:/callback`.
* Chrome Custom Tabs redirects here once Zitadel finishes the login/register
* flow; this activity just forwards the URL to [AndroidAuthBridge] and closes.
*/
diff --git a/mobile/composeApp/src/commonMain/kotlin/com/musify/app/Config.kt b/mobile/composeApp/src/commonMain/kotlin/com/musify/app/Config.kt
index 33da1be..81f8bdb 100644
--- a/mobile/composeApp/src/commonMain/kotlin/com/musify/app/Config.kt
+++ b/mobile/composeApp/src/commonMain/kotlin/com/musify/app/Config.kt
@@ -9,7 +9,8 @@ package com.musify.app
* `nativeRedirectUri` is the OAuth redirect registered for the "Musify
* Native" Zitadel app (see deploy/compose.yml, zitadel-init): a fixed
* loopback port on Desktop (RFC 8252 native-app loopback flow), and the
- * `musify://callback` custom scheme on Android/iOS.
+ * `cat.ikerdemo.musify:/callback` reverse-domain custom scheme on Android/iOS
+ * (RFC 8252 §7.1: based on a domain we own, so no other app plausibly claims it).
*/
expect object AppConfig {
val apiBaseUrl: String
diff --git a/mobile/composeApp/src/commonMain/kotlin/com/musify/app/auth/BrowserAuthLauncher.kt b/mobile/composeApp/src/commonMain/kotlin/com/musify/app/auth/BrowserAuthLauncher.kt
index 137a8c8..05947cc 100644
--- a/mobile/composeApp/src/commonMain/kotlin/com/musify/app/auth/BrowserAuthLauncher.kt
+++ b/mobile/composeApp/src/commonMain/kotlin/com/musify/app/auth/BrowserAuthLauncher.kt
@@ -12,7 +12,7 @@ package com.musify.app.auth
* - Desktop: a one-shot loopback HTTP server on the fixed port registered in
* Zitadel (RFC 8252 §7.3), opened via the OS default browser.
* - Android: Chrome Custom Tabs; the redirect is caught by
- * [AuthCallbackActivity] via the `musify://callback` intent filter.
+ * [AuthCallbackActivity] via the `cat.ikerdemo.musify:/callback` intent filter.
* - iOS: `ASWebAuthenticationSession`, which owns the whole round trip.
*/
expect class BrowserAuthLauncher() {
diff --git a/mobile/composeApp/src/iosMain/kotlin/com/musify/app/Config.ios.kt b/mobile/composeApp/src/iosMain/kotlin/com/musify/app/Config.ios.kt
index ebd3b63..c8272d1 100644
--- a/mobile/composeApp/src/iosMain/kotlin/com/musify/app/Config.ios.kt
+++ b/mobile/composeApp/src/iosMain/kotlin/com/musify/app/Config.ios.kt
@@ -12,5 +12,5 @@ actual object AppConfig {
// Matches CFBundleURLTypes in Info.plist and the redirect URI registered
// for the "Musify Native" app in Zitadel.
- actual val nativeRedirectUri: String = "musify://callback"
+ actual val nativeRedirectUri: String = "cat.ikerdemo.musify:/callback"
}
diff --git a/mobile/composeApp/src/iosMain/kotlin/com/musify/app/auth/BrowserAuthLauncher.ios.kt b/mobile/composeApp/src/iosMain/kotlin/com/musify/app/auth/BrowserAuthLauncher.ios.kt
index cbb1006..d2b136d 100644
--- a/mobile/composeApp/src/iosMain/kotlin/com/musify/app/auth/BrowserAuthLauncher.ios.kt
+++ b/mobile/composeApp/src/iosMain/kotlin/com/musify/app/auth/BrowserAuthLauncher.ios.kt
@@ -23,14 +23,15 @@ private class PresentationContextProvider : NSObject(), ASWebAuthenticationPrese
/**
* `ASWebAuthenticationSession` owns the whole round trip: it opens a system
* browser sheet at [authorizeUrl] (Zitadel's hosted login, with its
- * "Regístrate" link) and intercepts the `musify://` redirect itself, no app
+ * "Regístrate" link) and intercepts the `cat.ikerdemo.musify:` redirect itself, no app
* delegate / `onOpenURL` wiring required.
*/
@OptIn(ExperimentalForeignApi::class)
actual class BrowserAuthLauncher actual constructor() {
actual suspend fun launch(authorizeUrl: String, redirectUri: String): String =
suspendCancellableCoroutine { continuation ->
- val scheme = redirectUri.substringBefore("://")
+ // `scheme:/path` (RFC 8252 private-use scheme): everything before the first ':'.
+ val scheme = redirectUri.substringBefore(":")
val contextProvider = PresentationContextProvider()
val session = ASWebAuthenticationSession(