Add notification preferences UI and subscription verification on launch

- Add NotificationPreferencesScreen (Android) and NotificationPreferencesView (iOS)
- Add NotificationPreferencesViewModel for shared business logic
- Wire up notification preferences from ProfileScreen on both platforms
- Add subscription verification on app launch for iOS (StoreKit) and Android (Google Play Billing)
- Update SubscriptionApi to match Go backend endpoints (/subscription/purchase/)
- Update StoreKit Configuration with correct product IDs and pricing ($2.99/month, $27.99/year)
- Update Android placeholder prices to match App Store pricing
- Fix NotificationPreference model to match Go backend schema

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-11-29 14:01:35 -06:00
parent 5a1a87fe8d
commit c748f792d0
21 changed files with 1032 additions and 38 deletions

View File

@@ -30,10 +30,12 @@ import com.example.casera.storage.ThemeStorage
import com.example.casera.storage.ThemeStorageManager
import com.example.casera.ui.theme.ThemeManager
import com.example.casera.fcm.FCMManager
import com.example.casera.platform.BillingManager
import kotlinx.coroutines.launch
class MainActivity : ComponentActivity(), SingletonImageLoader.Factory {
private var deepLinkResetToken by mutableStateOf<String?>(null)
private lateinit var billingManager: BillingManager
override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
@@ -49,12 +51,18 @@ class MainActivity : ComponentActivity(), SingletonImageLoader.Factory {
ThemeStorage.initialize(ThemeStorageManager.getInstance(applicationContext))
ThemeManager.initialize()
// Initialize BillingManager for subscription management
billingManager = BillingManager.getInstance(applicationContext)
// Handle deep link from intent
handleDeepLink(intent)
// Request notification permission and setup FCM
setupFCM()
// Verify subscriptions if user is authenticated
verifySubscriptionsOnLaunch()
setContent {
App(
deepLinkResetToken = deepLinkResetToken,
@@ -65,6 +73,36 @@ class MainActivity : ComponentActivity(), SingletonImageLoader.Factory {
}
}
/**
* Verify subscriptions with Google Play and sync with backend on app launch
*/
private fun verifySubscriptionsOnLaunch() {
val authToken = TokenStorage.getToken()
if (authToken == null) {
Log.d("MainActivity", "No auth token, skipping subscription verification")
return
}
Log.d("MainActivity", "🔄 Verifying subscriptions on launch...")
billingManager.startConnection(
onSuccess = {
Log.d("MainActivity", "✅ Billing connected, restoring purchases...")
lifecycleScope.launch {
val restored = billingManager.restorePurchases()
if (restored) {
Log.d("MainActivity", "✅ Subscriptions verified and synced with backend")
} else {
Log.d("MainActivity", "📦 No active subscriptions found")
}
}
},
onError = { error ->
Log.e("MainActivity", "❌ Failed to connect to billing: $error")
}
)
}
private fun setupFCM() {
// Request notification permission if needed
if (!FCMManager.isNotificationPermissionGranted(this)) {
@@ -86,9 +124,15 @@ class MainActivity : ComponentActivity(), SingletonImageLoader.Factory {
val authToken = TokenStorage.getToken()
if (authToken != null) {
val notificationApi = com.example.casera.network.NotificationApi()
val deviceId = android.provider.Settings.Secure.getString(
contentResolver,
android.provider.Settings.Secure.ANDROID_ID
)
val request = com.example.casera.models.DeviceRegistrationRequest(
deviceId = deviceId,
registrationId = fcmToken,
platform = "android"
platform = "android",
name = android.os.Build.MODEL
)
when (val result = notificationApi.registerDevice(authToken, request)) {

View File

@@ -33,9 +33,15 @@ class MyFirebaseMessagingService : FirebaseMessagingService() {
val authToken = com.example.casera.storage.TokenStorage.getToken()
if (authToken != null) {
val notificationApi = com.example.casera.network.NotificationApi()
val deviceId = android.provider.Settings.Secure.getString(
applicationContext.contentResolver,
android.provider.Settings.Secure.ANDROID_ID
)
val request = com.example.casera.models.DeviceRegistrationRequest(
deviceId = deviceId,
registrationId = token,
platform = "android"
platform = "android",
name = android.os.Build.MODEL
)
when (val result = notificationApi.registerDevice(authToken, request)) {

View File

@@ -350,6 +350,8 @@ class BillingManager private constructor(private val context: Context) {
true
} else {
Log.d(TAG, "No active purchases to restore")
// Still fetch subscription status from backend to get free tier limits
APILayer.getSubscriptionStatus(forceRefresh = true)
false
}
} else {