From 08e325b69e40dfcbeee65a1fd4734fa04a2b8742 Mon Sep 17 00:00:00 2001 From: Siro Devs Date: Fri, 10 Oct 2025 23:33:45 +0300 Subject: [PATCH 1/4] showing the paywall better in selection screen --- .../com/songlib/core/utils/AppConstants.kt | 1 - .../repository/PreferencesRepository.kt | 6 - .../components/action/UpgradeBanner.kt | 56 +++++++++ .../components/listitems/SongBook.kt | 14 +-- .../presentation/screens/home/HomeScreen.kt | 23 +--- .../screens/home/tabs/HomeListings.kt | 119 ++++++++++++++---- .../screens/selection/step1/Step1Content.kt | 3 +- .../screens/selection/step1/Step1Screen.kt | 50 ++++++++ .../presentation/viewmodels/HomeViewModel.kt | 25 +++- .../viewmodels/SplashViewModel.kt | 3 +- .../presentation/viewmodels/Step1ViewModel.kt | 51 +++++++- gradle/config/config.properties | 4 +- 12 files changed, 282 insertions(+), 73 deletions(-) create mode 100644 app/src/main/java/com/songlib/presentation/components/action/UpgradeBanner.kt diff --git a/app/src/main/java/com/songlib/core/utils/AppConstants.kt b/app/src/main/java/com/songlib/core/utils/AppConstants.kt index 3aee6c6..ccf709c 100644 --- a/app/src/main/java/com/songlib/core/utils/AppConstants.kt +++ b/app/src/main/java/com/songlib/core/utils/AppConstants.kt @@ -28,7 +28,6 @@ object PrefConstants { const val DATA_LOADED = "dataLoaded" const val SELECT_AFRESH = "selectAfresh" const val IS_PRO_USER = "isProUser" - const val CAN_SHOW_PAYWALL = "canShowPaywall" const val INSTALL_DATE = "install_date" const val REVIEW_REQUESTED = "review_requested" const val IS_USER_A_KID = "is_user_a_kid" diff --git a/app/src/main/java/com/songlib/domain/repository/PreferencesRepository.kt b/app/src/main/java/com/songlib/domain/repository/PreferencesRepository.kt index 330fc39..71c8b44 100644 --- a/app/src/main/java/com/songlib/domain/repository/PreferencesRepository.kt +++ b/app/src/main/java/com/songlib/domain/repository/PreferencesRepository.kt @@ -3,7 +3,6 @@ package com.songlib.domain.repository import android.content.Context import android.content.SharedPreferences import com.songlib.core.utils.PrefConstants -import androidx.core.content.edit import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.* @@ -14,7 +13,6 @@ class PreferencesRepository @Inject constructor( private val prefs = context.getSharedPreferences(PrefConstants.PREFERENCE_FILE, Context.MODE_PRIVATE) - // Existing properties var initialBooks: String get() = prefs.getString(PrefConstants.INITIAL_BOOKS, "") ?: "" set(value) = prefs.edit { putString(PrefConstants.INITIAL_BOOKS, value) } @@ -35,10 +33,6 @@ class PreferencesRepository @Inject constructor( get() = prefs.getBoolean(PrefConstants.IS_PRO_USER, false) set(value) = prefs.edit { putBoolean(PrefConstants.IS_PRO_USER, value) } - var canShowPaywall: Boolean - get() = prefs.getBoolean(PrefConstants.CAN_SHOW_PAYWALL, false) - set(value) = prefs.edit { putBoolean(PrefConstants.CAN_SHOW_PAYWALL, value) } - var isDataLoaded: Boolean get() = prefs.getBoolean(PrefConstants.DATA_LOADED, false) set(value) = prefs.edit { putBoolean(PrefConstants.DATA_LOADED, value) } diff --git a/app/src/main/java/com/songlib/presentation/components/action/UpgradeBanner.kt b/app/src/main/java/com/songlib/presentation/components/action/UpgradeBanner.kt new file mode 100644 index 0000000..5b571ec --- /dev/null +++ b/app/src/main/java/com/songlib/presentation/components/action/UpgradeBanner.kt @@ -0,0 +1,56 @@ +package com.songlib.presentation.components.action + +import androidx.compose.foundation.layout.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Star +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.* +import androidx.compose.ui.unit.dp + +@Composable +fun UpgradeBanner(onUpgradeClick: () -> Unit) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.primaryContainer + ), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Filled.Star, + contentDescription = "Pro feature", + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(20.dp) + ) + + Spacer(modifier = Modifier.width(8.dp)) + + Text( + text = "You are currently limited to only 1 listing", + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.weight(1f) + ) + + Spacer(modifier = Modifier.width(8.dp)) + + TextButton( + onClick = onUpgradeClick, + modifier = Modifier.height(32.dp) + ) { + Text( + text = "Upgrade to PRO", + style = MaterialTheme.typography.labelSmall + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/songlib/presentation/components/listitems/SongBook.kt b/app/src/main/java/com/songlib/presentation/components/listitems/SongBook.kt index 6ef2e67..72996bd 100644 --- a/app/src/main/java/com/songlib/presentation/components/listitems/SongBook.kt +++ b/app/src/main/java/com/songlib/presentation/components/listitems/SongBook.kt @@ -17,11 +17,11 @@ import com.songlib.core.utils.refineTitle import com.songlib.data.models.Book import com.songlib.data.sample.SampleBooks import com.songlib.domain.entity.Selectable - @Composable fun SongBook( item: Selectable, - onClick: (Selectable) -> Unit + onClick: (Selectable) -> Unit, + modifier: Modifier = Modifier ) { val bgColor = if (item.isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.inversePrimary @@ -29,7 +29,7 @@ fun SongBook( if (item.isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.scrim ElevatedCard( - modifier = Modifier + modifier = modifier .fillMaxWidth() .padding(2.dp) .clickable { onClick(item) }, @@ -41,16 +41,10 @@ fun SongBook( ) { Row( modifier = Modifier - .fillMaxWidth() + .fillMaxSize() .padding(5.dp), verticalAlignment = Alignment.CenterVertically ) { - Icon( - imageVector = if (item.isSelected) Icons.Filled.CheckBox else Icons.Filled.CheckBoxOutlineBlank, - contentDescription = null, - tint = txtColor, - modifier = Modifier.padding(end = 12.dp) - ) Text( text = buildAnnotatedString { withStyle(style = SpanStyle(fontSize = 16.sp, color = txtColor)) { diff --git a/app/src/main/java/com/songlib/presentation/screens/home/HomeScreen.kt b/app/src/main/java/com/songlib/presentation/screens/home/HomeScreen.kt index 9a3320d..d9b004d 100644 --- a/app/src/main/java/com/songlib/presentation/screens/home/HomeScreen.kt +++ b/app/src/main/java/com/songlib/presentation/screens/home/HomeScreen.kt @@ -30,32 +30,11 @@ fun HomeScreen( val selectedTab by viewModel.selectedTab.collectAsState() val songs by viewModel.songs.collectAsState(initial = emptyList()) - val canShowPaywall by viewModel.canShowPaywall.collectAsState() + val isProUser by viewModel.isProUser.collectAsState() var showPaywall by remember { mutableStateOf(false) } LaunchedEffect(Unit) { viewModel.fetchData() - showPaywall = canShowPaywall - } - - if (showPaywall) { - Dialog( - onDismissRequest = { showPaywall = false }, - properties = DialogProperties(usePlatformDefaultWidth = false) - ) { - val paywallOptions = remember { - PaywallOptions.Builder(dismissRequest = { showPaywall = false }) - .setShouldDisplayDismissButton(true) - .build() - } - Box() { - if (canShowPaywall) { - Paywall(paywallOptions) - } else { - CustomerCenter(onDismiss = { showPaywall = false }) - } - } - } } when (uiState) { diff --git a/app/src/main/java/com/songlib/presentation/screens/home/tabs/HomeListings.kt b/app/src/main/java/com/songlib/presentation/screens/home/tabs/HomeListings.kt index f9e239a..70b17da 100644 --- a/app/src/main/java/com/songlib/presentation/screens/home/tabs/HomeListings.kt +++ b/app/src/main/java/com/songlib/presentation/screens/home/tabs/HomeListings.kt @@ -3,11 +3,12 @@ package com.songlib.presentation.screens.home.tabs import androidx.compose.foundation.layout.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.* +import androidx.compose.ui.window.* import androidx.navigation.NavHostController +import com.revenuecat.purchases.ui.revenuecatui.* import com.songlib.presentation.screens.home.components.ListingsList import com.songlib.data.models.ListingUi import com.songlib.domain.entity.UiState @@ -27,9 +28,21 @@ fun HomeListings( val uiState by viewModel.uiState.collectAsState() var showAddAlert by remember { mutableStateOf(false) } var showDeleteAlert by remember { mutableStateOf(false) } + var showPaywall by remember { mutableStateOf(false) } val listings by viewModel.listings.collectAsState(initial = emptyList()) + val isProUser by viewModel.isProUser.collectAsState() + val showProLimitDialog by viewModel.showProLimitDialog.collectAsState() var selectedListings by remember { mutableStateOf>(emptySet()) } + LaunchedEffect(showAddAlert) { + if (showAddAlert) { + if (!isProUser && listings.size >= 3) { + showAddAlert = false + viewModel.checkAndHandleNewListing() + } + } + } + if (showAddAlert) { QuickFormDialog( title = "New Listing", @@ -44,23 +57,73 @@ fun HomeListings( if (showDeleteAlert) { ConfirmDialog( - title = "Delete this listing${selectedListings.size} == 1 ? 's' : ''", - message = "Are you sure you want to deleted the selected listings?", + title = "Delete ${if (selectedListings.size == 1) "this listing" else "these listings"}", + message = "Are you sure you want to delete the selected listing${if (selectedListings.size != 1) "s" else ""}?", onDismiss = { showDeleteAlert = false }, onConfirm = { viewModel.deleteListings(selectedListings) showDeleteAlert = false + selectedListings = emptySet() + } + ) + } + + if (showProLimitDialog) { + AlertDialog( + onDismissRequest = { viewModel.onProLimitDismiss() }, + title = { Text("Support us by upgrading") }, + text = { + Text("Please purchase a subscription if you want to continue using this feature and all other Pro features.") + }, + confirmButton = { + TextButton( + onClick = { + viewModel.onProLimitProceed() + showPaywall = true + } + ) { + Text("Upgrade") + } + }, + dismissButton = { + TextButton( + onClick = { viewModel.onProLimitDismiss() } + ) { + Text("Not Now") + } } ) } + if (showPaywall) { + Dialog( + onDismissRequest = { showPaywall = false }, + properties = DialogProperties(usePlatformDefaultWidth = false) + ) { + val paywallOptions = remember { + PaywallOptions.Builder(dismissRequest = { showPaywall = false }) + .setShouldDisplayDismissButton(true) + .build() + } + Box { + Paywall(paywallOptions) + } + } + } + Scaffold( topBar = { AppTopBar( title = if (selectedListings.isEmpty()) "Song Listings" else "${selectedListings.size} selected", actions = { if (selectedListings.isEmpty()) { - IconButton(onClick = { showAddAlert = true }) { + IconButton(onClick = { + if (!isProUser && listings.isNotEmpty()) { + viewModel.checkAndHandleNewListing() + } else { + showAddAlert = true + } + }) { Icon(Icons.Filled.Add, contentDescription = "New") } IconButton(onClick = { navController.navigate(Routes.SETTINGS) }) { @@ -83,26 +146,34 @@ fun HomeListings( .padding(innerPadding), contentAlignment = Alignment.Center ) { - when (uiState) { - is UiState.Filtered -> - if (listings.isEmpty()) { - EmptyState( - message = "Start adding lists of songs,\\nif you don't want to see this again", - messageIcon = Icons.Default.FormatListNumbered - ) - } else { - ListingsList( - listings = listings, - navController = navController, - selectedListings = selectedListings, - onListingSelected = { listing -> - selectedListings = - if (selectedListings.contains(listing)) selectedListings - listing - else selectedListings + listing - }, - ) - } - else -> EmptyState() + Column(modifier = Modifier.fillMaxSize()) { + if (!isProUser && listings.isNotEmpty()) { + UpgradeBanner( + onUpgradeClick = { showPaywall = true } + ) + } + + when (uiState) { + is UiState.Filtered -> + if (listings.isEmpty()) { + EmptyState( + message = "Start adding lists of songs,\nif you don't want to see this again", + messageIcon = Icons.Default.FormatListNumbered + ) + } else { + ListingsList( + listings = listings, + navController = navController, + selectedListings = selectedListings, + onListingSelected = { listing -> + selectedListings = + if (selectedListings.contains(listing)) selectedListings - listing + else selectedListings + listing + }, + ) + } + else -> EmptyState() + } } } } diff --git a/app/src/main/java/com/songlib/presentation/screens/selection/step1/Step1Content.kt b/app/src/main/java/com/songlib/presentation/screens/selection/step1/Step1Content.kt index 2e91020..aa36cf3 100644 --- a/app/src/main/java/com/songlib/presentation/screens/selection/step1/Step1Content.kt +++ b/app/src/main/java/com/songlib/presentation/screens/selection/step1/Step1Content.kt @@ -35,7 +35,8 @@ fun Step1Content( items(books) { book -> SongBook( item = book, - onClick = { onBookClick(book) } + onClick = { onBookClick(book) }, + modifier = Modifier.height(60.dp) ) } } diff --git a/app/src/main/java/com/songlib/presentation/screens/selection/step1/Step1Screen.kt b/app/src/main/java/com/songlib/presentation/screens/selection/step1/Step1Screen.kt index f23c985..9d7d80d 100644 --- a/app/src/main/java/com/songlib/presentation/screens/selection/step1/Step1Screen.kt +++ b/app/src/main/java/com/songlib/presentation/screens/selection/step1/Step1Screen.kt @@ -7,7 +7,11 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties import androidx.navigation.NavHostController +import com.revenuecat.purchases.ui.revenuecatui.Paywall +import com.revenuecat.purchases.ui.revenuecatui.PaywallOptions import com.songlib.domain.entity.UiState import com.songlib.domain.repository.* import com.songlib.presentation.components.action.AppTopBar @@ -25,6 +29,7 @@ fun Step1Screen( ) { var fetchData by rememberSaveable { mutableIntStateOf(0) } var showThemeDialog by remember { mutableStateOf(false) } + var showPaywall by remember { mutableStateOf(false) } if (fetchData == 0) { viewModel.fetchBooks() @@ -33,6 +38,8 @@ fun Step1Screen( val books by viewModel.books.collectAsState(initial = emptyList()) val uiState by viewModel.uiState.collectAsState() + val showUpgradeDialog by viewModel.showUpgradeDialog.collectAsState() + val isProUser by viewModel.isProUser.collectAsState() val theme = themeRepo.selectedTheme LaunchedEffect(uiState) { @@ -41,6 +48,49 @@ fun Step1Screen( } } + if (showUpgradeDialog) { + AlertDialog( + onDismissRequest = { viewModel.onUpgradeDismis() }, + title = { Text("You selected more than 3 Songbooks...") }, + text = { + Text("Please purchase a subscription if you want to have more than 3 songbooks in your collection.") + }, + confirmButton = { + TextButton( + onClick = { + viewModel.onUpgradeProceed() + showPaywall = true + } + ) { + Text("OKAY") + } + }, + dismissButton = { + TextButton( + onClick = { viewModel.onUpgradeDismis() } + ) { + Text("CANCEL") + } + } + ) + } + + if (showPaywall) { + Dialog( + onDismissRequest = { showPaywall = false }, + properties = DialogProperties(usePlatformDefaultWidth = false) + ) { + val paywallOptions = remember { + PaywallOptions.Builder(dismissRequest = { showPaywall = false }) + .setShouldDisplayDismissButton(true) + .build() + } + Box() { + Paywall(paywallOptions) + } + } + } + if (showThemeDialog) { ThemeSelectorDialog( current = theme, diff --git a/app/src/main/java/com/songlib/presentation/viewmodels/HomeViewModel.kt b/app/src/main/java/com/songlib/presentation/viewmodels/HomeViewModel.kt index 689a4bf..f06fdd4 100644 --- a/app/src/main/java/com/songlib/presentation/viewmodels/HomeViewModel.kt +++ b/app/src/main/java/com/songlib/presentation/viewmodels/HomeViewModel.kt @@ -43,8 +43,11 @@ class HomeViewModel @Inject constructor( private val _listings = MutableStateFlow>(emptyList()) val listings: StateFlow> get() = _listings - private val _canShowPaywall = MutableStateFlow(false) - val canShowPaywall: StateFlow = _canShowPaywall.asStateFlow() + private val _isProUser = MutableStateFlow(false) + val isProUser: StateFlow = _isProUser.asStateFlow() + + private val _showProLimitDialog = MutableStateFlow(false) + val showProLimitDialog: StateFlow = _showProLimitDialog.asStateFlow() fun setSelectedTab(tab: HomeNavItem) { _selectedTab.value = tab @@ -53,7 +56,7 @@ class HomeViewModel @Inject constructor( fun fetchData() { _uiState.tryEmit(UiState.Loading) viewModelScope.launch { - _canShowPaywall.value = prefsRepo.canShowPaywall + _isProUser.value = prefsRepo.isProUser _books.value = songbkRepo.fetchLocalBooks() _songs.value = songbkRepo.fetchLocalSongs() _listings.value = listRepo.fetchListings(0) @@ -146,6 +149,22 @@ class HomeViewModel @Inject constructor( _uiState.emit(UiState.Filtered) } } + fun checkAndHandleNewListing() { + val currentListingsCount = listings.value.size + if (!_isProUser.value && currentListingsCount >= 1) { + _showProLimitDialog.value = true + } else { + _showProLimitDialog.value = false + } + } + + fun onProLimitProceed() { + _showProLimitDialog.value = false + } + + fun onProLimitDismiss() { + _showProLimitDialog.value = false + } fun clearData() { viewModelScope.launch { diff --git a/app/src/main/java/com/songlib/presentation/viewmodels/SplashViewModel.kt b/app/src/main/java/com/songlib/presentation/viewmodels/SplashViewModel.kt index d5f8f08..b13c2c2 100644 --- a/app/src/main/java/com/songlib/presentation/viewmodels/SplashViewModel.kt +++ b/app/src/main/java/com/songlib/presentation/viewmodels/SplashViewModel.kt @@ -39,10 +39,9 @@ class SplashViewModel @Inject constructor( } private suspend fun checkSubscriptionAndTime(isOnline: Boolean) { - if (!prefsRepo.isProUser && prefsRepo.hasTimeExceeded(5)) { + if (!prefsRepo.isProUser) { subsRepo.isProUser(isOnline) { isActive -> prefsRepo.isProUser = isActive - prefsRepo.canShowPaywall = !isActive } } prefsRepo.updateAppOpenTime() diff --git a/app/src/main/java/com/songlib/presentation/viewmodels/Step1ViewModel.kt b/app/src/main/java/com/songlib/presentation/viewmodels/Step1ViewModel.kt index 2ff0673..85124d9 100644 --- a/app/src/main/java/com/songlib/presentation/viewmodels/Step1ViewModel.kt +++ b/app/src/main/java/com/songlib/presentation/viewmodels/Step1ViewModel.kt @@ -14,6 +14,7 @@ import javax.inject.Inject @HiltViewModel class Step1ViewModel @Inject constructor( private val prefsRepo: PreferencesRepository, + private val subsRepo: SubscriptionsRepository, private val songbkRepo: SongBookRepository, ) : ViewModel() { private val _uiState = MutableStateFlow(UiState.Loading) @@ -22,6 +23,15 @@ class Step1ViewModel @Inject constructor( private val _books = MutableStateFlow>>(emptyList()) val books: StateFlow>> get() = _books + private val _showUpgradeDialog = MutableStateFlow(false) + val showUpgradeDialog: StateFlow = _showUpgradeDialog.asStateFlow() + + private val _pendingBookSelection = MutableStateFlow?>(null) + val pendingBookSelection: StateFlow?> = _pendingBookSelection.asStateFlow() + + private val _isProUser = MutableStateFlow(false) + val isProUser: StateFlow = _isProUser.asStateFlow() + private fun getSelectedIds(): Set = prefsRepo.selectedBooks .split(",") @@ -46,6 +56,7 @@ class Step1ViewModel @Inject constructor( } _books.emit(selectableBooks) Log.d("TAG", "${_books.value.size} books fetched") + _isProUser.value = prefsRepo.isProUser _uiState.tryEmit(UiState.Loaded) } } @@ -59,6 +70,15 @@ class Step1ViewModel @Inject constructor( saveBooks(getSelectedBookList()) } + private suspend fun refreshSubscription(isOnline: Boolean) { + if (!prefsRepo.isProUser) { + subsRepo.isProUser(isOnline) { isActive -> + prefsRepo.isProUser = isActive + _isProUser.value = isActive + } + } + } + private fun saveBooks(books: List) { _uiState.tryEmit(UiState.Saving) Log.d("TAG", "saving books") @@ -91,8 +111,35 @@ class Step1ViewModel @Inject constructor( } fun toggleBookSelection(book: Selectable) { - _books.value = _books.value.map { - if (it.data.bookId == book.data.bookId) it.copy(isSelected = !it.isSelected) else it + val currentSelectedCount = _books.value.count { it.isSelected } + val isCurrentlySelected = book.isSelected + + if (isCurrentlySelected || _isProUser.value) { + _books.value = _books.value.map { + if (it.data.bookId == book.data.bookId) it.copy(isSelected = !it.isSelected) else it + } + return } + + if (currentSelectedCount >= 3) { + _pendingBookSelection.value = book + _showUpgradeDialog.value = true + } else { + _books.value = _books.value.map { + if (it.data.bookId == book.data.bookId) it.copy(isSelected = !it.isSelected) else it + } + } + } + + fun onUpgradeProceed() { + _showUpgradeDialog.value = false + _pendingBookSelection.value = null +// refreshSubscription(on) + } + + fun onUpgradeDismis() { + _showUpgradeDialog.value = false + _pendingBookSelection.value = null } + } diff --git a/gradle/config/config.properties b/gradle/config/config.properties index 849043d..da7b44d 100644 --- a/gradle/config/config.properties +++ b/gradle/config/config.properties @@ -1,5 +1,5 @@ applicationId=com.songlib -versionName=1.0.817 -versionCode=817 +versionName=1.0.818 +versionCode=818 targetSdk=35 minSdk=24 \ No newline at end of file From ae31f1ee3eab3a8f6e83d0a402e20a0ef7df7936 Mon Sep 17 00:00:00 2001 From: Siro Devs Date: Tue, 25 Nov 2025 23:40:20 +0300 Subject: [PATCH 2/4] minor fixes --- .github/workflows/deploy-to-playstore.yml | 45 +++++++++++++++++++ .../presentation/components/action/Dialogs.kt | 32 +++++++++++++ .../presentation/screens/home/HomeScreen.kt | 12 +++-- .../home/components/ChooseListingSheet.kt | 43 ++++++++++++++++-- .../screens/home/components/HomeAppBar.kt | 32 +++++++++---- .../screens/home/tabs/HomeListings.kt | 27 +++-------- .../screens/home/tabs/HomeSearch.kt | 33 ++++++++++++++ .../presentation/viewmodels/HomeViewModel.kt | 44 ++++++++++++------ .../viewmodels/SplashViewModel.kt | 6 +-- app/src/main/res/xml/backup_rules.xml | 12 +---- gradle/config/config.properties | 4 +- 11 files changed, 220 insertions(+), 70 deletions(-) create mode 100644 .github/workflows/deploy-to-playstore.yml create mode 100644 app/src/main/java/com/songlib/presentation/components/action/Dialogs.kt diff --git a/.github/workflows/deploy-to-playstore.yml b/.github/workflows/deploy-to-playstore.yml new file mode 100644 index 0000000..8751c0f --- /dev/null +++ b/.github/workflows/deploy-to-playstore.yml @@ -0,0 +1,45 @@ +#name: Deploy to Play Store +# +#on: +# push: +# branches: [ main ] +# +#jobs: +# build-and-deploy: +# runs-on: ubuntu-latest +# +# steps: +# - name: Checkout code +# uses: actions/checkout@v4 +# +# - name: Set up JDK +# uses: actions/setup-java@v3 +# with: +# java-version: '17' +# distribution: 'temurin' +# +# - name: Setup Android SDK +# uses: android-actions/setup-android@v3 +# +# - name: Build APK/AAB +# run: | +# chmod +x gradlew +# ./gradlew assembleRelease +# +# - name: Build App Bundle +# run: ./gradlew bundleRelease +# +# - name: Upload artifacts +# uses: actions/upload-artifact@v3 +# with: +# name: app-bundle +# path: app/build/outputs/ +# +# - name: Deploy to Play Store +# uses: r0adkll/upload-google-play@v1 +# with: +# serviceAccountJsonPlainText: ${{ secrets.GCP_SERVICE_ACCOUNT_KEY }} +# packageName: com.yourcompany.yourapp +# releaseFiles: app/build/outputs/bundle/release/app-release.aab +# track: internal +# status: completed \ No newline at end of file diff --git a/app/src/main/java/com/songlib/presentation/components/action/Dialogs.kt b/app/src/main/java/com/songlib/presentation/components/action/Dialogs.kt new file mode 100644 index 0000000..ca79b53 --- /dev/null +++ b/app/src/main/java/com/songlib/presentation/components/action/Dialogs.kt @@ -0,0 +1,32 @@ +package com.songlib.presentation.components.action + +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable + +@Composable +fun ProLimitDialog( + onDismiss: () -> Unit, + onUpgrade: () -> Unit, + title: String = "Support us by upgrading", + message: String = "Please purchase a subscription if you want to continue using this feature and all other Pro features.", + upgradeButtonText: String = "Upgrade", + dismissButtonText: String = "Not Now" +) { + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(title) }, + text = { Text(message) }, + confirmButton = { + TextButton(onClick = onUpgrade) { + Text(upgradeButtonText) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(dismissButtonText) + } + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/songlib/presentation/screens/home/HomeScreen.kt b/app/src/main/java/com/songlib/presentation/screens/home/HomeScreen.kt index d9b004d..8fc9f8b 100644 --- a/app/src/main/java/com/songlib/presentation/screens/home/HomeScreen.kt +++ b/app/src/main/java/com/songlib/presentation/screens/home/HomeScreen.kt @@ -30,9 +30,6 @@ fun HomeScreen( val selectedTab by viewModel.selectedTab.collectAsState() val songs by viewModel.songs.collectAsState(initial = emptyList()) - val isProUser by viewModel.isProUser.collectAsState() - var showPaywall by remember { mutableStateOf(false) } - LaunchedEffect(Unit) { viewModel.fetchData() } @@ -61,10 +58,11 @@ fun HomeScreen( message = "It appears you didn't finish your songbook selection, that's why it's empty here at the moment.\n\nLet's fix that asap!", messageIcon = Icons.Default.EditNote, onAction = { - viewModel.clearData() - navController.navigate(Routes.SPLASH) { - popUpTo(0) { inclusive = true } - launchSingleTop = true + if (viewModel.clearData()) { + navController.navigate(Routes.SPLASH) { + popUpTo(0) { inclusive = true } + launchSingleTop = true + } } } ) diff --git a/app/src/main/java/com/songlib/presentation/screens/home/components/ChooseListingSheet.kt b/app/src/main/java/com/songlib/presentation/screens/home/components/ChooseListingSheet.kt index 14c01ec..9e78991 100644 --- a/app/src/main/java/com/songlib/presentation/screens/home/components/ChooseListingSheet.kt +++ b/app/src/main/java/com/songlib/presentation/screens/home/components/ChooseListingSheet.kt @@ -17,6 +17,7 @@ import com.songlib.data.models.ListingUi @Composable fun ChoosingListingSheet( listings: List, + isProUser: Boolean, onDismiss: () -> Unit, onNewListClick: () -> Unit, onListingClick: (ListingUi) -> Unit, @@ -39,16 +40,51 @@ fun ChoosingListingSheet( .padding(bottom = 12.dp) ) + // Upgrade banner for free users with existing listings + if (!isProUser && listings.isNotEmpty()) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.primaryContainer + ), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Filled.Star, + contentDescription = "Pro feature", + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(20.dp) + ) + + Spacer(modifier = Modifier.width(8.dp)) + + Text( + text = "Free users can only have 1 listing", + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.weight(1f) + ) + } + } + } + LazyColumn( modifier = Modifier .weight(1f, fill = false) .padding(bottom = 16.dp) ) { - item() { + item { Row( modifier = Modifier .fillMaxWidth() - .clickable { onNewListClick } + .clickable { onNewListClick() } .padding(vertical = 12.dp), verticalAlignment = Alignment.CenterVertically ) { @@ -76,7 +112,7 @@ fun ChoosingListingSheet( Row( modifier = Modifier .fillMaxWidth() - .clickable { onListingClick } + .clickable { onListingClick(listing) } .padding(vertical = 12.dp), verticalAlignment = Alignment.CenterVertically ) { @@ -109,4 +145,3 @@ fun ChoosingListingSheet( } } } - diff --git a/app/src/main/java/com/songlib/presentation/screens/home/components/HomeAppBar.kt b/app/src/main/java/com/songlib/presentation/screens/home/components/HomeAppBar.kt index 993d1fb..ffbe25e 100644 --- a/app/src/main/java/com/songlib/presentation/screens/home/components/HomeAppBar.kt +++ b/app/src/main/java/com/songlib/presentation/screens/home/components/HomeAppBar.kt @@ -4,7 +4,9 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* import androidx.compose.material3.* import androidx.compose.runtime.* +import androidx.compose.ui.platform.LocalContext import com.songlib.data.models.Song +import com.songlib.domain.repository.PreferencesRepository import com.songlib.presentation.components.action.AppTopBar import com.songlib.presentation.components.general.QuickFormDialog import com.songlib.presentation.viewmodels.HomeViewModel @@ -19,8 +21,12 @@ fun HomeSearchAppBar( onShareClick: () -> Unit, onClearSelection: () -> Unit, ) { + val context = LocalContext.current + val prefs = remember { PreferencesRepository(context) } var showAddDialog by remember { mutableStateOf(false) } var showListingSheet by remember { mutableStateOf(false) } + val isProUser by viewModel.isProUser.collectAsState() + val listings by viewModel.listings.collectAsState(initial = emptyList()) if (showAddDialog) { QuickFormDialog( @@ -28,31 +34,37 @@ fun HomeSearchAppBar( label = "Listing title", onDismiss = { showAddDialog = false }, onConfirm = { title -> - viewModel.saveListing(title) - showAddDialog = false + if (viewModel.checkAndHandleNewListing()) { + viewModel.saveListing(title) + showAddDialog = false + } } ) } if (showListingSheet) { - val listings by viewModel.listings.collectAsState(initial = emptyList()) ChoosingListingSheet( listings = listings, + isProUser = isProUser, onDismiss = { showListingSheet = false }, onNewListClick = { - + showListingSheet = false + if (viewModel.checkAndHandleNewListing()) { + showAddDialog = true + } }, onListingClick = { listing -> viewModel.saveListItems(listing, selectedSongs) showListingSheet = false - onClearSelection + onClearSelection() }, onDone = { showListingSheet = false } ) } AppTopBar( - title = if (selectedSongs.isEmpty()) "SongLib" else "${selectedSongs.size} selected", +// title = if (selectedSongs.isEmpty()) "SongLib" else "${selectedSongs.size} selected", + title = if (prefs.isDataLoaded) "True" else "False", actions = { if (selectedSongs.isEmpty()) { IconButton(onClick = onSearchClick) { @@ -73,7 +85,12 @@ fun HomeSearchAppBar( } } IconButton( - onClick = { showListingSheet = true } + onClick = { + // Only show listing sheet if we have songs selected + if (selectedSongs.isNotEmpty()) { + showListingSheet = true + } + } ) { Icon(Icons.Default.FormatListNumbered, contentDescription = "Listing") } @@ -83,4 +100,3 @@ fun HomeSearchAppBar( onNavIconClick = onClearSelection ) } - diff --git a/app/src/main/java/com/songlib/presentation/screens/home/tabs/HomeListings.kt b/app/src/main/java/com/songlib/presentation/screens/home/tabs/HomeListings.kt index 70b17da..c301a00 100644 --- a/app/src/main/java/com/songlib/presentation/screens/home/tabs/HomeListings.kt +++ b/app/src/main/java/com/songlib/presentation/screens/home/tabs/HomeListings.kt @@ -69,28 +69,11 @@ fun HomeListings( } if (showProLimitDialog) { - AlertDialog( - onDismissRequest = { viewModel.onProLimitDismiss() }, - title = { Text("Support us by upgrading") }, - text = { - Text("Please purchase a subscription if you want to continue using this feature and all other Pro features.") - }, - confirmButton = { - TextButton( - onClick = { - viewModel.onProLimitProceed() - showPaywall = true - } - ) { - Text("Upgrade") - } - }, - dismissButton = { - TextButton( - onClick = { viewModel.onProLimitDismiss() } - ) { - Text("Not Now") - } + ProLimitDialog( + onDismiss = { viewModel.onProLimitDismiss() }, + onUpgrade = { + viewModel.onProLimitProceed() + showPaywall = true } ) } diff --git a/app/src/main/java/com/songlib/presentation/screens/home/tabs/HomeSearch.kt b/app/src/main/java/com/songlib/presentation/screens/home/tabs/HomeSearch.kt index 4a2815d..d5c0a62 100644 --- a/app/src/main/java/com/songlib/presentation/screens/home/tabs/HomeSearch.kt +++ b/app/src/main/java/com/songlib/presentation/screens/home/tabs/HomeSearch.kt @@ -13,6 +13,10 @@ import com.songlib.presentation.components.action.* import com.songlib.presentation.navigation.Routes import com.songlib.presentation.viewmodels.HomeViewModel import androidx.compose.ui.Modifier +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import com.revenuecat.purchases.ui.revenuecatui.Paywall +import com.revenuecat.purchases.ui.revenuecatui.PaywallOptions import com.songlib.data.models.Song import com.songlib.presentation.components.indicators.EmptyState import com.songlib.presentation.screens.home.components.* @@ -28,6 +32,35 @@ fun HomeSearch( var searchQry by rememberSaveable { mutableStateOf("") } val songs by viewModel.filtered.collectAsState(initial = emptyList()) var selectedSongs by remember { mutableStateOf>(emptySet()) } + var showPaywall by remember { mutableStateOf(false) } + val isProUser by viewModel.isProUser.collectAsState() + val showProLimitDialog by viewModel.showProLimitDialog.collectAsState() + + if (showProLimitDialog) { + ProLimitDialog( + onDismiss = { viewModel.onProLimitDismiss() }, + onUpgrade = { + viewModel.onProLimitProceed() + showPaywall = true + } + ) + } + + if (showPaywall) { + Dialog( + onDismissRequest = { showPaywall = false }, + properties = DialogProperties(usePlatformDefaultWidth = false) + ) { + val paywallOptions = remember { + PaywallOptions.Builder(dismissRequest = { showPaywall = false }) + .setShouldDisplayDismissButton(true) + .build() + } + Box { + Paywall(paywallOptions) + } + } + } Scaffold( topBar = { diff --git a/app/src/main/java/com/songlib/presentation/viewmodels/HomeViewModel.kt b/app/src/main/java/com/songlib/presentation/viewmodels/HomeViewModel.kt index f06fdd4..c03cee4 100644 --- a/app/src/main/java/com/songlib/presentation/viewmodels/HomeViewModel.kt +++ b/app/src/main/java/com/songlib/presentation/viewmodels/HomeViewModel.kt @@ -149,13 +149,9 @@ class HomeViewModel @Inject constructor( _uiState.emit(UiState.Filtered) } } - fun checkAndHandleNewListing() { - val currentListingsCount = listings.value.size - if (!_isProUser.value && currentListingsCount >= 1) { - _showProLimitDialog.value = true - } else { - _showProLimitDialog.value = false - } + + fun checkAndHandleNewListing(): Boolean { + return !_isProUser.value && listings.value.isNotEmpty() } fun onProLimitProceed() { @@ -166,13 +162,33 @@ class HomeViewModel @Inject constructor( _showProLimitDialog.value = false } - fun clearData() { - viewModelScope.launch { - songbkRepo.deleteAllData() - listRepo.deleteAllListings() - prefsRepo.isDataLoaded = false - prefsRepo.isDataSelected = false - prefsRepo.selectedBooks = "" + fun clearData(): Boolean { + return try { + _uiState.tryEmit(UiState.Loading) + viewModelScope.launch(Dispatchers.IO) { + songbkRepo.deleteAllData() + listRepo.deleteAllListings() + + withContext(Dispatchers.Main) { + prefsRepo.isDataLoaded = false + prefsRepo.isDataSelected = false + prefsRepo.selectAfresh = false + prefsRepo.initialBooks = "" + prefsRepo.selectedBooks = "" + } + + _books.value = emptyList() + _songs.value = emptyList() + _filtered.value = emptyList() + _likes.value = emptyList() + _listings.value = emptyList() + } + _uiState.tryEmit(UiState.Loaded) + true + } catch (e: Exception) { + _uiState.tryEmit(UiState.Error("Error clearing data")) + Log.e("HomeViewModel", "Error clearing data", e) + false } } } diff --git a/app/src/main/java/com/songlib/presentation/viewmodels/SplashViewModel.kt b/app/src/main/java/com/songlib/presentation/viewmodels/SplashViewModel.kt index b13c2c2..4063856 100644 --- a/app/src/main/java/com/songlib/presentation/viewmodels/SplashViewModel.kt +++ b/app/src/main/java/com/songlib/presentation/viewmodels/SplashViewModel.kt @@ -50,9 +50,9 @@ class SplashViewModel @Inject constructor( private fun determineNextRoute() { _nextRoute.value = when { prefsRepo.selectAfresh -> Routes.STEP_1 - prefsRepo.isDataLoaded -> Routes.HOME - prefsRepo.isDataSelected -> Routes.STEP_2 - else -> Routes.STEP_1 + prefsRepo.isDataSelected && prefsRepo.isDataLoaded -> Routes.HOME + prefsRepo.isDataSelected && !prefsRepo.isDataLoaded -> Routes.STEP_2 + else -> Routes.HOME } } } diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml index fa0f996..c9d2214 100644 --- a/app/src/main/res/xml/backup_rules.xml +++ b/app/src/main/res/xml/backup_rules.xml @@ -1,13 +1,5 @@ - + - + \ No newline at end of file diff --git a/gradle/config/config.properties b/gradle/config/config.properties index da7b44d..28cc3de 100644 --- a/gradle/config/config.properties +++ b/gradle/config/config.properties @@ -1,5 +1,5 @@ applicationId=com.songlib -versionName=1.0.818 -versionCode=818 +versionName=1.0.820 +versionCode=820 targetSdk=35 minSdk=24 \ No newline at end of file From 1a82b8773dfc44736fa65e483f0e6eda9b263fdb Mon Sep 17 00:00:00 2001 From: Siro Devs Date: Tue, 25 Nov 2025 23:46:03 +0300 Subject: [PATCH 3/4] minor fixes --- .../components/indicators/EmptyState.kt | 2 +- .../presentation/screens/home/HomeScreen.kt | 10 +++--- .../presentation/viewmodels/HomeViewModel.kt | 35 +++++++++++-------- 3 files changed, 27 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/com/songlib/presentation/components/indicators/EmptyState.kt b/app/src/main/java/com/songlib/presentation/components/indicators/EmptyState.kt index d6e31e4..efcb516 100644 --- a/app/src/main/java/com/songlib/presentation/components/indicators/EmptyState.kt +++ b/app/src/main/java/com/songlib/presentation/components/indicators/EmptyState.kt @@ -24,7 +24,7 @@ fun EmptyState( message: String? = null, messageIcon: ImageVector? = null, actionTitle: String? = "Retry", - onAction: (() -> Unit)? = null, + onAction: @Composable (() -> Unit)? = null, titleColor: Color = MaterialTheme.colorScheme.primary, messageColor: Color = MaterialTheme.colorScheme.secondary, spacing: Dp = 20.dp diff --git a/app/src/main/java/com/songlib/presentation/screens/home/HomeScreen.kt b/app/src/main/java/com/songlib/presentation/screens/home/HomeScreen.kt index 8fc9f8b..21735d4 100644 --- a/app/src/main/java/com/songlib/presentation/screens/home/HomeScreen.kt +++ b/app/src/main/java/com/songlib/presentation/screens/home/HomeScreen.kt @@ -58,10 +58,12 @@ fun HomeScreen( message = "It appears you didn't finish your songbook selection, that's why it's empty here at the moment.\n\nLet's fix that asap!", messageIcon = Icons.Default.EditNote, onAction = { - if (viewModel.clearData()) { - navController.navigate(Routes.SPLASH) { - popUpTo(0) { inclusive = true } - launchSingleTop = true + viewModel.clearData { success -> + if (success) { + navController.navigate(Routes.SPLASH) { + popUpTo(0) { inclusive = true } + launchSingleTop = true + } } } } diff --git a/app/src/main/java/com/songlib/presentation/viewmodels/HomeViewModel.kt b/app/src/main/java/com/songlib/presentation/viewmodels/HomeViewModel.kt index c03cee4..7d2a43c 100644 --- a/app/src/main/java/com/songlib/presentation/viewmodels/HomeViewModel.kt +++ b/app/src/main/java/com/songlib/presentation/viewmodels/HomeViewModel.kt @@ -162,10 +162,11 @@ class HomeViewModel @Inject constructor( _showProLimitDialog.value = false } - fun clearData(): Boolean { - return try { - _uiState.tryEmit(UiState.Loading) - viewModelScope.launch(Dispatchers.IO) { + fun clearData(onComplete: (Boolean) -> Unit) { + _uiState.tryEmit(UiState.Loading) + + viewModelScope.launch(Dispatchers.IO) { + try { songbkRepo.deleteAllData() listRepo.deleteAllListings() @@ -177,18 +178,22 @@ class HomeViewModel @Inject constructor( prefsRepo.selectedBooks = "" } - _books.value = emptyList() - _songs.value = emptyList() - _filtered.value = emptyList() - _likes.value = emptyList() - _listings.value = emptyList() + withContext(Dispatchers.Main) { + _books.value = emptyList() + _songs.value = emptyList() + _filtered.value = emptyList() + _likes.value = emptyList() + _listings.value = emptyList() + _uiState.tryEmit(UiState.Loaded) + } + + onComplete(true) + } catch (e: Exception) { + _uiState.tryEmit(UiState.Error("Error clearing data")) + Log.e("HomeViewModel", "Error clearing data", e) + onComplete(false) } - _uiState.tryEmit(UiState.Loaded) - true - } catch (e: Exception) { - _uiState.tryEmit(UiState.Error("Error clearing data")) - Log.e("HomeViewModel", "Error clearing data", e) - false } } + } From a6f26d2423554f6e3c81e5b45a75a8a1e34b9643 Mon Sep 17 00:00:00 2001 From: Siro Devs Date: Tue, 25 Nov 2025 23:53:32 +0300 Subject: [PATCH 4/4] minor fixes --- .kotlin/sessions/kotlin-compiler-18110105652542382796.salive | 0 .../songlib/presentation/components/indicators/EmptyState.kt | 4 +--- 2 files changed, 1 insertion(+), 3 deletions(-) create mode 100644 .kotlin/sessions/kotlin-compiler-18110105652542382796.salive diff --git a/.kotlin/sessions/kotlin-compiler-18110105652542382796.salive b/.kotlin/sessions/kotlin-compiler-18110105652542382796.salive new file mode 100644 index 0000000..e69de29 diff --git a/app/src/main/java/com/songlib/presentation/components/indicators/EmptyState.kt b/app/src/main/java/com/songlib/presentation/components/indicators/EmptyState.kt index efcb516..e612d21 100644 --- a/app/src/main/java/com/songlib/presentation/components/indicators/EmptyState.kt +++ b/app/src/main/java/com/songlib/presentation/components/indicators/EmptyState.kt @@ -1,6 +1,5 @@ package com.songlib.presentation.components.indicators -import android.graphics.Bitmap import androidx.compose.foundation.Image import androidx.compose.foundation.layout.* import androidx.compose.material3.* @@ -13,7 +12,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import com.songlib.R @@ -24,7 +22,7 @@ fun EmptyState( message: String? = null, messageIcon: ImageVector? = null, actionTitle: String? = "Retry", - onAction: @Composable (() -> Unit)? = null, + onAction: (() -> Unit)? = null, titleColor: Color = MaterialTheme.colorScheme.primary, messageColor: Color = MaterialTheme.colorScheme.secondary, spacing: Dp = 20.dp