diff --git a/android/Omnivore/app/.gitignore b/android/Omnivore/app/.gitignore index 42afabfd2..6a4362e1c 100644 --- a/android/Omnivore/app/.gitignore +++ b/android/Omnivore/app/.gitignore @@ -1 +1,3 @@ -/build \ No newline at end of file +/build +keystore.properties +*.keystore \ No newline at end of file diff --git a/android/Omnivore/app/build.gradle b/android/Omnivore/app/build.gradle index 86c1239ca..78849006b 100644 --- a/android/Omnivore/app/build.gradle +++ b/android/Omnivore/app/build.gradle @@ -6,6 +6,10 @@ plugins { id 'com.apollographql.apollo3' version '3.5.0' } +def keystorePropertiesFile = rootProject.file("app/external/keystore.properties"); +def keystoreProperties = new Properties() +keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) + android { compileSdk 33 @@ -13,8 +17,8 @@ android { applicationId "app.omnivore.omnivore" minSdk 23 targetSdk 32 - versionCode 1 - versionName "1.0" + versionCode 2 + versionName "0.0.2" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { @@ -22,9 +26,32 @@ android { } } + signingConfigs{ + release{ + keyAlias 'key0' + storeFile file('external/omnivore-prod.keystore') + storePassword keystoreProperties['prodStorePassword'] + keyPassword keystoreProperties['prodKeyPassword'] + } + debug{ + keyAlias 'androiddebugkey' + storeFile file('external/omnivore-demo.keystore') + storePassword keystoreProperties['demoStorePassword'] + keyPassword keystoreProperties['demoKeyPassword'] + } + } + buildTypes { + debug{ + signingConfig signingConfigs.debug + buildConfigField("String", "OMNIVORE_API_URL", "\"https://api-demo.omnivore.app\"") + buildConfigField("String", "OMNIVORE_GAUTH_SERVER_CLIENT_ID", "\"267918240109-eu2ar09unac3lqqigluknhk7t0021b54.apps.googleusercontent.com\"") + } release { minifyEnabled false + signingConfig signingConfigs.release + buildConfigField("String", "OMNIVORE_API_URL", "\"https://api-prod.omnivore.app\"") + buildConfigField("String", "OMNIVORE_GAUTH_SERVER_CLIENT_ID", "\"687911924401-lq8j1e97n0sv3khhb8g8n368lk4dqkbp.apps.googleusercontent.com\"") proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } @@ -128,6 +155,7 @@ dependencies { implementation 'androidx.compose.material3:material3-window-size-class:1.0.0-alpha16' implementation 'com.google.android.gms:play-services-auth:20.2.0' + implementation "com.google.accompanist:accompanist-systemuicontroller:0.25.1" } apollo { diff --git a/android/Omnivore/app/src/main/AndroidManifest.xml b/android/Omnivore/app/src/main/AndroidManifest.xml index ed213ee7f..bf65b39f7 100644 --- a/android/Omnivore/app/src/main/AndroidManifest.xml +++ b/android/Omnivore/app/src/main/AndroidManifest.xml @@ -29,7 +29,7 @@ @@ -38,26 +38,5 @@ - diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/Constants.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/Constants.kt index 9ae6feaf6..e4852a6bc 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/Constants.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/Constants.kt @@ -1,7 +1,7 @@ package app.omnivore.omnivore object Constants { - const val demoProdURL = "https://api-demo.omnivore.app" + const val apiURL = BuildConfig.OMNIVORE_API_URL const val dataStoreName = "omnivore-datastore" } @@ -9,3 +9,11 @@ object DatastoreKeys { const val omnivoreAuthToken = "omnivoreAuthToken" const val omnivoreAuthCookieString = "omnivoreAuthCookieString" } + +object AppleConstants { + const val clientId = "app.omnivore" + const val redirectURI = "https://api-demo.omnivore.app/api/auth/vercel/apple-redirect" + const val scope = "name%20email" + const val authUrl = "https://appleid.apple.com/auth/authorize" + const val tokenUrl = "https://appleid.apple.com/auth/token" +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/MainActivity.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/MainActivity.kt index fbdd52768..eed5b92be 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/MainActivity.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/MainActivity.kt @@ -1,46 +1,17 @@ package app.omnivore.omnivore -import android.annotation.SuppressLint -import android.app.Activity -import android.content.Context import android.os.Bundle import android.view.View -import android.widget.Toast import androidx.activity.ComponentActivity -import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.setContent -import androidx.activity.result.ActivityResult -import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.PasswordVisualTransformation -import androidx.compose.ui.unit.dp import app.omnivore.omnivore.ui.theme.OmnivoreTheme -import androidx.compose.runtime.livedata.observeAsState import androidx.core.view.ViewCompat import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat -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 -import com.google.android.gms.auth.api.signin.GoogleSignInOptions -import com.google.android.gms.common.GoogleApiAvailability -import com.google.android.gms.common.GooglePlayServicesUtil.isGooglePlayServicesAvailable -import com.google.android.gms.tasks.Task +import app.omnivore.omnivore.ui.auth.LoginViewModel +import app.omnivore.omnivore.ui.root.RootView import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.launch @AndroidEntryPoint class MainActivity : ComponentActivity() { @@ -51,11 +22,7 @@ class MainActivity : ComponentActivity() { setContent { OmnivoreTheme { - // A surface container using the 'background' color from the theme - ScreenMain(viewModel = viewModel) -// Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) { -// WelcomeView(viewModel) -// } + RootView(viewModel = viewModel) } } @@ -69,170 +36,3 @@ class MainActivity : ComponentActivity() { } } } - -@Composable -fun WelcomeView(viewModel: LoginViewModel) { - val hasAuthToken: Boolean by viewModel.hasAuthTokenLiveData.observeAsState(false) - - if (hasAuthToken) { - LoggedInView(viewModel) - } else { - LoginView(viewModel) - } -} - -@Composable -fun LoginView(viewModel: LoginViewModel) { - val isGoogleAuthAvailable: Boolean = GoogleApiAvailability - .getInstance() - .isGooglePlayServicesAvailable(LocalContext.current) == 0 - - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .background(MaterialTheme.colorScheme.background) - ) { - if (isGoogleAuthAvailable) { - GoogleAuthButton(viewModel) - } - EmailLoginView(viewModel) - } -} - -@Composable -fun LoggedInView(viewModel: LoginViewModel) { - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .background(MaterialTheme.colorScheme.background) - .fillMaxSize() - ) { - Text("You have a valid auth token. Nice. Go save something in Chrome!") - - Button(onClick = { - viewModel.logout() - }) { - Text(text = "Logout") - } - } -} - -@SuppressLint("CoroutineCreationDuringComposition") -@Composable -fun EmailLoginView(viewModel: LoginViewModel) { - var email by rememberSaveable { mutableStateOf("") } - var password by rememberSaveable { mutableStateOf("") } - val focusManager = LocalFocusManager.current - val snackBarHostState = remember { SnackbarHostState() } - val coroutineScope = rememberCoroutineScope() - - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .background(MaterialTheme.colorScheme.background) - .fillMaxSize() - .clickable { focusManager.clearFocus() } - ) { - LoginFields( - email, - password, - onEmailChange = { email = it }, - onPasswordChange = { password = it }, - onLoginClick = { viewModel.login(email, password) } - ) - - // TODO: add a activity indicator (maybe after a delay?) - if (viewModel.isLoading) { - Text("Loading...") - } - - if (viewModel.errorMessage != null) { - coroutineScope.launch { - val result = snackBarHostState - .showSnackbar( - viewModel.errorMessage!!, - actionLabel = "Dismiss", - duration = SnackbarDuration.Indefinite - ) - when (result) { - SnackbarResult.ActionPerformed -> viewModel.resetErrorMessage() - } - } - - SnackbarHost(hostState = snackBarHostState) - } - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun LoginFields( - email: String, - password: String, - onEmailChange: (String) -> Unit, - onPasswordChange: (String) -> Unit, - onLoginClick: () -> Unit -) { - val context = LocalContext.current - val focusManager = LocalFocusManager.current - - Column( - modifier = Modifier - .fillMaxWidth() - .height(300.dp), - verticalArrangement = Arrangement.spacedBy(25.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text("Never miss a great read") - - OutlinedTextField( - value = email, - placeholder = { Text(text = "user@email.com") }, - label = { Text(text = "email") }, - onValueChange = onEmailChange, - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), - keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }) - ) - - OutlinedTextField( - value = password, - placeholder = { Text(text = "password") }, - label = { Text(text = "password") }, - onValueChange = onPasswordChange, - visualTransformation = PasswordVisualTransformation(), - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), - keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }) - ) - - Button(onClick = { - if (email.isNotBlank() && password.isNotBlank()) { - onLoginClick() - focusManager.clearFocus() - } else { - Toast.makeText( - context, - "Please enter an email address and password.", - Toast.LENGTH_SHORT - ).show() - } - }) { - Text(text = "Login") - } - } -} - -//@Preview( -// uiMode = Configuration.UI_MODE_NIGHT_YES, -// showBackground = true, -// name = "Dark Mode" -//) -//@Preview(showBackground = true) -//@Composable -//fun DefaultPreview() { -// OmnivoreTheme { -// LoginView() -// } -//} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/RESTNetworker.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/RESTNetworker.kt index c3956f73e..c9e238e67 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/RESTNetworker.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/RESTNetworker.kt @@ -43,7 +43,7 @@ interface AuthProviderLoginSubmit { object RetrofitHelper { fun getInstance(): Retrofit { - return Retrofit.Builder().baseUrl(Constants.demoProdURL) + return Retrofit.Builder().baseUrl(Constants.apiURL) .addConverterFactory(GsonConverterFactory.create()) .build() } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/Routes.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/Routes.kt index c6ca342c8..47f70b2b3 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/Routes.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/Routes.kt @@ -1,6 +1,7 @@ package app.omnivore.omnivore sealed class Routes(val route: String) { - object Splash : Routes("Splash") - object EmailLogin : Routes("EmailLogin") + object Root : Routes("Root") + object Home : Routes("Home") + object Welcome : Routes("Welcome") } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ScreenMain.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ScreenMain.kt deleted file mode 100644 index a2248f02e..000000000 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ScreenMain.kt +++ /dev/null @@ -1,22 +0,0 @@ -package app.omnivore.omnivore - -import androidx.compose.runtime.Composable -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.rememberNavController -import app.omnivore.omnivore.screen.EmailLoginPage -import app.omnivore.omnivore.screen.SplashPage - -@Composable -fun ScreenMain(viewModel: LoginViewModel){ - val navController = rememberNavController() - - NavHost(navController = navController, startDestination = Routes.Splash.route) { - composable(Routes.Splash.route) { - SplashPage(viewModel = viewModel, navController = navController) - } - composable(Routes.EmailLogin.route) { - EmailLoginPage(viewModel = viewModel, navController = navController) - } - } -} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/screen/EmailLogin.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/screen/EmailLogin.kt deleted file mode 100644 index 8d2784eb8..000000000 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/screen/EmailLogin.kt +++ /dev/null @@ -1,31 +0,0 @@ -package app.omnivore.omnivore.screen - -import android.content.Intent -import android.net.Uri -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.navigation.NavHostController -import app.omnivore.omnivore.EmailLoginView -import app.omnivore.omnivore.LoginViewModel - - -@Composable -fun EmailLoginPage(viewModel: LoginViewModel, navController: NavHostController) { - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .background(MaterialTheme.colorScheme.background) - .fillMaxSize() - .navigationBarsPadding() - ) { - EmailLoginView(viewModel) - } -} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/screen/SplashPage.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/screen/SplashPage.kt deleted file mode 100644 index 8e79e6930..000000000 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/screen/SplashPage.kt +++ /dev/null @@ -1,78 +0,0 @@ -package app.omnivore.omnivore.screen - -import android.content.Intent -import android.net.Uri -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.navigationBarsPadding -import androidx.compose.foundation.text.ClickableText -import androidx.compose.material3.Button -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalUriHandler -import androidx.compose.ui.text.AnnotatedString -import androidx.core.content.ContextCompat.startActivity -import androidx.lifecycle.viewModelScope -import androidx.navigation.NavHostController -import app.omnivore.omnivore.EmailLoginView -import app.omnivore.omnivore.GoogleAuthButton -import app.omnivore.omnivore.LoginViewModel -import app.omnivore.omnivore.Routes -import com.google.android.gms.common.GoogleApiAvailability -import kotlinx.coroutines.launch - -@Composable -fun SplashPage(viewModel: LoginViewModel, navController: NavHostController) { - val isGoogleAuthAvailable: Boolean = GoogleApiAvailability - .getInstance() - .isGooglePlayServicesAvailable(LocalContext.current) == 0 - - Column( - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .background(MaterialTheme.colorScheme.background) - .fillMaxSize() - .navigationBarsPadding() - ) { - Text("Never miss a great read") - MoreInfoButton() - - if (isGoogleAuthAvailable) { - GoogleAuthButton(viewModel) - } - - ContinueWithEmailButton(navController) - } -} - -@Composable -fun MoreInfoButton() { - val context = LocalContext.current - val intent = remember { Intent(Intent.ACTION_VIEW, Uri.parse("https://omnivore.app/about")) } - - ClickableText( - text = AnnotatedString("Learn More ->"), - onClick = { - context.startActivity(intent) - } - ) -} - -@Composable -fun ContinueWithEmailButton(navController: NavHostController) { - ClickableText( - text = AnnotatedString("Continue with Email ->"), - onClick = { - navController.navigate(Routes.EmailLogin.route) - } - ) -} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/AppleAuth.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/AppleAuth.kt new file mode 100644 index 000000000..5bfd56466 --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/AppleAuth.kt @@ -0,0 +1,197 @@ +package app.omnivore.omnivore.ui.auth + +import android.annotation.SuppressLint +import android.util.Log +import android.view.ViewGroup +import android.webkit.WebResourceRequest +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.TopAppBar +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.compose.ui.window.Dialog +import app.omnivore.omnivore.AppleConstants +import app.omnivore.omnivore.R +import java.util.* + +@Composable +fun AppleAuthButton(viewModel: LoginViewModel) { + val showDialog = remember { mutableStateOf(false) } + + LoadingButtonWithIcon( + text = "Continue with Apple", + loadingText = "Signing in...", + isLoading = viewModel.isLoading, + icon = painterResource(id = R.drawable.ic_logo_apple), + modifier = Modifier.padding(vertical = 6.dp), + onClick = { showDialog.value = true } + ) + + if (showDialog.value) { + AppleAuthDialog(onDismiss = { + showDialog.value = false + Log.i("Apple payload: ", it ?: "null") + }) + } +} + +@Composable +fun AppleAuthDialog(onDismiss: (String?) -> Unit) { + Dialog(onDismissRequest = { onDismiss(null) }) { + Surface( + shape = RoundedCornerShape(16.dp), + color = Color.White + ) { + AppleAuthWebContainerView(onDismiss) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") +@Composable +fun AppleAuthWebContainerView(onDismiss: (String?) -> Unit) { + Scaffold( + topBar = { TopAppBar(title = { Text("WebView", color = Color.White) }, backgroundColor = Color(0xff0f9d58)) }, + content = { AppleAuthWebView(onDismiss) } + ) +} + +@SuppressLint("SetJavaScriptEnabled") +@Composable +fun AppleAuthWebView(onDismiss: (String?) -> Unit) { + val url = AppleConstants.authUrl + + "?client_id=" + + AppleConstants.clientId + + "&redirect_uri=" + + AppleConstants.redirectURI + + "&response_type=code%20id_token&scope=" + + AppleConstants.scope + + "&response_mode=form_post&state=android:login" + +// clientId="app.omnivore" +// scope="name email" +// state="web:login" +// redirectURI={appleAuthRedirectURI} +// responseMode="form_post" +// responseType="code id_token" +// designProp={{ +// color: 'black', + + // Adding a WebView inside AndroidView + // with layout as full screen + AndroidView(factory = { + WebView(it).apply { + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) +// webViewClient = WebViewClient() + webViewClient = object : WebViewClient() { + override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean { + Log.i("Apple payload one: ", request?.url.toString() ?: "null") + if (request?.url.toString().startsWith(AppleConstants.redirectURI)) { +// handleUrl(request?.url.toString()) + onDismiss(request?.url.toString()) + // Close the dialog after getting the authorization code + if (request?.url.toString().contains("success=")) { + onDismiss(null) + } + return true + } + return true + } + } + settings.javaScriptEnabled = true + loadUrl(url) + } + }, update = { + it.loadUrl(url) + }) +} + +//// A client to know about WebView navigation +//// For API 21 and above +//class AppleWebViewClient : WebViewClient() { +// @TargetApi(Build.VERSION_CODES.LOLLIPOP) +// override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean { +// if (request?.url.toString().startsWith(AppleConstants.redirectURI)) { +// handleUrl(request?.url.toString()) +// // Close the dialog after getting the authorization code +// if (request.url.toString().contains("success=")) { +//// appledialog.dismiss() +// } +// return true +// } +// return true +// } + +// // For API 19 and below +// override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean { +// if (url.startsWith(AppleConstants.redirectURI)) { +// handleUrl(url) +// // Close the dialog after getting the authorization code +// if (url.contains("success=")) { +//// appledialog.dismiss() +// } +// return true +// } +// return false +// } + +// @SuppressLint("ClickableViewAccessibility") +// override fun onPageFinished(view: WebView?, url: String?) { +// super.onPageFinished(view, url) +// // retrieve display dimensions +// val displayRectangle = Rect() +// val window = this@AppleWebViewClient.w +// window.decorView.getWindowVisibleDisplayFrame(displayRectangle) +// // Set height of the Dialog to 90% of the screen +// val layoutParams = view?.layoutParams +// layoutParams?.height = (displayRectangle.height() * 0.9f).toInt() +// view?.layoutParams = layoutParams +// } + +// // Check WebView url for access token code or error +// @SuppressLint("LongLogTag") +// private fun handleUrl(url: String) { +// val uri = Uri.parse(url) +// val success = uri.getQueryParameter("success") +// if (success == "true") { +// // Get the Authorization Code from the URL +//// appleAuthCode = uri.getQueryParameter("code") ?: "" +//// Log.i("Apple Code: ", appleAuthCode) +// // Get the Client Secret from the URL +//// appleClientSecret = uri.getQueryParameter("client_secret") ?: "" +//// Log.i("Apple Client Secret: ", appleClientSecret) +// //Check if user gave access to the app for the first time by checking if the url contains their email +// if (url.contains("email")) { +// //Get user's First Name +// val firstName = uri.getQueryParameter("first_name") +// Log.i("Apple User First Name: ", firstName ?: "") +// //Get user's Middle Name +// val middleName = uri.getQueryParameter("middle_name") +// Log.i("Apple User Middle Name: ", middleName ?: "") +// //Get user's Last Name +// val lastName = uri.getQueryParameter("last_name") +// Log.i("Apple User Last Name: ", lastName ?: "") +// //Get user's email +// val email = uri.getQueryParameter("email") +// Log.i("Apple User Email: ", email ?: "Not exists") +// } +// // Exchange the Auth Code for Access Token +//// requestForAccessToken(appleAuthCode, appleClientSecret) +// } else if (success == "false") { +// Log.e("ERROR", "We couldn't get the Auth Code") +// } +// } +//} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/EmailLogin.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/EmailLogin.kt new file mode 100644 index 000000000..99b41f432 --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/EmailLogin.kt @@ -0,0 +1,123 @@ +package app.omnivore.omnivore.ui.auth + +import android.annotation.SuppressLint +import android.widget.Toast +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.text.ClickableText +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.unit.dp + +@SuppressLint("CoroutineCreationDuringComposition") +@Composable +fun EmailLoginView(viewModel: LoginViewModel, onAuthProviderButtonTap: () -> Unit) { + var email by rememberSaveable { mutableStateOf("") } + var password by rememberSaveable { mutableStateOf("") } + + Row( + horizontalArrangement = Arrangement.Center + ) { + Spacer(modifier = Modifier.weight(1.0F)) + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + LoginFields( + email, + password, + onEmailChange = { email = it }, + onPasswordChange = { password = it }, + onLoginClick = { viewModel.login(email, password) } + ) + + // TODO: add a activity indicator (maybe after a delay?) + if (viewModel.isLoading) { + Text("Loading...") + } + + ClickableText( + text = AnnotatedString("Return to Social Login"), + style = MaterialTheme.typography.titleMedium + .plus(TextStyle(textDecoration = TextDecoration.Underline)), + onClick = { onAuthProviderButtonTap() } + ) + } + Spacer(modifier = Modifier.weight(1.0F)) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LoginFields( + email: String, + password: String, + onEmailChange: (String) -> Unit, + onPasswordChange: (String) -> Unit, + onLoginClick: () -> Unit +) { + val context = LocalContext.current + val focusManager = LocalFocusManager.current + + Column( + modifier = Modifier + .fillMaxWidth() + .height(300.dp), + verticalArrangement = Arrangement.spacedBy(25.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + OutlinedTextField( + value = email, + placeholder = { Text(text = "user@email.com") }, + label = { Text(text = "Email") }, + onValueChange = onEmailChange, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }) + ) + + OutlinedTextField( + value = password, + placeholder = { Text(text = "Password") }, + label = { Text(text = "Password") }, + onValueChange = onPasswordChange, + visualTransformation = PasswordVisualTransformation(), + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() }) + ) + + Button(onClick = { + if (email.isNotBlank() && password.isNotBlank()) { + onLoginClick() + focusManager.clearFocus() + } else { + Toast.makeText( + context, + "Please enter an email address and password.", + Toast.LENGTH_SHORT + ).show() + } + }, colors = ButtonDefaults.buttonColors( + contentColor = Color(0xFF3D3D3D), + containerColor = Color(0xffffd234) + ) + ) { + Text( + text = "Login", + modifier = Modifier.padding(horizontal = 100.dp) + ) + } + } +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/GoogleAuth.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/GoogleAuth.kt similarity index 70% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/GoogleAuth.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/GoogleAuth.kt index ba8b9eae0..436bc31c6 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/GoogleAuth.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/GoogleAuth.kt @@ -1,32 +1,27 @@ -package app.omnivore.omnivore +package app.omnivore.omnivore.ui.auth import android.app.Activity import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.ActivityResult import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material3.Button -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource +import app.omnivore.omnivore.BuildConfig +import app.omnivore.omnivore.R 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.GoogleSignInOptions import com.google.android.gms.tasks.Task - -@OptIn(ExperimentalMaterialApi::class) @Composable fun GoogleAuthButton(viewModel: LoginViewModel) { val context = LocalContext.current + val signInOptions = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) + .requestIdToken(BuildConfig.OMNIVORE_GAUTH_SERVER_CLIENT_ID) .requestEmail() - .requestIdToken(stringResource(R.string.gcp_id)) - .requestId() - .requestProfile() .build() val startForResult = @@ -37,17 +32,30 @@ fun GoogleAuthButton(viewModel: LoginViewModel) { val task: Task = GoogleSignIn.getSignedInAccountFromIntent(intent) viewModel.handleGoogleAuthTask(task) } + } else { + viewModel.showGoogleErrorMessage() } } - GoogleSignInButton( + LoadingButtonWithIcon( text = "Continue with Google", loadingText = "Signing in...", isLoading = viewModel.isLoading, icon = painterResource(id = R.drawable.ic_logo_google), onClick = { val googleSignIn = GoogleSignIn.getClient(context, signInOptions) - startForResult.launch(googleSignIn.signInIntent) + + googleSignIn.silentSignIn() + .addOnCompleteListener { task -> + if (task.isSuccessful) { + viewModel.handleGoogleAuthTask(task) + } else { + startForResult.launch(googleSignIn.signInIntent) + } + } + .addOnFailureListener { + startForResult.launch(googleSignIn.signInIntent) + } } ) } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/GoogleSignInButton.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/LoadingButtonWithIcon.kt similarity index 83% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/GoogleSignInButton.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/LoadingButtonWithIcon.kt index e80f8c0ce..03e273231 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/GoogleSignInButton.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/LoadingButtonWithIcon.kt @@ -1,10 +1,9 @@ -package app.omnivore.omnivore +package app.omnivore.omnivore.ui.auth import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* -import androidx.compose.material.* -import androidx.compose.material.Surface +import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -14,21 +13,21 @@ import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.unit.dp -@ExperimentalMaterialApi @Composable -fun GoogleSignInButton( +fun LoadingButtonWithIcon( text: String, - loadingText: String = "Signing in...", + loadingText: String, icon: Painter, isLoading: Boolean = false, shape: Shape = Shapes().medium, borderColor: Color = Color.LightGray, - backgroundColor: Color = MaterialTheme.colors.surface, - progressIndicatorColor: Color = MaterialTheme.colors.primary, + backgroundColor: Color = MaterialTheme.colorScheme.surface, + progressIndicatorColor: Color = MaterialTheme.colorScheme.primary, + modifier: Modifier = Modifier, onClick: () -> Unit ) { Surface( - modifier = Modifier.clickable( + modifier = modifier.clickable( enabled = !isLoading, onClick = onClick ), diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/LoginViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/LoginViewModel.kt similarity index 80% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/LoginViewModel.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/LoginViewModel.kt index 14db1873b..352281598 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/LoginViewModel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/LoginViewModel.kt @@ -1,13 +1,12 @@ -package app.omnivore.omnivore +package app.omnivore.omnivore.ui.auth import android.content.ContentValues import android.util.Log -import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.compose.ui.platform.LocalUriHandler import androidx.lifecycle.* +import app.omnivore.omnivore.* import com.google.android.gms.auth.api.signin.GoogleSignInAccount import com.google.android.gms.common.api.ApiException import com.google.android.gms.tasks.Task @@ -16,6 +15,12 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch import javax.inject.Inject + +enum class RegistrationState { + AuthProviderButtons, + EmailSignIn +} + @HiltViewModel class LoginViewModel @Inject constructor( private val datastoreRepo: DatastoreRepository @@ -73,13 +78,21 @@ class LoginViewModel @Inject constructor( errorMessage = null } - fun handleGoogleAuthTask(task: Task) { - val googleIdToken = task?.getResult(ApiException::class.java).idToken - Log.d(ContentValues.TAG, "Google Result?: $googleIdToken") - // TODO: submit id token to backend - // If token is missing then set the error message + fun showGoogleErrorMessage() { + errorMessage = "Failed to authenticate with Google." + } + fun handleGoogleAuthTask(task: Task) { + val result = task?.getResult(ApiException::class.java) + Log.d(ContentValues.TAG, "server auth code?: ${result.serverAuthCode}") + Log.d(ContentValues.TAG, "is Expired?: ${result.isExpired}") + Log.d(ContentValues.TAG, "granted Scopes?: ${result.grantedScopes}") + val googleIdToken = result.idToken + Log.d(ContentValues.TAG, "Google id token?: $googleIdToken") + + // If token is missing then set the error message if (googleIdToken == null) { + errorMessage = "No authentication token found." return } @@ -98,7 +111,7 @@ class LoginViewModel @Inject constructor( if (result.body()?.authToken != null) { datastoreRepo.putString(DatastoreKeys.omnivoreAuthToken, result.body()?.authToken!!) } else { - errorMessage = "Something went wrong. Please check your email/password and try again" + errorMessage = "Something went wrong. Please check your credentials and try again" } if (result.body()?.authCookieString != null) { diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/WelcomeScreen.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/WelcomeScreen.kt new file mode 100644 index 000000000..ccd6b4162 --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/auth/WelcomeScreen.kt @@ -0,0 +1,170 @@ +package app.omnivore.omnivore.ui.auth + +import android.annotation.SuppressLint +import android.content.Intent +import android.net.Uri +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.text.ClickableText +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.unit.dp +import app.omnivore.omnivore.R +import com.google.android.gms.common.GoogleApiAvailability +import kotlinx.coroutines.launch + +@Composable +fun WelcomeScreen(viewModel: LoginViewModel) { + Surface(modifier = Modifier.fillMaxSize(), color = Color(0xFFFCEBA8)) { + WelcomeScreenContent(viewModel = viewModel) + } +} + +@SuppressLint("CoroutineCreationDuringComposition") +@Composable +fun WelcomeScreenContent(viewModel: LoginViewModel) { + var registrationState by rememberSaveable { mutableStateOf(RegistrationState.AuthProviderButtons) } + + val onRegistrationStateChange = { state: RegistrationState -> + registrationState = state + } + + val snackBarHostState = remember { SnackbarHostState() } + val coroutineScope = rememberCoroutineScope() + val focusManager = LocalFocusManager.current + + Column( + verticalArrangement = Arrangement.SpaceAround, + horizontalAlignment = Alignment.Start, + modifier = Modifier + .fillMaxSize() + .navigationBarsPadding() + .padding(horizontal = 16.dp) + .clickable { focusManager.clearFocus() } + ) { + Spacer(modifier = Modifier.height(50.dp)) + Image( + painter = painterResource(id = R.drawable.ic_omnivore_name_logo), + contentDescription = "Omnivore Icon with Name" + ) + Spacer(modifier = Modifier.height(50.dp)) + + when(registrationState) { + RegistrationState.EmailSignIn -> { + EmailLoginView( + viewModel = viewModel, + onAuthProviderButtonTap = { + onRegistrationStateChange(RegistrationState.AuthProviderButtons) + } + ) + } + RegistrationState.AuthProviderButtons -> { + Text( + text = stringResource(id = R.string.welcome_title), + style = MaterialTheme.typography.headlineLarge + ) + + Text( + text = stringResource(id = R.string.welcome_subtitle), + style = MaterialTheme.typography.titleSmall + ) + + MoreInfoButton() + + Spacer(modifier = Modifier.height(50.dp)) + + AuthProviderView( + viewModel = viewModel, + onEmailButtonTap = { onRegistrationStateChange(RegistrationState.EmailSignIn) } + ) + } + } + + Spacer(modifier = Modifier.weight(1.0F)) + } + + if (viewModel.errorMessage != null) { + coroutineScope.launch { + val result = snackBarHostState + .showSnackbar( + viewModel.errorMessage!!, + actionLabel = "Dismiss", + duration = SnackbarDuration.Indefinite + ) + when (result) { + SnackbarResult.ActionPerformed -> viewModel.resetErrorMessage() + } + } + + SnackbarHost(hostState = snackBarHostState) + } +} + +@Composable +fun AuthProviderView( + viewModel: LoginViewModel, + onEmailButtonTap: () -> Unit +) { + val isGoogleAuthAvailable: Boolean = GoogleApiAvailability + .getInstance() + .isGooglePlayServicesAvailable(LocalContext.current) == 0 + + Row( + horizontalArrangement = Arrangement.Center + ) { + Spacer(modifier = Modifier.weight(1.0F)) + Column( +// verticalArrangement = Arrangement.Center, +// horizontalAlignment = Alignment.CenterHorizontally + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + if (isGoogleAuthAvailable) { + GoogleAuthButton(viewModel) + } + + // AppleAuthButton(viewModel) + + ClickableText( + text = AnnotatedString("Continue with Email"), + style = MaterialTheme.typography.titleMedium + .plus(TextStyle(textDecoration = TextDecoration.Underline)), + onClick = { onEmailButtonTap() } + ) + } + Spacer(modifier = Modifier.weight(1.0F)) + } +} + +@Composable +fun MoreInfoButton() { + val context = LocalContext.current + val intent = remember { Intent(Intent.ACTION_VIEW, Uri.parse("https://omnivore.app/about")) } + + ClickableText( + text = AnnotatedString( + stringResource(id = R.string.learn_more), + ), + style = MaterialTheme.typography.titleSmall + .plus(TextStyle(textDecoration = TextDecoration.Underline)), + onClick = { + context.startActivity(intent) + }, + modifier = Modifier.padding(vertical = 6.dp) + ) +} + diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/home/HomeView.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/home/HomeView.kt new file mode 100644 index 000000000..da1c0e4d9 --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/home/HomeView.kt @@ -0,0 +1,48 @@ +package app.omnivore.omnivore.ui.home + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import androidx.navigation.NavHostController +import app.omnivore.omnivore.ui.auth.LoginViewModel +import com.google.android.gms.auth.api.signin.GoogleSignIn +import com.google.android.gms.auth.api.signin.GoogleSignInOptions + +@Composable +fun HomeView(viewModel: LoginViewModel) { + val context = LocalContext.current + + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .background(MaterialTheme.colorScheme.background) + .fillMaxSize() + .padding(horizontal = 6.dp) + ) { + Text("You have a valid auth token. Nice. Go save something in Chrome!") + + Button(onClick = { + // Sign out google users + val signInOptions = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) + .build() + + val googleSignIn = GoogleSignIn.getClient(context, signInOptions) + googleSignIn.signOut() + + viewModel.logout() + }) { + Text(text = "Logout") + } + } +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/root/RootView.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/root/RootView.kt new file mode 100644 index 000000000..9bdd36a04 --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/root/RootView.kt @@ -0,0 +1,19 @@ +package app.omnivore.omnivore.ui.root + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import app.omnivore.omnivore.ui.auth.LoginViewModel +import app.omnivore.omnivore.ui.auth.WelcomeScreen +import app.omnivore.omnivore.ui.home.HomeView + +@Composable +fun RootView(viewModel: LoginViewModel) { + val hasAuthToken: Boolean by viewModel.hasAuthTokenLiveData.observeAsState(false) + + if (hasAuthToken) { + HomeView(viewModel = viewModel) + } else { + WelcomeScreen(viewModel = viewModel) + } +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/NewFlowActivity.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/save/NewFlowActivity.kt similarity index 72% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/NewFlowActivity.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/save/NewFlowActivity.kt index 0748fda0f..571a622f8 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/NewFlowActivity.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/save/NewFlowActivity.kt @@ -1,6 +1,7 @@ -package app.omnivore.omnivore +package app.omnivore.omnivore.ui.save import androidx.compose.material.ExperimentalMaterialApi +import app.omnivore.omnivore.ui.save.SaveSheetActivity // Not sure why we need this class, but directly opening SaveSheetActivity // causes the app to crash. diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/SaveContent.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/save/SaveContent.kt similarity index 79% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/SaveContent.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/save/SaveContent.kt index 4a569178b..710768e67 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/SaveContent.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/save/SaveContent.kt @@ -8,9 +8,11 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.material.* -import androidx.compose.material.MaterialTheme.colors +import androidx.compose.material.ButtonDefaults import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp +import app.omnivore.omnivore.ui.save.SaveViewModel import kotlinx.coroutines.launch @Composable @@ -32,7 +34,12 @@ fun SaveContent(viewModel: SaveViewModel, modalBottomSheetState: ModalBottomShee coroutineScope.launch { modalBottomSheetState.hide() } - }) { + }, + colors = ButtonDefaults.buttonColors( + contentColor = Color(0xFF3D3D3D), + backgroundColor = Color(0xffffd234) + ) + ) { Text(text = "Dismiss") } } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/SaveSheetActivity.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/save/SaveSheetActivity.kt similarity index 98% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/SaveSheetActivity.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/save/SaveSheetActivity.kt index 521e5c3df..4ffc0fcae 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/SaveSheetActivity.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/save/SaveSheetActivity.kt @@ -1,4 +1,4 @@ -package app.omnivore.omnivore +package app.omnivore.omnivore.ui.save import android.content.ContentValues import android.content.Intent @@ -18,6 +18,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp +import app.omnivore.omnivore.SaveContent import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/SaveViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/save/SaveViewModel.kt similarity index 89% rename from android/Omnivore/app/src/main/java/app/omnivore/omnivore/SaveViewModel.kt rename to android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/save/SaveViewModel.kt index 1cbf43078..4e1acc23c 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/SaveViewModel.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/save/SaveViewModel.kt @@ -1,4 +1,4 @@ -package app.omnivore.omnivore +package app.omnivore.omnivore.ui.save import android.content.ContentValues import android.util.Log @@ -7,6 +7,9 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import app.omnivore.omnivore.Constants +import app.omnivore.omnivore.DatastoreKeys +import app.omnivore.omnivore.DatastoreRepository import app.omnivore.omnivore.graphql.generated.SaveUrlMutation import app.omnivore.omnivore.graphql.generated.type.SaveUrlInput import com.apollographql.apollo3.ApolloClient @@ -44,7 +47,7 @@ class SaveViewModel @Inject constructor( } val apolloClient = ApolloClient.Builder() - .serverUrl("${Constants.demoProdURL}/api/graphql") + .serverUrl("${Constants.apiURL}/api/graphql") .addHttpHeader("Authorization", value = apiKey) .build() diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/theme/Theme.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/theme/Theme.kt index f55b9dea7..38c6bc295 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/theme/Theme.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/theme/Theme.kt @@ -82,7 +82,7 @@ fun OmnivoreTheme( MaterialTheme( colorScheme = colorScheme, -// typography = Typography, + typography = Typography, // shapes = Shapes, content = content ) diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/theme/Type.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/theme/Type.kt index 5a70fbaec..86df5fe88 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/theme/Type.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/theme/Type.kt @@ -1,28 +1,25 @@ package app.omnivore.omnivore.ui.theme -import androidx.compose.material.Typography -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.sp +import androidx.compose.material3.Typography + +val Typography = Typography() + +// ex: https://github.com/android/compose-samples/blob/main/Jetchat/app/src/main/java/com/example/compose/jetchat/theme/Typography.kt +//val Typography = Typography( +//displayLarge: TextStyle = TypographyTokens.DisplayLarge, +//displayMedium: TextStyle = TypographyTokens.DisplayMedium, +//displaySmall: TextStyle = TypographyTokens.DisplaySmall, +//headlineLarge: TextStyle = TypographyTokens.HeadlineLarge, +//headlineMedium: TextStyle = TypographyTokens.HeadlineMedium, +//headlineSmall: TextStyle = TypographyTokens.HeadlineSmall, +//titleLarge: TextStyle = TypographyTokens.TitleLarge, +//titleMedium: TextStyle = TypographyTokens.TitleMedium, +//titleSmall: TextStyle = TypographyTokens.TitleSmall, +//bodyLarge: TextStyle = TypographyTokens.BodyLarge, +//bodyMedium: TextStyle = TypographyTokens.BodyMedium, +//bodySmall: TextStyle = TypographyTokens.BodySmall, +//labelLarge: TextStyle = TypographyTokens.LabelLarge, +//labelMedium: TextStyle = TypographyTokens.LabelMedium, +//labelSmall: TextStyle = TypographyTokens.LabelSmall +//) -// Set of Material typography styles to start with -val Typography = Typography( - body1 = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 16.sp - ) - /* Other default text styles to override - button = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.W500, - fontSize = 14.sp - ), - caption = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 12.sp - ) - */ -) diff --git a/android/Omnivore/app/src/main/res/drawable/ic_logo_apple.xml b/android/Omnivore/app/src/main/res/drawable/ic_logo_apple.xml new file mode 100644 index 000000000..84a25baf6 --- /dev/null +++ b/android/Omnivore/app/src/main/res/drawable/ic_logo_apple.xml @@ -0,0 +1,9 @@ + + + diff --git a/android/Omnivore/app/src/main/res/drawable/ic_omnivore_name_logo.xml b/android/Omnivore/app/src/main/res/drawable/ic_omnivore_name_logo.xml new file mode 100644 index 000000000..3129b743c --- /dev/null +++ b/android/Omnivore/app/src/main/res/drawable/ic_omnivore_name_logo.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + diff --git a/android/Omnivore/app/src/main/res/values/strings.xml b/android/Omnivore/app/src/main/res/values/strings.xml index 2f4570ea1..366c15319 100644 --- a/android/Omnivore/app/src/main/res/values/strings.xml +++ b/android/Omnivore/app/src/main/res/values/strings.xml @@ -1,4 +1,8 @@ Omnivore 590528454690-mpmbvcb4c3ifmnojmrultt37eo8f64at.apps.googleusercontent.com + + Never miss a great read + Learn More + Save articles and read them later in our distraction-free reader. diff --git a/android/Omnivore/app/src/main/res/values/themes.xml b/android/Omnivore/app/src/main/res/values/themes.xml index f5b5ca333..fa82596de 100644 --- a/android/Omnivore/app/src/main/res/values/themes.xml +++ b/android/Omnivore/app/src/main/res/values/themes.xml @@ -1,8 +1,7 @@ -