Skip to content

Commit a232c4d

Browse files
authored
Merge pull request #99 from swingmx/fix/auth-channel-navigation
Fix Post-Login Freezing #98
2 parents 750f6a4 + d003b79 commit a232c4d

File tree

6 files changed

+116
-20
lines changed

6 files changed

+116
-20
lines changed

README.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@
33
</div>
44
<div align="center" style="font-size: 2rem"><b>Swing Music Android Client</b></div>
55

6-
<div align="center"><b><sub><code>v1.0.0</code></sub></b></div>
6+
<div align="center"><b><sub><code>v1.0.1</code></sub></b></div>
77

88
**<div align="center" style="padding-top: 1.25rem">[Download](https://github.com/swingmx/android/releases) • <a href="https://github.com/sponsors/swingmx" target="_blank">Sponsor Us ❤️</a> • [Swing Music Docs](https://swingmx.com/guide/introduction.html)[r/SwingMusicApp](https://www.reddit.com/r/SwingMusicApp)</div>**
99

1010
##
1111

12+
![Image](.github/images/readme.webp)
1213
This client application allows you to stream music on your Android device from your Swing Music server.
1314

1415
### Features
@@ -29,9 +30,6 @@ Download the app from the [releases page](https://github.com/swingmx/android/rel
2930

3031
You can go to `Settings > Pair device` on the webclient to get the QR code.
3132

32-
## Screenshots
33-
34-
![Image](.github/images/readme.webp)
3533

3634
<!-- [![wakatime](https://wakatime.com/badge/user/99206146-a1fc-4be5-adc8-c2351f27ecef/project/018e7aae-f9e9-42e9-99e1-fc381580884d.svg)](https://wakatime.com/badge/user/99206146-a1fc-4be5-adc8-c2351f27ecef/project/018e7aae-f9e9-42e9-99e1-fc381580884d) -->
3735

auth/src/main/java/com/android/swingmusic/auth/data/repository/DataAuthRepository.kt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,12 +90,12 @@ class DataAuthRepository @Inject constructor(
9090
)
9191

9292
result.toModel()
93-
} catch (e: Exception) {
94-
Timber.e(message = "Failed to get refresh tokens!")
95-
null
9693
} catch (e: HttpException) {
9794
Timber.e(message = "Connection Failed!")
9895
null
96+
} catch (e: Exception) {
97+
Timber.e(message = "Failed to get refresh tokens!")
98+
null
9999
}
100100
}
101101

@@ -107,10 +107,10 @@ class DataAuthRepository @Inject constructor(
107107
val result = authApiService.getAllUsers("$baseUrl/auth/users").toModel()
108108
emit(Resource.Success(data = result))
109109

110-
} catch (e: Exception) {
111-
emit(Resource.Error(message = "Failed to load users!"))
112110
} catch (e: HttpException) {
113111
emit(Resource.Error(message = "Connection Failed!"))
112+
} catch (e: Exception) {
113+
emit(Resource.Error(message = "Failed to load users!"))
114114
}
115115
}
116116
}

auth/src/main/java/com/android/swingmusic/auth/presentation/screen/LoginWithQrCode.kt

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -98,12 +98,13 @@ fun LoginWithQrCode(
9898
else -> ""
9999
}
100100

101-
LaunchedEffect(key1 = authUiState.authState, block = {
102-
if (authUiState.authState == AuthState.AUTHENTICATED) {
103-
// authNavigator.gotoHomeNavGraph()
104-
authNavigator.gotoFolders()
101+
LaunchedEffect(Unit) {
102+
authViewModel.authStateEvent.collect { state ->
103+
if (state == AuthState.AUTHENTICATED) {
104+
authNavigator.gotoFolders()
105+
}
105106
}
106-
})
107+
}
107108

108109
Scaffold {
109110
Scaffold(

auth/src/main/java/com/android/swingmusic/auth/presentation/screen/LoginWithUsername.kt

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -349,12 +349,13 @@ fun LoginWithUsernameScreen(
349349
) {
350350
val authUiState by authViewModel.authUiState.collectAsState()
351351

352-
LaunchedEffect(key1 = authUiState.authState, block = {
353-
if (authUiState.authState == AuthState.AUTHENTICATED) {
354-
// authNavigator.gotoHomeNavGraph()
355-
authNavigator.gotoFolders()
352+
LaunchedEffect(Unit) {
353+
authViewModel.authStateEvent.collect { state ->
354+
if (state == AuthState.AUTHENTICATED) {
355+
authNavigator.gotoFolders()
356+
}
356357
}
357-
})
358+
}
358359

359360
LoginWithUsername(
360361
authUiState = authUiState,

auth/src/main/java/com/android/swingmusic/auth/presentation/viewmodel/AuthViewModel.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@ import com.android.swingmusic.auth.presentation.state.AuthUiState
1616
import com.android.swingmusic.auth.presentation.util.AuthError
1717
import com.android.swingmusic.core.data.util.Resource
1818
import dagger.hilt.android.lifecycle.HiltViewModel
19+
import kotlinx.coroutines.channels.Channel
1920
import kotlinx.coroutines.flow.MutableStateFlow
2021
import kotlinx.coroutines.flow.StateFlow
2122
import kotlinx.coroutines.flow.asStateFlow
2223
import kotlinx.coroutines.flow.collectLatest
24+
import kotlinx.coroutines.flow.receiveAsFlow
2325
import kotlinx.coroutines.flow.update
2426
import kotlinx.coroutines.launch
2527
import javax.inject.Inject
@@ -36,6 +38,9 @@ class AuthViewModel @Inject constructor(
3638
private val _isUserLoggedIn = MutableStateFlow<Boolean?>(null)
3739
val isUserLoggedIn: StateFlow<Boolean?> = _isUserLoggedIn.asStateFlow()
3840

41+
private val _authStateEvent = Channel<AuthState>(Channel.BUFFERED)
42+
val authStateEvent = _authStateEvent.receiveAsFlow()
43+
3944
init {
4045
viewModelScope.launch {
4146
authRepository.initializeBaseUrlAndAuthTokens()
@@ -151,6 +156,8 @@ class AuthViewModel @Inject constructor(
151156
authError = AuthError.None,
152157
baseUrl = baseUrl
153158
)
159+
160+
_authStateEvent.trySend(AuthState.AUTHENTICATED)
154161
}
155162
}
156163
}
@@ -165,7 +172,7 @@ class AuthViewModel @Inject constructor(
165172
val url = pair.first
166173
val pairCode = pair.second
167174

168-
if (url.isEmpty() or pairCode.isEmpty()) {
175+
if (url.isEmpty() || pairCode.isEmpty()) {
169176
_authUiState.value = _authUiState.value.copy(
170177
authState = AuthState.LOGGED_OUT,
171178
isLoading = false,
@@ -210,6 +217,8 @@ class AuthViewModel @Inject constructor(
210217
authError = AuthError.None,
211218
baseUrl = url
212219
)
220+
221+
_authStateEvent.trySend(AuthState.AUTHENTICATED)
213222
}
214223
}
215224
}

docs/auth-module.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# Auth Module
2+
3+
Self-contained authentication feature module using MVVM + Repository pattern.
4+
5+
## Structure
6+
7+
```
8+
auth/
9+
├── data/
10+
│ ├── api/ # Retrofit API service
11+
│ ├── datastore/ # Token persistence (DataStore)
12+
│ ├── di/ # Hilt modules
13+
│ ├── dto/ # API response models
14+
│ ├── repository/ # AuthRepository implementation
15+
│ ├── tokenholder/ # In-memory token storage
16+
│ └── workmanager/ # Background token refresh
17+
├── domain/
18+
│ ├── model/ # Domain models
19+
│ └── repository/ # Repository interface
20+
└── presentation/
21+
├── screen/ # Login screens (QR, Username)
22+
├── state/ # UI state classes
23+
└── viewmodel/ # AuthViewModel
24+
```
25+
26+
## API Endpoints
27+
28+
| Endpoint | Method | Purpose |
29+
|----------|--------|---------|
30+
| `/auth/login` | POST | Username/password login |
31+
| `/auth/pair?code=` | GET | QR code pairing |
32+
| `/auth/refresh` | POST | Token refresh |
33+
| `/auth/users` | GET | Get all users |
34+
| `/auth/profile/create` | POST | Create user |
35+
36+
## Authentication Flows
37+
38+
### Username/Password
39+
1. User enters server URL + credentials
40+
2. ViewModel validates input, calls API
41+
3. Tokens stored in DataStore + memory
42+
4. Navigate to home
43+
44+
### QR Code
45+
1. Scan QR from web client (format: `URL CODE`)
46+
2. API call to `/auth/pair`
47+
3. Same token storage and navigation
48+
49+
### Token Refresh
50+
- `TokenRefreshWorker` runs every 6 hours via WorkManager
51+
- Automatically refreshes tokens in background
52+
53+
## Key Classes
54+
55+
| Class | Location | Responsibility |
56+
|-------|----------|----------------|
57+
| `AuthApiService` | `data/api/` | Retrofit API calls |
58+
| `AuthTokensDataStore` | `data/datastore/` | Persistent token storage |
59+
| `AuthTokenHolder` | `data/tokenholder/` | In-memory token access |
60+
| `DataAuthRepository` | `data/repository/` | Coordinates API + storage |
61+
| `AuthViewModel` | `presentation/viewmodel/` | UI state management |
62+
| `LoginWithQrCode` | `presentation/screen/` | QR scanner screen |
63+
| `LoginWithUsername` | `presentation/screen/` | Form login screen |
64+
65+
## State Management
66+
67+
```kotlin
68+
data class AuthUiState(
69+
val baseUrl: String?,
70+
val username: String?,
71+
val password: String?,
72+
val authState: AuthState, // LOGGED_OUT | AUTHENTICATED
73+
val isLoading: Boolean,
74+
val authError: AuthError // None | InputError | LoginError
75+
)
76+
```
77+
78+
## Dependencies
79+
80+
- `:database` - User entity, DAOs
81+
- `:core` - Resource wrapper
82+
- `:uicomponent` - UI components
83+
84+
## Error Codes
85+
86+
- `401` - Incorrect password
87+
- `404` - User not found

0 commit comments

Comments
 (0)