diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 39cb9bf..9f8938b 100755 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -38,6 +38,9 @@ + diff --git a/app/src/main/java/com/dscvit/vitty/activity/AuthActivity.kt b/app/src/main/java/com/dscvit/vitty/activity/AuthActivity.kt index 7882045..a58db1c 100755 --- a/app/src/main/java/com/dscvit/vitty/activity/AuthActivity.kt +++ b/app/src/main/java/com/dscvit/vitty/activity/AuthActivity.kt @@ -10,6 +10,7 @@ import android.view.View import android.widget.Toast import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.OnBackPressedCallback import androidx.appcompat.app.AppCompatActivity import androidx.core.content.edit import androidx.databinding.DataBindingUtil @@ -24,6 +25,7 @@ import com.dscvit.vitty.util.Constants.TOKEN import com.dscvit.vitty.util.Constants.UID import com.dscvit.vitty.util.Constants.USER_INFO import com.dscvit.vitty.util.NotificationPermissionHelper +import com.dscvit.vitty.util.MaintenanceChecker import com.google.android.gms.auth.api.signin.GoogleSignIn import com.google.android.gms.auth.api.signin.GoogleSignInAccount import com.google.android.gms.auth.api.signin.GoogleSignInClient @@ -62,6 +64,22 @@ class AuthActivity : AppCompatActivity() { configureGoogleSignIn() setupUI() + setupBackPressedHandler() + } + + private fun setupBackPressedHandler() { + val callback = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + binding.apply { + if (introPager.currentItem == 0 || loginClick) { + finish() + } else { + introPager.currentItem-- + } + } + } + } + onBackPressedDispatcher.addCallback(this, callback) } private fun setupNotificationPermissionLauncher() { @@ -266,8 +284,9 @@ class AuthActivity : AppCompatActivity() { val email = firebaseAuth.currentUser?.email Timber.d("Firebase authentication successful - uid: $uid, email: $email") saveInfo(acct.idToken, uid) - authViewModel.signInAndGetTimeTable("", "", uid ?: "", "") - leadToNextPage() + + // Quick maintenance check for new login + checkMaintenanceBeforeProceed() } else { Timber.e("Firebase authentication failed: ${authResult.exception?.message}") logoutFailed() @@ -278,6 +297,20 @@ class AuthActivity : AppCompatActivity() { } } + private fun checkMaintenanceBeforeProceed() { + MaintenanceChecker.checkMaintenanceStatusAsync(this) { isUnderMaintenance -> + if (isUnderMaintenance) { + binding.loadingView.visibility = View.GONE + val intent = Intent(this, MaintenanceActivity::class.java) + startActivity(intent) + finish() + } else { + authViewModel.signInAndGetTimeTable("", "", firebaseAuth.currentUser?.uid ?: "", "") + leadToNextPage() + } + } + } + private fun leadToNextPage() { authViewModel.signInResponse.observe(this) { if (it != null) { @@ -323,14 +356,4 @@ class AuthActivity : AppCompatActivity() { } } } - - override fun onBackPressed() { - binding.apply { - if (introPager.currentItem == 0 || loginClick) { - super.onBackPressed() - } else { - introPager.currentItem-- - } - } - } } diff --git a/app/src/main/java/com/dscvit/vitty/activity/MaintenanceActivity.kt b/app/src/main/java/com/dscvit/vitty/activity/MaintenanceActivity.kt new file mode 100644 index 0000000..1b4fef1 --- /dev/null +++ b/app/src/main/java/com/dscvit/vitty/activity/MaintenanceActivity.kt @@ -0,0 +1,52 @@ +package com.dscvit.vitty.activity + +import android.content.Intent +import android.os.Bundle +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.databinding.DataBindingUtil +import androidx.fragment.app.FragmentActivity +import com.dscvit.vitty.R +import com.dscvit.vitty.databinding.ActivityMaintenanceBinding +import com.dscvit.vitty.theme.VittyTheme +import com.dscvit.vitty.ui.maintenance.MaintenanceScreen +import com.dscvit.vitty.util.MaintenanceChecker + +class MaintenanceActivity : FragmentActivity() { + + private lateinit var binding: ActivityMaintenanceBinding + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + binding = DataBindingUtil.setContentView(this, R.layout.activity_maintenance) + + binding.composeView.apply { + setViewCompositionStrategy( + ViewCompositionStrategy.DisposeOnLifecycleDestroyed(this@MaintenanceActivity) + ) + setContent { + VittyTheme { + MaintenanceScreen( + onRetryClick = { retryConnection() }, + onExitClick = { exitApp() } + ) + } + } + } + } + + private fun retryConnection() { + MaintenanceChecker.checkMaintenanceStatusAsync(this) { isUnderMaintenance -> + if (!isUnderMaintenance) { + val intent = Intent(this, InstructionsActivity::class.java) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + startActivity(intent) + finish() + } + } + } + + private fun exitApp() { + finishAffinity() + } +} diff --git a/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunity.kt b/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunity.kt index bbc5bfe..f3a0380 100644 --- a/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunity.kt +++ b/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunity.kt @@ -29,6 +29,9 @@ import retrofit2.http.Path import retrofit2.http.Query interface APICommunity { + @GET("/") + fun checkServerStatus(): Call + @Headers("Content-Type: application/json") @POST("/api/v3/auth/check-username") fun checkUsername( diff --git a/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunityRestClient.kt b/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunityRestClient.kt index d9ef161..5ef302a 100644 --- a/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunityRestClient.kt +++ b/app/src/main/java/com/dscvit/vitty/network/api/community/APICommunityRestClient.kt @@ -1050,4 +1050,30 @@ class APICommunityRestClient { }, ) } + + fun checkServerStatus( + retrofitServerStatusListener: RetrofitServerStatusListener, + ) { + mApiUser = retrofit.create(APICommunity::class.java) + val apiServerStatusCall = mApiUser!!.checkServerStatus() + apiServerStatusCall.enqueue( + object : Callback { + override fun onResponse( + call: Call, + response: Response, + ) { + Timber.d("ServerStatus: ${response.body()}") + retrofitServerStatusListener.onSuccess(call, response.body(), response.isSuccessful) + } + + override fun onFailure( + call: Call, + t: Throwable, + ) { + Timber.d("ServerStatusError: ${t.message}") + retrofitServerStatusListener.onError(call, t) + } + }, + ) + } } diff --git a/app/src/main/java/com/dscvit/vitty/network/api/community/RetrofitCommunityListener.kt b/app/src/main/java/com/dscvit/vitty/network/api/community/RetrofitCommunityListener.kt index 41ab0b8..f178876 100644 --- a/app/src/main/java/com/dscvit/vitty/network/api/community/RetrofitCommunityListener.kt +++ b/app/src/main/java/com/dscvit/vitty/network/api/community/RetrofitCommunityListener.kt @@ -169,3 +169,15 @@ interface RetrofitActiveFriendsListener { t: Throwable, ) } + +interface RetrofitServerStatusListener { + fun onSuccess( + call: Call, + response: String?, + isSuccessful: Boolean + ) + fun onError( + call: Call, + t: Throwable + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/dscvit/vitty/ui/main/MainComposeApp.kt b/app/src/main/java/com/dscvit/vitty/ui/main/MainComposeApp.kt index 0437040..58d4ad1 100644 --- a/app/src/main/java/com/dscvit/vitty/ui/main/MainComposeApp.kt +++ b/app/src/main/java/com/dscvit/vitty/ui/main/MainComposeApp.kt @@ -103,8 +103,10 @@ import com.dscvit.vitty.ui.schedule.ScheduleViewModel import com.dscvit.vitty.util.Analytics import com.dscvit.vitty.util.Constants import com.dscvit.vitty.util.LogoutHelper +import com.dscvit.vitty.util.MaintenanceChecker import com.dscvit.vitty.util.SemesterUtils import com.dscvit.vitty.util.UtilFunctions +import com.dscvit.vitty.ui.maintenance.MaintenanceBannerDialog import com.google.gson.Gson import com.google.gson.reflect.TypeToken import java.net.URLDecoder @@ -113,6 +115,7 @@ import java.nio.charset.StandardCharsets import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import timber.log.Timber @Composable fun MainComposeApp() { @@ -129,6 +132,9 @@ fun MainComposeApp() { var campus by remember { mutableStateOf(prefs.getString(Constants.COMMUNITY_CAMPUS, "") ?: "") } var showCampusDialog by remember { mutableStateOf(campus.isEmpty()) } + + // Maintenance banner state + var showMaintenanceBanner by remember { mutableStateOf(false) } DisposableEffect(Unit) { val listener = @@ -208,13 +214,24 @@ fun MainComposeApp() { } } + LaunchedEffect(Unit) { + if (UtilFunctions.isNetworkAvailable(context)) { + MaintenanceChecker.checkMaintenanceStatusAsync(context) { isUnderMaintenance -> + Timber.d("Dialog check result: isUnderMaintenance=%s", isUnderMaintenance) + showMaintenanceBanner = isUnderMaintenance + } + } else { + Timber.d("No network available") + } + } + VittyTheme { ModalNavigationDrawer( drawerState = drawerState, drawerContent = { DrawerContent( navController = navController, - onCloseDrawer = { scope.launch { drawerState.close() } }, + onCloseDrawer = { scope.launch { drawerState.close() } } ) }, ) { @@ -866,6 +883,19 @@ fun MainComposeApp() { onDismiss = { showCampusDialog = false }, ) } + + // Maintenance Banner Dialog + MaintenanceBannerDialog( + isVisible = showMaintenanceBanner, + onDismiss = { + showMaintenanceBanner = false + }, + onRetryClick = { + MaintenanceChecker.checkMaintenanceStatusAsync(context) { isUnderMaintenance -> + showMaintenanceBanner = isUnderMaintenance + } + } + ) } } } @@ -1091,7 +1121,7 @@ fun NavigationItem( @Composable fun DrawerContent( navController: NavHostController, - onCloseDrawer: () -> Unit, + onCloseDrawer: () -> Unit ) { val context = LocalContext.current val prefs = remember { context.getSharedPreferences(Constants.USER_INFO, 0) } diff --git a/app/src/main/java/com/dscvit/vitty/ui/maintenance/MaintenanceBanner.kt b/app/src/main/java/com/dscvit/vitty/ui/maintenance/MaintenanceBanner.kt new file mode 100644 index 0000000..161adc5 --- /dev/null +++ b/app/src/main/java/com/dscvit/vitty/ui/maintenance/MaintenanceBanner.kt @@ -0,0 +1,270 @@ +package com.dscvit.vitty.ui.maintenance + +import androidx.compose.animation.* +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Warning +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import com.dscvit.vitty.R +import com.dscvit.vitty.theme.* + +@Composable +fun MaintenanceBannerDialog( + isVisible: Boolean, + onDismiss: () -> Unit, + onRetryClick: () -> Unit +) { + if (isVisible) { + Dialog( + onDismissRequest = onDismiss, + properties = DialogProperties( + dismissOnBackPress = true, + dismissOnClickOutside = true + ) + ) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + shape = RoundedCornerShape(20.dp), + colors = CardDefaults.cardColors( + containerColor = Background + ), + elevation = CardDefaults.cardElevation( + defaultElevation = 8.dp + ) + ) { + Column( + modifier = Modifier.padding(20.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + // Header with close button + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + IconButton( + onClick = onDismiss, + modifier = Modifier.size(24.dp) + ) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = "Close", + tint = Accent.copy(alpha = 0.7f), + modifier = Modifier.size(20.dp) + ) + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + // Maintenance icon with gradient background + Box( + modifier = Modifier + .size(80.dp) + .clip(RoundedCornerShape(16.dp)) + .background( + brush = Brush.radialGradient( + colors = listOf( + Accent.copy(alpha = 0.2f), + Accent.copy(alpha = 0.1f) + ) + ) + ), + contentAlignment = Alignment.Center + ) { + Icon( + painter = painterResource(id = R.drawable.ic_maintenance), + contentDescription = "Maintenance", + modifier = Modifier.size(40.dp), + tint = Accent + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + // Title + Text( + text = "Server Maintenance", + fontSize = 22.sp, + fontWeight = FontWeight.Bold, + color = TextColor, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(8.dp)) + + // Message + Text( + text = "The server is currently under maintenance. Some features may be temporarily unavailable.", + fontSize = 14.sp, + color = Accent.copy(alpha = 0.8f), + textAlign = TextAlign.Center, + lineHeight = 18.sp + ) + + Spacer(modifier = Modifier.height(20.dp)) + + // Action buttons + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + // Retry button + Button( + onClick = onRetryClick, + modifier = Modifier.weight(1f), + colors = ButtonDefaults.buttonColors( + containerColor = Accent, + contentColor = Background + ), + shape = RoundedCornerShape(12.dp) + ) { + Text( + text = "Retry", + fontSize = 14.sp, + fontWeight = FontWeight.Medium + ) + } + + // Dismiss button + OutlinedButton( + onClick = onDismiss, + modifier = Modifier.weight(1f), + colors = ButtonDefaults.outlinedButtonColors( + contentColor = Accent.copy(alpha = 0.8f) + ), + shape = RoundedCornerShape(12.dp) + ) { + Text( + text = "Continue", + fontSize = 14.sp, + fontWeight = FontWeight.Medium + ) + } + } + + Spacer(modifier = Modifier.height(8.dp)) + + // Footer + Text( + text = "Thank you for your patience", + fontSize = 12.sp, + color = Accent.copy(alpha = 0.6f), + textAlign = TextAlign.Center, + fontWeight = FontWeight.Medium + ) + } + } + } + } +} + +@Composable +fun MaintenanceBanner( + isVisible: Boolean, + onDismiss: () -> Unit, + onRetryClick: () -> Unit +) { + AnimatedVisibility( + visible = isVisible, + enter = slideInVertically( + initialOffsetY = { -it } + ) + fadeIn(), + exit = slideOutVertically( + targetOffsetY = { -it } + ) + fadeOut() + ) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + shape = RoundedCornerShape(16.dp), + colors = CardDefaults.cardColors( + containerColor = Secondary.copy(alpha = 0.95f) + ), + elevation = CardDefaults.cardElevation( + defaultElevation = 6.dp + ) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // Icon + Icon( + painter = painterResource(id = R.drawable.ic_maintenance), + contentDescription = "Maintenance", + modifier = Modifier.size(24.dp), + tint = Accent + ) + + Spacer(modifier = Modifier.width(12.dp)) + + // Text content + Column( + modifier = Modifier.weight(1f) + ) { + Text( + text = "Server Maintenance", + fontSize = 14.sp, + fontWeight = FontWeight.SemiBold, + color = Accent + ) + Text( + text = "Some features may be limited", + fontSize = 12.sp, + color = Accent.copy(alpha = 0.7f) + ) + } + + // Action buttons + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + TextButton( + onClick = onRetryClick, + contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp) + ) { + Text( + text = "Retry", + fontSize = 12.sp, + color = Accent, + fontWeight = FontWeight.Medium + ) + } + + IconButton( + onClick = onDismiss, + modifier = Modifier.size(24.dp) + ) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = "Dismiss", + tint = Accent.copy(alpha = 0.7f), + modifier = Modifier.size(16.dp) + ) + } + } + } + } + } +} diff --git a/app/src/main/java/com/dscvit/vitty/ui/maintenance/MaintenanceScreen.kt b/app/src/main/java/com/dscvit/vitty/ui/maintenance/MaintenanceScreen.kt new file mode 100644 index 0000000..429d00d --- /dev/null +++ b/app/src/main/java/com/dscvit/vitty/ui/maintenance/MaintenanceScreen.kt @@ -0,0 +1,115 @@ +package com.dscvit.vitty.ui.maintenance + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.dscvit.vitty.R +import com.dscvit.vitty.theme.* + +@Composable +fun MaintenanceScreen( + onRetryClick: () -> Unit, + onExitClick: () -> Unit +) { + Box( + modifier = Modifier + .fillMaxSize() + .padding(32.dp), + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Image( + painter = painterResource(id = R.drawable.ic_maintenance), + contentDescription = stringResource(R.string.maintenance_icon_desc), + modifier = Modifier.size(120.dp), + colorFilter = ColorFilter.tint(Accent) + ) + + Spacer(modifier = Modifier.height(32.dp)) + + Text( + text = stringResource(R.string.maintenance_title), + fontSize = 28.sp, + fontWeight = FontWeight.SemiBold, + color = TextColor, + textAlign = TextAlign.Center, + lineHeight = 36.sp + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = stringResource(R.string.maintenance_message), + fontSize = 16.sp, + color = Accent, + textAlign = TextAlign.Center, + lineHeight = 20.sp + ) + + Spacer(modifier = Modifier.height(24.dp)) + + Text( + text = stringResource(R.string.maintenance_description), + fontSize = 14.sp, + color = Accent.copy(alpha = 0.8f), + textAlign = TextAlign.Center, + lineHeight = 16.sp + ) + + Spacer(modifier = Modifier.height(48.dp)) + + Button( + onClick = onRetryClick, + modifier = Modifier + .fillMaxWidth() + .height(56.dp), + colors = ButtonDefaults.buttonColors( + containerColor = Accent, + contentColor = Background + ), + shape = RoundedCornerShape(12.dp) + ) { + Text( + text = stringResource(R.string.retry_connection), + fontSize = 16.sp, + fontWeight = FontWeight.Medium + ) + } + + Spacer(modifier = Modifier.height(16.dp)) + + TextButton( + onClick = onExitClick + ) { + Text( + text = stringResource(R.string.exit_app), + fontSize = 14.sp, + color = Accent.copy(alpha = 0.7f) + ) + } + + Spacer(modifier = Modifier.height(32.dp)) + + Text( + text = stringResource(R.string.maintenance_footer), + fontSize = 12.sp, + color = Accent.copy(alpha = 0.6f), + textAlign = TextAlign.Center + ) + } + } +} diff --git a/app/src/main/java/com/dscvit/vitty/util/MaintenanceChecker.kt b/app/src/main/java/com/dscvit/vitty/util/MaintenanceChecker.kt new file mode 100644 index 0000000..e03b6f3 --- /dev/null +++ b/app/src/main/java/com/dscvit/vitty/util/MaintenanceChecker.kt @@ -0,0 +1,57 @@ +package com.dscvit.vitty.util + +import android.content.Context +import com.dscvit.vitty.network.api.community.APICommunityRestClient +import com.dscvit.vitty.network.api.community.RetrofitServerStatusListener +import retrofit2.Call +import timber.log.Timber + +object MaintenanceChecker { + + private const val API_OPERATIONAL_MESSAGE = "Welcome to VITTY API!🎉" + private var isChecking = false + + fun checkMaintenanceStatusAsync( + context: Context, + onResult: (isUnderMaintenance: Boolean) -> Unit + ) { + if (isChecking) return + + isChecking = true + + APICommunityRestClient.instance.checkServerStatus( + object : RetrofitServerStatusListener { + override fun onSuccess(call: Call, response: String?, isSuccessful: Boolean) { + val result = when { + !isSuccessful -> { + Timber.d("Server returned error") + true // Server error = maintenance + } + response == null -> { + Timber.d("No response body received") + true // No response = maintenance + } + response.contains(API_OPERATIONAL_MESSAGE) -> { + Timber.d("API operational message found") + false // API working + } + else -> { + Timber.d("Different response received: $response") + true // Different response = maintenance + } + } + + Timber.d("Maintenance check: isUnderMaintenance=$result, response=$response") + isChecking = false + onResult(result) + } + + override fun onError(call: Call, t: Throwable) { + Timber.e(t, "Maintenance check failed") + isChecking = false + onResult(false) // Network error = don't assume maintenance + } + } + ) + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_maintenance.xml b/app/src/main/res/drawable/ic_maintenance.xml new file mode 100644 index 0000000..8554ac4 --- /dev/null +++ b/app/src/main/res/drawable/ic_maintenance.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_screwdriver.xml b/app/src/main/res/drawable/ic_screwdriver.xml new file mode 100644 index 0000000..8554ac4 --- /dev/null +++ b/app/src/main/res/drawable/ic_screwdriver.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/activity_maintenance.xml b/app/src/main/res/layout/activity_maintenance.xml new file mode 100644 index 0000000..3475e7a --- /dev/null +++ b/app/src/main/res/layout/activity_maintenance.xml @@ -0,0 +1,22 @@ + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0bf6f78..882d931 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -132,4 +132,12 @@ Hello blank fragment + + Server Under Maintenance + We\'re currently performing server maintenance to improve your experience. + Please check back in a few minutes. We\'ll be back online soon! + Try Again + Exit App + Thank you for your patience + Maintenance Icon