diff --git a/android/Omnivore/app/build.gradle b/android/Omnivore/app/build.gradle index f403cb72d..86c1239ca 100644 --- a/android/Omnivore/app/build.gradle +++ b/android/Omnivore/app/build.gradle @@ -1,14 +1,17 @@ plugins { id 'com.android.application' id 'org.jetbrains.kotlin.android' + id 'kotlin-kapt' + id 'dagger.hilt.android.plugin' + id 'com.apollographql.apollo3' version '3.5.0' } android { - compileSdk 32 + compileSdk 33 defaultConfig { applicationId "app.omnivore.omnivore" - minSdk 21 + minSdk 23 targetSdk 32 versionCode 1 versionName "1.0" @@ -36,7 +39,7 @@ android { compose true } composeOptions { - kotlinCompilerExtensionVersion compose_version + kotlinCompilerExtensionVersion = '1.2.0-alpha08' } packagingOptions { resources { @@ -46,6 +49,7 @@ android { } dependencies { + def nav_version = "2.5.1" implementation 'androidx.core:core-ktx:1.7.0' implementation "androidx.compose.ui:ui:$compose_version" @@ -53,10 +57,79 @@ dependencies { implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1' implementation 'androidx.activity:activity-compose:1.3.1' + implementation 'androidx.appcompat:appcompat:1.4.1' + implementation 'com.google.android.gms:play-services-base:17.6.0' + + implementation "androidx.navigation:navigation-compose:$nav_version" + testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version" + + // Jetpack Lifecycle deps + + // ViewModel + implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version") + // ViewModel utilities for Compose + implementation("androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version") + // LiveData + implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version") + implementation("androidx.compose.runtime:runtime-livedata:1.2.1") + + // Saved state module for ViewModel + implementation("androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version") + + // Annotation processor +// kapt("androidx.lifecycle:lifecycle-compiler:$lifecycle_version") + // alternately - if using Java8, use the following instead of lifecycle-compiler + implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycle_version") + + // optional - helpers for implementing LifecycleOwner in a Service +// implementation("androidx.lifecycle:lifecycle-service:$lifecycle_version") + + // optional - ProcessLifecycleOwner provides a lifecycle for the whole application process +// implementation("androidx.lifecycle:lifecycle-process:$lifecycle_version") + + // optional - ReactiveStreams support for LiveData +// implementation("androidx.lifecycle:lifecycle-reactivestreams-ktx:$lifecycle_version") + + // optional - Test helpers for LiveData +// testImplementation("androidx.arch.core:core-testing:$arch_version") + + // optional - Test helpers for Lifecycle runtime +// testImplementation ("androidx.lifecycle:lifecycle-runtime-testing:$lifecycle_version") + + implementation 'com.squareup.retrofit2:converter-gson:2.9.0' + implementation 'com.squareup.retrofit2:retrofit:2.9.0' + +// coroutines + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4' + + implementation "androidx.security:security-crypto:1.0.0" + implementation "androidx.datastore:datastore-preferences:1.0.0" + + + +// Activity KTX for viewModels() + // TODO: remove this since compose-activity imports ktx? +// implementation "androidx.activity:activity-ktx:1.3.1" + +//Dagger - Hilt + implementation 'com.google.dagger:hilt-android:2.42' + kapt 'com.google.dagger:hilt-compiler:2.42' + + implementation 'com.apollographql.apollo3:apollo-runtime:3.5.0' + + implementation 'androidx.compose.material3:material3:1.0.0-alpha16' + implementation 'androidx.compose.material3:material3-window-size-class:1.0.0-alpha16' + + implementation 'com.google.android.gms:play-services-auth:20.2.0' +} + +apollo { + packageName.set 'app.omnivore.omnivore.graphql.generated' } diff --git a/android/Omnivore/app/src/main/AndroidManifest.xml b/android/Omnivore/app/src/main/AndroidManifest.xml index f8e001890..ed213ee7f 100644 --- a/android/Omnivore/app/src/main/AndroidManifest.xml +++ b/android/Omnivore/app/src/main/AndroidManifest.xml @@ -3,7 +3,10 @@ xmlns:tools="http://schemas.android.com/tools" package="app.omnivore.omnivore"> + + + - - + + + + + + + + + diff --git a/android/SaveToOmnivore/app/src/main/graphql/SaveUrl.graphql b/android/Omnivore/app/src/main/graphql/SaveUrl.graphql similarity index 100% rename from android/SaveToOmnivore/app/src/main/graphql/SaveUrl.graphql rename to android/Omnivore/app/src/main/graphql/SaveUrl.graphql diff --git a/android/SaveToOmnivore/app/src/main/graphql/schema.graphqls b/android/Omnivore/app/src/main/graphql/schema.graphqls similarity index 93% rename from android/SaveToOmnivore/app/src/main/graphql/schema.graphqls rename to android/Omnivore/app/src/main/graphql/schema.graphqls index 187b8d072..a0d0e1161 100644 --- a/android/SaveToOmnivore/app/src/main/graphql/schema.graphqls +++ b/android/Omnivore/app/src/main/graphql/schema.graphqls @@ -148,6 +148,7 @@ enum ArticleSavingRequestErrorCode { union ArticleSavingRequestResult = ArticleSavingRequestError | ArticleSavingRequestSuccess enum ArticleSavingRequestStatus { + DELETED FAILED PROCESSING SUCCEEDED @@ -419,6 +420,22 @@ type DeleteHighlightSuccess { highlight: Highlight! } +type DeleteIntegrationError { + errorCodes: [DeleteIntegrationErrorCode!]! +} + +enum DeleteIntegrationErrorCode { + BAD_REQUEST + NOT_FOUND + UNAUTHORIZED +} + +union DeleteIntegrationResult = DeleteIntegrationError | DeleteIntegrationSuccess + +type DeleteIntegrationSuccess { + integration: Integration! +} + type DeleteLabelError { errorCodes: [DeleteLabelErrorCode!]! } @@ -657,12 +674,41 @@ type HighlightStats { highlightCount: Int! } +type Integration { + createdAt: Date! + enabled: Boolean! + id: ID! + token: String! + type: IntegrationType! + updatedAt: Date! +} + +enum IntegrationType { + READWISE +} + +type IntegrationsError { + errorCodes: [IntegrationsErrorCode!]! +} + +enum IntegrationsErrorCode { + BAD_REQUEST + UNAUTHORIZED +} + +union IntegrationsResult = IntegrationsError | IntegrationsSuccess + +type IntegrationsSuccess { + integrations: [Integration!]! +} + type Label { color: String! createdAt: Date description: String id: ID! name: String! + position: Int } type LabelsError { @@ -730,11 +776,6 @@ enum LoginErrorCode { WRONG_SOURCE } -input LoginInput { - email: String! - password: String! -} - union LoginResult = LoginError | LoginSuccess type LoginSuccess { @@ -772,6 +813,27 @@ type MergeHighlightSuccess { overlapHighlightIdList: [String!]! } +type MoveLabelError { + errorCodes: [MoveLabelErrorCode!]! +} + +enum MoveLabelErrorCode { + BAD_REQUEST + NOT_FOUND + UNAUTHORIZED +} + +input MoveLabelInput { + afterLabelId: ID + labelId: ID! +} + +union MoveLabelResult = MoveLabelError | MoveLabelSuccess + +type MoveLabelSuccess { + label: Label! +} + type Mutation { addPopularRead(name: String!): AddPopularReadResult! createArticle(input: CreateArticleInput!): CreateArticleResult! @@ -785,6 +847,7 @@ type Mutation { deleteAccount(userID: ID!): DeleteAccountResult! deleteHighlight(highlightId: ID!): DeleteHighlightResult! deleteHighlightReply(highlightReplyId: ID!): DeleteHighlightReplyResult! + deleteIntegration(id: ID!): DeleteIntegrationResult! deleteLabel(id: ID!): DeleteLabelResult! deleteNewsletterEmail(newsletterEmailId: ID!): DeleteNewsletterEmailResult! deleteReaction(id: ID!): DeleteReactionResult! @@ -794,8 +857,8 @@ type Mutation { googleLogin(input: GoogleLoginInput!): LoginResult! googleSignup(input: GoogleSignupInput!): GoogleSignupResult! logOut: LogOutResult! - login(input: LoginInput!): LoginResult! mergeHighlight(input: MergeHighlightInput!): MergeHighlightResult! + moveLabel(input: MoveLabelInput!): MoveLabelResult! reportItem(input: ReportItemInput!): ReportItemResult! revokeApiKey(id: ID!): RevokeApiKeyResult! saveArticleReadingProgress(input: SaveArticleReadingProgressInput!): SaveArticleReadingProgressResult! @@ -805,6 +868,7 @@ type Mutation { setBookmarkArticle(input: SetBookmarkArticleInput!): SetBookmarkArticleResult! setDeviceToken(input: SetDeviceTokenInput!): SetDeviceTokenResult! setFollow(input: SetFollowInput!): SetFollowResult! + setIntegration(input: SetIntegrationInput!): SetIntegrationResult! setLabels(input: SetLabelsInput!): SetLabelsResult! setLabelsForHighlight(input: SetLabelsForHighlightInput!): SetLabelsResult! setLinkArchived(input: ArchiveLinkInput!): ArchiveLinkResult! @@ -812,7 +876,6 @@ type Mutation { setShareHighlight(input: SetShareHighlightInput!): SetShareHighlightResult! setUserPersonalization(input: SetUserPersonalizationInput!): SetUserPersonalizationResult! setWebhook(input: SetWebhookInput!): SetWebhookResult! - signup(input: SignupInput!): SignupResult! subscribe(name: String!): SubscribeResult! unsubscribe(name: String!): UnsubscribeResult! updateHighlight(input: UpdateHighlightInput!): UpdateHighlightResult! @@ -916,6 +979,7 @@ type Query { getFollowing(userId: ID): GetFollowingResult! getUserPersonalization: GetUserPersonalizationResult! hello: String + integrations: IntegrationsResult! labels: LabelsResult! me: User newsletterEmails: NewsletterEmailsResult! @@ -925,6 +989,7 @@ type Query { sharedArticle(selectedHighlightId: String, slug: String!, username: String!): SharedArticleResult! subscriptions(sort: SortParams): SubscriptionsResult! typeaheadSearch(first: Int, query: String!): TypeaheadSearchResult! + updatesSince(after: String, first: Int, since: Date!): UpdatesSinceResult! user(userId: ID, username: String): UserResult! users: UsersResult! validateUsername(username: String!): Boolean! @@ -1207,6 +1272,31 @@ type SetFollowSuccess { updatedUser: User! } +type SetIntegrationError { + errorCodes: [SetIntegrationErrorCode!]! +} + +enum SetIntegrationErrorCode { + ALREADY_EXISTS + BAD_REQUEST + INVALID_TOKEN + NOT_FOUND + UNAUTHORIZED +} + +input SetIntegrationInput { + enabled: Boolean! + id: ID + token: String! + type: IntegrationType! +} + +union SetIntegrationResult = SetIntegrationError | SetIntegrationSuccess + +type SetIntegrationSuccess { + integration: Integration! +} + type SetLabelsError { errorCodes: [SetLabelsErrorCode!]! } @@ -1347,35 +1437,17 @@ type SharedArticleSuccess { article: Article! } -type SignupError { - errorCodes: [SignupErrorCode]! -} - enum SignupErrorCode { ACCESS_DENIED EXPIRED_TOKEN GOOGLE_AUTH_ERROR + INVALID_EMAIL INVALID_PASSWORD INVALID_USERNAME UNKNOWN USER_EXISTS } -input SignupInput { - bio: String - email: String! - name: String! - password: String! - pictureUrl: String - username: String! -} - -union SignupResult = SignupError | SignupSuccess - -type SignupSuccess { - me: User! -} - enum SortBy { PUBLISHED_AT SAVED_AT @@ -1444,6 +1516,13 @@ type SubscriptionsSuccess { subscriptions: [Subscription!]! } +type SyncUpdatedItemEdge { + cursor: String! + itemID: ID! + node: SearchItem + updateReason: UpdateReason! +} + type TypeaheadSearchError { errorCodes: [TypeaheadSearchErrorCode!]! } @@ -1596,6 +1675,12 @@ type UpdatePageSuccess { updatedPage: Article! } +enum UpdateReason { + CREATED + DELETED + UPDATED +} + type UpdateReminderError { errorCodes: [UpdateReminderErrorCode!]! } @@ -1687,6 +1772,21 @@ type UpdateUserSuccess { user: User! } +type UpdatesSinceError { + errorCodes: [UpdatesSinceErrorCode!]! +} + +enum UpdatesSinceErrorCode { + UNAUTHORIZED +} + +union UpdatesSinceResult = UpdatesSinceError | UpdatesSinceSuccess + +type UpdatesSinceSuccess { + edges: [SyncUpdatedItemEdge!]! + pageInfo: PageInfo! +} + type UploadFileRequestError { errorCodes: [UploadFileRequestErrorCode!]! } diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/AppModule.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/AppModule.kt new file mode 100644 index 000000000..c48739f95 --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/AppModule.kt @@ -0,0 +1,21 @@ +package app.omnivore.omnivore + +import android.content.Context +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object AppModule { + + @Singleton + @Provides + fun provideDataStoreRepository( + @ApplicationContext app: Context + ): DatastoreRepository = OmnivoreDatastore(app) + +} 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 new file mode 100644 index 000000000..9ae6feaf6 --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/Constants.kt @@ -0,0 +1,11 @@ +package app.omnivore.omnivore + +object Constants { + const val demoProdURL = "https://api-demo.omnivore.app" + const val dataStoreName = "omnivore-datastore" +} + +object DatastoreKeys { + const val omnivoreAuthToken = "omnivoreAuthToken" + const val omnivoreAuthCookieString = "omnivoreAuthCookieString" +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DatastoreRepository.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DatastoreRepository.kt new file mode 100644 index 000000000..7392ffeb9 --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/DatastoreRepository.kt @@ -0,0 +1,64 @@ +package app.omnivore.omnivore + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.* +import androidx.datastore.preferences.preferencesDataStore +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import javax.inject.Inject + +interface DatastoreRepository { + val hasAuthTokenFlow: Flow + suspend fun clear() + suspend fun putString(key: String, value: String) + suspend fun putInt(key: String, value: Int) + suspend fun getString(key: String): String? + suspend fun getInt(key: String): Int? +} + +class OmnivoreDatastore @Inject constructor( + private val context: Context +) : DatastoreRepository { + private val Context.dataStore: DataStore by preferencesDataStore( + name = Constants.dataStoreName + ) + + override suspend fun putString(key: String, value: String) { + val preferencesKey = stringPreferencesKey(key) + context.dataStore.edit { preferences -> + preferences[preferencesKey] = value + } + } + + override suspend fun putInt(key: String, value: Int) { + val preferencesKey = intPreferencesKey(key) + context.dataStore.edit { preferences -> + preferences[preferencesKey] = value + } + } + + override suspend fun getString(key: String): String? { + val preferencesKey = stringPreferencesKey(key) + val preferences = context.dataStore.data.first() + return preferences[preferencesKey] + } + + override suspend fun getInt(key: String): Int? { + val preferencesKey = intPreferencesKey(key) + val preferences = context.dataStore.data.first() + return preferences[preferencesKey] + } + + override suspend fun clear() { + context.dataStore.edit { it.clear() } + } + + override val hasAuthTokenFlow: Flow = context + .dataStore.data.map { preferences -> + val key = stringPreferencesKey(DatastoreKeys.omnivoreAuthToken) + val token = preferences[key] + token != null + } +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/GoogleAuth.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/GoogleAuth.kt new file mode 100644 index 000000000..ba8b9eae0 --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/GoogleAuth.kt @@ -0,0 +1,53 @@ +package app.omnivore.omnivore + +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 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) + .requestEmail() + .requestIdToken(stringResource(R.string.gcp_id)) + .requestId() + .requestProfile() + .build() + + val startForResult = + rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult -> + if (result.resultCode == Activity.RESULT_OK) { + val intent = result.data + if (result.data != null) { + val task: Task = GoogleSignIn.getSignedInAccountFromIntent(intent) + viewModel.handleGoogleAuthTask(task) + } + } + } + + GoogleSignInButton( + 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) + } + ) +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/GoogleSignInButton.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/GoogleSignInButton.kt new file mode 100644 index 000000000..e80f8c0ce --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/GoogleSignInButton.kt @@ -0,0 +1,70 @@ +package app.omnivore.omnivore + +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.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape + +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.unit.dp + +@ExperimentalMaterialApi +@Composable +fun GoogleSignInButton( + text: String, + loadingText: String = "Signing in...", + icon: Painter, + isLoading: Boolean = false, + shape: Shape = Shapes().medium, + borderColor: Color = Color.LightGray, + backgroundColor: Color = MaterialTheme.colors.surface, + progressIndicatorColor: Color = MaterialTheme.colors.primary, + onClick: () -> Unit +) { + Surface( + modifier = Modifier.clickable( + enabled = !isLoading, + onClick = onClick + ), + shape = shape, + border = BorderStroke(width = 1.dp, color = borderColor), + color = backgroundColor + ) { + Row( + modifier = Modifier + .padding( + start = 12.dp, + end = 16.dp, + top = 12.dp, + bottom = 12.dp + ), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center, + ) { + Icon( + painter = icon, + contentDescription = "SignInButton", + tint = Color.Unspecified + ) + Spacer(modifier = Modifier.width(8.dp)) + + Text(text = if (isLoading) loadingText else text) + if (isLoading) { + Spacer(modifier = Modifier.width(16.dp)) + CircularProgressIndicator( + modifier = Modifier + .height(16.dp) + .width(16.dp), + strokeWidth = 2.dp, + color = progressIndicatorColor + ) + } + } + } +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/LoginViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/LoginViewModel.kt new file mode 100644 index 000000000..14db1873b --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/LoginViewModel.kt @@ -0,0 +1,111 @@ +package app.omnivore.omnivore + +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 com.google.android.gms.auth.api.signin.GoogleSignInAccount +import com.google.android.gms.common.api.ApiException +import com.google.android.gms.tasks.Task +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class LoginViewModel @Inject constructor( + private val datastoreRepo: DatastoreRepository +): ViewModel() { + var isLoading by mutableStateOf(false) + private set + + var errorMessage by mutableStateOf(null) + private set + + val hasAuthTokenLiveData: LiveData = datastoreRepo + .hasAuthTokenFlow + .distinctUntilChanged() + .asLiveData() + + fun login(email: String, password: String) { + val emailLogin = RetrofitHelper.getInstance().create(EmailLoginSubmit::class.java) + + viewModelScope.launch { + isLoading = true + errorMessage = null + + val result = emailLogin.submitEmailLogin( + EmailLoginCredentials(email = email, password = password) + ) + + isLoading = false + + if (result.body()?.pendingEmailVerification == true) { + errorMessage = "Email needs verification" + return@launch + } + + 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" + } + + if (result.body()?.authCookieString != null) { + datastoreRepo.putString( + DatastoreKeys.omnivoreAuthCookieString, result.body()?.authCookieString!! + ) + } + } + } + + fun logout() { + viewModelScope.launch { + datastoreRepo.clear() + } + } + + fun resetErrorMessage() { + 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 + + if (googleIdToken == null) { + return + } + + val login = RetrofitHelper.getInstance().create(AuthProviderLoginSubmit::class.java) + + viewModelScope.launch { + isLoading = true + errorMessage = null + + val result = login.submitAuthProviderLogin( + SignInParams(token = googleIdToken, provider = "GOOGLE") + ) + + isLoading = false + + 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" + } + + if (result.body()?.authCookieString != null) { + datastoreRepo.putString( + DatastoreKeys.omnivoreAuthCookieString, result.body()?.authCookieString!! + ) + } + } + } +} 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 f81c89a49..fbdd52768 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,75 +1,180 @@ package app.omnivore.omnivore -import android.content.ContentValues.TAG -import android.content.res.Configuration +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Context import android.os.Bundle -import android.util.Log +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.material.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf +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.text.input.ImeAction import androidx.compose.ui.text.input.PasswordVisualTransformation -import androidx.compose.ui.tooling.preview.Preview 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 dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch +@AndroidEntryPoint class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + + val viewModel: LoginViewModel by viewModels() + setContent { OmnivoreTheme { // A surface container using the 'background' color from the theme - Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) { - LoginView() - } + ScreenMain(viewModel = viewModel) +// Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) { +// WelcomeView(viewModel) +// } } } + + // animate the view up when keyboard appears + WindowCompat.setDecorFitsSystemWindows(window, false) + val rootView = findViewById(android.R.id.content).rootView + ViewCompat.setOnApplyWindowInsetsListener(rootView) { _, insets -> + val imeHeight = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom + rootView.setPadding(0, 0, 0, imeHeight) + insets + } } } @Composable -fun LoginView() { - var email by rememberSaveable { mutableStateOf("") } - var password by rememberSaveable { mutableStateOf("") } +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.colors.background) + .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 = { Log.v(TAG, "$email / $password") } + 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) + } } } -//Log.v(TAG, "${numerator / denominator}") +@OptIn(ExperimentalMaterial3Api::class) @Composable fun LoginFields( email: String, password: String, onEmailChange: (String) -> Unit, onPasswordChange: (String) -> Unit, - onLoginClick: (String) -> Unit + onLoginClick: () -> Unit ) { val context = LocalContext.current val focusManager = LocalFocusManager.current @@ -81,7 +186,7 @@ fun LoginFields( verticalArrangement = Arrangement.spacedBy(25.dp), horizontalAlignment = Alignment.CenterHorizontally ) { - Text("Please login") + Text("Never miss a great read") OutlinedTextField( value = email, @@ -104,17 +209,17 @@ fun LoginFields( Button(onClick = { if (email.isNotBlank() && password.isNotBlank()) { - onLoginClick(email) + onLoginClick() focusManager.clearFocus() } else { Toast.makeText( context, - "Please enter an email and password", + "Please enter an email address and password.", Toast.LENGTH_SHORT ).show() } }) { - Text("Login") + Text(text = "Login") } } } @@ -124,10 +229,10 @@ fun LoginFields( // showBackground = true, // name = "Dark Mode" //) -@Preview(showBackground = true) -@Composable -fun DefaultPreview() { - OmnivoreTheme { - LoginView() - } -} +//@Preview(showBackground = true) +//@Composable +//fun DefaultPreview() { +// OmnivoreTheme { +// LoginView() +// } +//} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/NewFlowActivity.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/NewFlowActivity.kt new file mode 100644 index 000000000..0748fda0f --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/NewFlowActivity.kt @@ -0,0 +1,11 @@ +package app.omnivore.omnivore + +import androidx.compose.material.ExperimentalMaterialApi + +// Not sure why we need this class, but directly opening SaveSheetActivity +// causes the app to crash. + +@OptIn(ExperimentalMaterialApi::class) +class NewFlowActivity : SaveSheetActivity() { + +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/OmnivoreApplication.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/OmnivoreApplication.kt new file mode 100644 index 000000000..7e339793a --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/OmnivoreApplication.kt @@ -0,0 +1,8 @@ +package app.omnivore.omnivore + +import android.app.Application +import dagger.hilt.android.HiltAndroidApp + +@HiltAndroidApp +class OmnivoreApplication: Application() { +} 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 new file mode 100644 index 000000000..c3956f73e --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/RESTNetworker.kt @@ -0,0 +1,50 @@ +package app.omnivore.omnivore + +import retrofit2.Response +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import retrofit2.http.Body +import retrofit2.http.Headers +import retrofit2.http.POST + +data class AuthPayload( + val authCookieString: String, + val authToken: String +) + +data class SignInParams( + val token: String, + val provider: String, // APPLE or GOOGLE + val source: String = "ANDROID" +) + +data class EmailAuthPayload( + val authCookieString: String?, + val authToken: String?, + val pendingEmailVerification: Boolean? +) + +data class EmailLoginCredentials( + val email: String, + val password: String +) + +interface EmailLoginSubmit { + @Headers("Content-Type: application/json") + @POST("/api/mobile-auth/email-sign-in") + suspend fun submitEmailLogin(@Body credentials: EmailLoginCredentials): Response +} + +interface AuthProviderLoginSubmit { + @Headers("Content-Type: application/json") + @POST("/api/mobile-auth/sign-in") + suspend fun submitAuthProviderLogin(@Body params: SignInParams): Response +} + +object RetrofitHelper { + fun getInstance(): Retrofit { + return Retrofit.Builder().baseUrl(Constants.demoProdURL) + .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 new file mode 100644 index 000000000..c6ca342c8 --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/Routes.kt @@ -0,0 +1,6 @@ +package app.omnivore.omnivore + +sealed class Routes(val route: String) { + object Splash : Routes("Splash") + object EmailLogin : Routes("EmailLogin") +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/SaveContent.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/SaveContent.kt new file mode 100644 index 000000000..4a569178b --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/SaveContent.kt @@ -0,0 +1,41 @@ +package app.omnivore.omnivore + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +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.runtime.rememberCoroutineScope +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch + +@Composable +@OptIn(ExperimentalMaterialApi::class) +fun SaveContent(viewModel: SaveViewModel, modalBottomSheetState: ModalBottomSheetState, modifier: Modifier) { + val coroutineScope = rememberCoroutineScope() + + Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) { + Column( + verticalArrangement = Arrangement.SpaceBetween, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .background(MaterialTheme.colors.background) + .fillMaxSize() + .padding(top = 48.dp, bottom = 32.dp) + ) { + Text(text = viewModel.message ?: "Saving") + Button(onClick = { + coroutineScope.launch { + modalBottomSheetState.hide() + } + }) { + 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/SaveSheetActivity.kt new file mode 100644 index 000000000..521e5c3df --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/SaveSheetActivity.kt @@ -0,0 +1,169 @@ +package app.omnivore.omnivore + +import android.content.ContentValues +import android.content.Intent +import android.os.Bundle +import android.util.Log +import androidx.activity.compose.BackHandler +import androidx.activity.compose.setContent +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment.Companion.TopCenter +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + + +@AndroidEntryPoint +@OptIn(ExperimentalMaterialApi::class) +abstract class SaveSheetActivity: AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val viewModel: SaveViewModel by viewModels() + var extractedText: String? = null + + when (intent?.action) { + Intent.ACTION_SEND -> { + if (intent.type?.startsWith("text/plain") == true) { + intent.getStringExtra(Intent.EXTRA_TEXT)?.let { + Log.d(ContentValues.TAG, "Extracted text: $extractedText") + extractedText = it + viewModel.saveURL(it) + } + } + + if (intent.type?.startsWith("text/html") == true) { + intent.getStringExtra(Intent.EXTRA_HTML_TEXT)?.let { + extractedText = it + } + } + } + else -> { + // Handle other intents, such as being started from the home screen + } + } + + setContent { + val coroutineScope = rememberCoroutineScope() + val modalBottomSheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden) + val isSheetOpened = remember { mutableStateOf(false) } + + ModalBottomSheetLayout( + sheetBackgroundColor = Color.Transparent, + sheetState = modalBottomSheetState, + sheetContent = { + BottomSheetUI { + ScreenContent(viewModel, modalBottomSheetState) + } + } + ) {} + + BackHandler { + onFinish(coroutineScope, modalBottomSheetState) + } + + // Take action based on hidden state + LaunchedEffect(modalBottomSheetState.currentValue) { + when (modalBottomSheetState.currentValue) { + ModalBottomSheetValue.Hidden -> { + handleBottomSheetAtHiddenState( + isSheetOpened, + modalBottomSheetState + ) + } + else -> { + Log.i(TAG, "Bottom sheet ${modalBottomSheetState.currentValue} state") + } + } + } + } + } + + @Composable + private fun BottomSheetUI(content: @Composable () -> Unit) { + Box( + modifier = Modifier + .wrapContentHeight() + .fillMaxWidth() + .clip(RoundedCornerShape(topEnd = 20.dp, topStart = 20.dp)) + .background(Color.White) + .statusBarsPadding() + ) { + content() + + Divider( + color = Color.Gray, + thickness = 5.dp, + modifier = Modifier + .padding(top = 15.dp) + .align(TopCenter) + .width(80.dp) + .clip(RoundedCornerShape(50.dp)) + ) + } + } + + // Helper methods + private suspend fun handleBottomSheetAtHiddenState( + isSheetOpened: MutableState, + modalBottomSheetState: ModalBottomSheetState + ) { + when { + !isSheetOpened.value -> initializeModalLayout(isSheetOpened, modalBottomSheetState) + else -> exit() + } + } + + private suspend fun initializeModalLayout( + isSheetOpened: MutableState, + modalBottomSheetState: ModalBottomSheetState + ) { + isSheetOpened.value = true + modalBottomSheetState.show() + } + + open fun exit() = finish() + + private fun onFinish( + coroutineScope: CoroutineScope, + modalBottomSheetState: ModalBottomSheetState, + withResults: Boolean = false, + result: Intent? = null + ) { + coroutineScope.launch { + if (withResults) setResult(RESULT_OK) + result?.let { intent = it} + modalBottomSheetState.hide() // will trigger the LaunchedEffect + } + } + + @Composable + fun ScreenContent( + viewModel: SaveViewModel, + modalBottomSheetState: ModalBottomSheetState + ) { + Box(modifier = Modifier.height(300.dp).background(Color.White)) { + SaveContent(viewModel, modalBottomSheetState, modifier = Modifier.fillMaxSize()) + } + } + + override fun onPause() { + super.onPause() + overridePendingTransition(0, 0) + } + + companion object { + private val TAG = SaveSheetActivity::class.java.simpleName + } +} diff --git a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/SaveViewModel.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/SaveViewModel.kt new file mode 100644 index 000000000..1cbf43078 --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/SaveViewModel.kt @@ -0,0 +1,73 @@ +package app.omnivore.omnivore + +import android.content.ContentValues +import android.util.Log +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import app.omnivore.omnivore.graphql.generated.SaveUrlMutation +import app.omnivore.omnivore.graphql.generated.type.SaveUrlInput +import com.apollographql.apollo3.ApolloClient +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import java.util.* +import javax.inject.Inject + +@HiltViewModel +class SaveViewModel @Inject constructor( + private val datastoreRepo: DatastoreRepository +): ViewModel() { + var isLoading by mutableStateOf(false) + private set + + var message by mutableStateOf(null) + private set + + private fun getAuthToken(): String? = runBlocking { + datastoreRepo.getString(DatastoreKeys.omnivoreAuthToken) + } + + fun saveURL(url: String) { + viewModelScope.launch { + isLoading = true + message = "Saving to Omnivore..." + + val apiKey = getAuthToken() + + if (apiKey == null) { + message = "You are not logged in. Please login before saving." + isLoading = false + return@launch + } + + val apolloClient = ApolloClient.Builder() + .serverUrl("${Constants.demoProdURL}/api/graphql") + .addHttpHeader("Authorization", value = apiKey) + .build() + + val response = apolloClient.mutation( + SaveUrlMutation( + SaveUrlInput( + clientRequestId = UUID.randomUUID().toString(), + source = "android", + url = url + ) + ) + ).execute() + + isLoading = false + + val success = (response.data?.saveUrl?.onSaveSuccess?.url != null) + message = if (success) { + "Page Saved" + } else { + "There was an error saving your page" + } + + Log.d(ContentValues.TAG, "Saved URL?: ${success.toString()}") + } + } +} 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 new file mode 100644 index 000000000..a2248f02e --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ScreenMain.kt @@ -0,0 +1,22 @@ +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 new file mode 100644 index 000000000..8d2784eb8 --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/screen/EmailLogin.kt @@ -0,0 +1,31 @@ +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 new file mode 100644 index 000000000..8e79e6930 --- /dev/null +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/screen/SplashPage.kt @@ -0,0 +1,78 @@ +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/theme/Color.kt b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/theme/Color.kt index 6ae28aba9..e974e2bd3 100644 --- a/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/theme/Color.kt +++ b/android/Omnivore/app/src/main/java/app/omnivore/omnivore/ui/theme/Color.kt @@ -1,8 +1,62 @@ package app.omnivore.omnivore.ui.theme - import androidx.compose.ui.graphics.Color -val Purple200 = Color(0xFFBB86FC) -val Purple500 = Color(0xFF6200EE) -val Purple700 = Color(0xFF3700B3) -val Teal200 = Color(0xFF03DAC5) \ No newline at end of file +val md_theme_light_primary = Color(0xFF745B00) +val md_theme_light_onPrimary = Color(0xFFFFFFFF) +val md_theme_light_primaryContainer = Color(0xFFFFE08C) +val md_theme_light_onPrimaryContainer = Color(0xFF241A00) +val md_theme_light_secondary = Color(0xFF6D5E00) +val md_theme_light_onSecondary = Color(0xFFFFFFFF) +val md_theme_light_secondaryContainer = Color(0xFFFCE365) +val md_theme_light_onSecondaryContainer = Color(0xFF211B00) +val md_theme_light_tertiary = Color(0xFF4B670A) +val md_theme_light_onTertiary = Color(0xFFFFFFFF) +val md_theme_light_tertiaryContainer = Color(0xFFCBEF86) +val md_theme_light_onTertiaryContainer = Color(0xFF141F00) +val md_theme_light_error = Color(0xFFBA1A1A) +val md_theme_light_errorContainer = Color(0xFFFFDAD6) +val md_theme_light_onError = Color(0xFFFFFFFF) +val md_theme_light_onErrorContainer = Color(0xFF410002) +val md_theme_light_background = Color(0xFFF7FFED) +val md_theme_light_onBackground = Color(0xFF032100) +val md_theme_light_surface = Color(0xFFF7FFED) +val md_theme_light_onSurface = Color(0xFF032100) +val md_theme_light_surfaceVariant = Color(0xFFEBE1CF) +val md_theme_light_onSurfaceVariant = Color(0xFF4C4639) +val md_theme_light_outline = Color(0xFF7E7667) +val md_theme_light_inverseOnSurface = Color(0xFFCBFFB5) +val md_theme_light_inverseSurface = Color(0xFF083900) +val md_theme_light_inversePrimary = Color(0xFFEFC125) +val md_theme_light_shadow = Color(0xFF000000) +val md_theme_light_surfaceTint = Color(0xFF745B00) + +val md_theme_dark_primary = Color(0xFFEFC125) +val md_theme_dark_onPrimary = Color(0xFF3D2F00) +val md_theme_dark_primaryContainer = Color(0xFF584400) +val md_theme_dark_onPrimaryContainer = Color(0xFFFFE08C) +val md_theme_dark_secondary = Color(0xFFDEC64C) +val md_theme_dark_onSecondary = Color(0xFF393000) +val md_theme_dark_secondaryContainer = Color(0xFF524600) +val md_theme_dark_onSecondaryContainer = Color(0xFFFCE365) +val md_theme_dark_tertiary = Color(0xFFB0D36D) +val md_theme_dark_onTertiary = Color(0xFF243600) +val md_theme_dark_tertiaryContainer = Color(0xFF364E00) +val md_theme_dark_onTertiaryContainer = Color(0xFFCBEF86) +val md_theme_dark_error = Color(0xFFFFB4AB) +val md_theme_dark_errorContainer = Color(0xFF93000A) +val md_theme_dark_onError = Color(0xFF690005) +val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6) +val md_theme_dark_background = Color(0xFF032100) +val md_theme_dark_onBackground = Color(0xFFB4F39B) +val md_theme_dark_surface = Color(0xFF032100) +val md_theme_dark_onSurface = Color(0xFFB4F39B) +val md_theme_dark_surfaceVariant = Color(0xFF4C4639) +val md_theme_dark_onSurfaceVariant = Color(0xFFCFC5B4) +val md_theme_dark_outline = Color(0xFF989080) +val md_theme_dark_inverseOnSurface = Color(0xFF032100) +val md_theme_dark_inverseSurface = Color(0xFFB4F39B) +val md_theme_dark_inversePrimary = Color(0xFF745B00) +val md_theme_dark_shadow = Color(0xFF000000) +val md_theme_dark_surfaceTint = Color(0xFFEFC125) + +val seed = Color(0xFFE2B513) 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 a1538a94b..f55b9dea7 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 @@ -1,44 +1,89 @@ package app.omnivore.omnivore.ui.theme +import android.os.Build import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material.MaterialTheme -import androidx.compose.material.darkColors -import androidx.compose.material.lightColors +import androidx.compose.material3.* import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext -private val DarkColorPalette = darkColors( - primary = Purple200, - primaryVariant = Purple700, - secondary = Teal200 +private val LightColors = lightColorScheme( + primary = md_theme_light_primary, + onPrimary = md_theme_light_onPrimary, + primaryContainer = md_theme_light_primaryContainer, + onPrimaryContainer = md_theme_light_onPrimaryContainer, + secondary = md_theme_light_secondary, + onSecondary = md_theme_light_onSecondary, + secondaryContainer = md_theme_light_secondaryContainer, + onSecondaryContainer = md_theme_light_onSecondaryContainer, + tertiary = md_theme_light_tertiary, + onTertiary = md_theme_light_onTertiary, + tertiaryContainer = md_theme_light_tertiaryContainer, + onTertiaryContainer = md_theme_light_onTertiaryContainer, + error = md_theme_light_error, + errorContainer = md_theme_light_errorContainer, + onError = md_theme_light_onError, + onErrorContainer = md_theme_light_onErrorContainer, + background = md_theme_light_background, + onBackground = md_theme_light_onBackground, + surface = md_theme_light_surface, + onSurface = md_theme_light_onSurface, + surfaceVariant = md_theme_light_surfaceVariant, + onSurfaceVariant = md_theme_light_onSurfaceVariant, + outline = md_theme_light_outline, + inverseOnSurface = md_theme_light_inverseOnSurface, + inverseSurface = md_theme_light_inverseSurface, + inversePrimary = md_theme_light_inversePrimary, + surfaceTint = md_theme_light_surfaceTint, ) -private val LightColorPalette = lightColors( - primary = Purple500, - primaryVariant = Purple700, - secondary = Teal200 - /* Other default colors to override - background = Color.White, - surface = Color.White, - onPrimary = Color.White, - onSecondary = Color.Black, - onBackground = Color.Black, - onSurface = Color.Black, - */ +private val DarkColors = darkColorScheme( + primary = md_theme_dark_primary, + onPrimary = md_theme_dark_onPrimary, + primaryContainer = md_theme_dark_primaryContainer, + onPrimaryContainer = md_theme_dark_onPrimaryContainer, + secondary = md_theme_dark_secondary, + onSecondary = md_theme_dark_onSecondary, + secondaryContainer = md_theme_dark_secondaryContainer, + onSecondaryContainer = md_theme_dark_onSecondaryContainer, + tertiary = md_theme_dark_tertiary, + onTertiary = md_theme_dark_onTertiary, + tertiaryContainer = md_theme_dark_tertiaryContainer, + onTertiaryContainer = md_theme_dark_onTertiaryContainer, + error = md_theme_dark_error, + errorContainer = md_theme_dark_errorContainer, + onError = md_theme_dark_onError, + onErrorContainer = md_theme_dark_onErrorContainer, + background = md_theme_dark_background, + onBackground = md_theme_dark_onBackground, + surface = md_theme_dark_surface, + onSurface = md_theme_dark_onSurface, + surfaceVariant = md_theme_dark_surfaceVariant, + onSurfaceVariant = md_theme_dark_onSurfaceVariant, + outline = md_theme_dark_outline, + inverseOnSurface = md_theme_dark_inverseOnSurface, + inverseSurface = md_theme_dark_inverseSurface, + inversePrimary = md_theme_dark_inversePrimary, + surfaceTint = md_theme_dark_surfaceTint, ) @Composable -fun OmnivoreTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) { - val colors = if (darkTheme) { - DarkColorPalette +fun OmnivoreTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + useDynamicTheme: Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S, + content: @Composable () -> Unit +) { + val colorScheme = if (useDynamicTheme) { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) } else { - LightColorPalette + if (darkTheme) DarkColors else LightColors } MaterialTheme( - colors = colors, - typography = Typography, - shapes = Shapes, + colorScheme = colorScheme, +// typography = Typography, +// shapes = Shapes, content = content ) } diff --git a/android/Omnivore/app/src/main/res/drawable/ic_logo_google.xml b/android/Omnivore/app/src/main/res/drawable/ic_logo_google.xml new file mode 100644 index 000000000..d9425d95c --- /dev/null +++ b/android/Omnivore/app/src/main/res/drawable/ic_logo_google.xml @@ -0,0 +1,22 @@ + + + + + + diff --git a/android/Omnivore/app/src/main/res/drawable/ic_x_2.xml b/android/Omnivore/app/src/main/res/drawable/ic_x_2.xml new file mode 100644 index 000000000..0899ee9ed --- /dev/null +++ b/android/Omnivore/app/src/main/res/drawable/ic_x_2.xml @@ -0,0 +1,16 @@ + + + + diff --git a/android/Omnivore/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/Omnivore/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml deleted file mode 100644 index 6b78462d6..000000000 --- a/android/Omnivore/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/android/Omnivore/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/Omnivore/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml deleted file mode 100644 index 6b78462d6..000000000 --- a/android/Omnivore/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/android/SaveToOmnivore/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/Omnivore/app/src/main/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from android/SaveToOmnivore/app/src/main/res/mipmap-hdpi/ic_launcher.png rename to android/Omnivore/app/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/android/Omnivore/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/android/Omnivore/app/src/main/res/mipmap-hdpi/ic_launcher.webp deleted file mode 100644 index c209e78ec..000000000 Binary files a/android/Omnivore/app/src/main/res/mipmap-hdpi/ic_launcher.webp and /dev/null differ diff --git a/android/SaveToOmnivore/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/android/Omnivore/app/src/main/res/mipmap-hdpi/ic_launcher_round.png similarity index 100% rename from android/SaveToOmnivore/app/src/main/res/mipmap-hdpi/ic_launcher_round.png rename to android/Omnivore/app/src/main/res/mipmap-hdpi/ic_launcher_round.png diff --git a/android/Omnivore/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/android/Omnivore/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp deleted file mode 100644 index b2dfe3d1b..000000000 Binary files a/android/Omnivore/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and /dev/null differ diff --git a/android/SaveToOmnivore/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/Omnivore/app/src/main/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from android/SaveToOmnivore/app/src/main/res/mipmap-mdpi/ic_launcher.png rename to android/Omnivore/app/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/android/Omnivore/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/android/Omnivore/app/src/main/res/mipmap-mdpi/ic_launcher.webp deleted file mode 100644 index 4f0f1d64e..000000000 Binary files a/android/Omnivore/app/src/main/res/mipmap-mdpi/ic_launcher.webp and /dev/null differ diff --git a/android/SaveToOmnivore/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/android/Omnivore/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png similarity index 100% rename from android/SaveToOmnivore/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png rename to android/Omnivore/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png diff --git a/android/SaveToOmnivore/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/android/Omnivore/app/src/main/res/mipmap-mdpi/ic_launcher_round.png similarity index 100% rename from android/SaveToOmnivore/app/src/main/res/mipmap-mdpi/ic_launcher_round.png rename to android/Omnivore/app/src/main/res/mipmap-mdpi/ic_launcher_round.png diff --git a/android/Omnivore/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/android/Omnivore/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp deleted file mode 100644 index 62b611da0..000000000 Binary files a/android/Omnivore/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and /dev/null differ diff --git a/android/SaveToOmnivore/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/Omnivore/app/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from android/SaveToOmnivore/app/src/main/res/mipmap-xhdpi/ic_launcher.png rename to android/Omnivore/app/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/android/Omnivore/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/android/Omnivore/app/src/main/res/mipmap-xhdpi/ic_launcher.webp deleted file mode 100644 index 948a3070f..000000000 Binary files a/android/Omnivore/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and /dev/null differ diff --git a/android/SaveToOmnivore/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/android/Omnivore/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png similarity index 100% rename from android/SaveToOmnivore/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png rename to android/Omnivore/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png diff --git a/android/SaveToOmnivore/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/android/Omnivore/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png similarity index 100% rename from android/SaveToOmnivore/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png rename to android/Omnivore/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png diff --git a/android/Omnivore/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/android/Omnivore/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp deleted file mode 100644 index 1b9a6956b..000000000 Binary files a/android/Omnivore/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/android/Omnivore/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/android/Omnivore/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp deleted file mode 100644 index 28d4b77f9..000000000 Binary files a/android/Omnivore/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and /dev/null differ diff --git a/android/SaveToOmnivore/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/android/Omnivore/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png similarity index 100% rename from android/SaveToOmnivore/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png rename to android/Omnivore/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png diff --git a/android/SaveToOmnivore/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/android/Omnivore/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png similarity index 100% rename from android/SaveToOmnivore/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png rename to android/Omnivore/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png diff --git a/android/Omnivore/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/android/Omnivore/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp deleted file mode 100644 index 9287f5083..000000000 Binary files a/android/Omnivore/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/android/SaveToOmnivore/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/Omnivore/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from android/SaveToOmnivore/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to android/Omnivore/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/android/Omnivore/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/android/Omnivore/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp deleted file mode 100644 index aa7d6427e..000000000 Binary files a/android/Omnivore/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and /dev/null differ diff --git a/android/SaveToOmnivore/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/android/Omnivore/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png similarity index 100% rename from android/SaveToOmnivore/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png rename to android/Omnivore/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png diff --git a/android/SaveToOmnivore/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/android/Omnivore/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png similarity index 100% rename from android/SaveToOmnivore/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png rename to android/Omnivore/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png diff --git a/android/Omnivore/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/android/Omnivore/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp deleted file mode 100644 index 9126ae37c..000000000 Binary files a/android/Omnivore/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/android/Omnivore/app/src/main/res/values/colors.xml b/android/Omnivore/app/src/main/res/values/colors.xml index ca1931bca..ee4d3d2e0 100644 --- a/android/Omnivore/app/src/main/res/values/colors.xml +++ b/android/Omnivore/app/src/main/res/values/colors.xml @@ -1,10 +1,7 @@ - #FFBB86FC - #FF6200EE - #FF3700B3 - #FF03DAC5 - #FF018786 #FF000000 #FFFFFFFF + #F7F7F7 + #B7B7B7 diff --git a/android/SaveToOmnivore/app/src/main/res/values/ic_launcher_background.xml b/android/Omnivore/app/src/main/res/values/ic_launcher_background.xml similarity index 100% rename from android/SaveToOmnivore/app/src/main/res/values/ic_launcher_background.xml rename to android/Omnivore/app/src/main/res/values/ic_launcher_background.xml diff --git a/android/Omnivore/app/src/main/res/values/strings.xml b/android/Omnivore/app/src/main/res/values/strings.xml index e537d2ef1..2f4570ea1 100644 --- a/android/Omnivore/app/src/main/res/values/strings.xml +++ b/android/Omnivore/app/src/main/res/values/strings.xml @@ -1,3 +1,4 @@ Omnivore - \ No newline at end of file + 590528454690-mpmbvcb4c3ifmnojmrultt37eo8f64at.apps.googleusercontent.com + diff --git a/android/Omnivore/app/src/main/res/values/themes.xml b/android/Omnivore/app/src/main/res/values/themes.xml index fb34267cb..f5b5ca333 100644 --- a/android/Omnivore/app/src/main/res/values/themes.xml +++ b/android/Omnivore/app/src/main/res/values/themes.xml @@ -1,7 +1,25 @@ - + + diff --git a/android/Omnivore/build.gradle b/android/Omnivore/build.gradle index abceb186b..a6761e531 100644 --- a/android/Omnivore/build.gradle +++ b/android/Omnivore/build.gradle @@ -1,14 +1,26 @@ buildscript { ext { - compose_version = '1.1.0-beta01' + compose_version = '1.3.0-alpha03' + lifecycle_version = '2.5.1' + kotlin_version = '1.6.20' } + + dependencies { + classpath "com.google.dagger:hilt-android-gradle-plugin:2.42" + classpath 'com.android.tools.build:gradle:7.2.2' + } + }// Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { - id 'com.android.application' version '7.2.1' apply false - id 'com.android.library' version '7.2.1' apply false - id 'org.jetbrains.kotlin.android' version '1.5.31' apply false + id 'com.android.application' version '7.2.2' apply false + id 'com.android.library' version '7.2.2' apply false + id 'org.jetbrains.kotlin.android' version '1.6.20' apply false +// id 'com.android.application' version '7.2.2' apply false +// id 'com.android.library' version '7.2.2' apply false +// id 'org.jetbrains.kotlin.android' version '1.7.20-Beta' apply false } task clean(type: Delete) { delete rootProject.buildDir } + diff --git a/android/Omnivore/gradle/wrapper/gradle-wrapper.properties b/android/Omnivore/gradle/wrapper/gradle-wrapper.properties index 29d14b04b..5bf8d8081 100644 --- a/android/Omnivore/gradle/wrapper/gradle-wrapper.properties +++ b/android/Omnivore/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Tue Aug 02 13:19:15 PDT 2022 +#Wed Aug 10 17:39:47 PDT 2022 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/android/SaveToOmnivore/.gitignore b/android/SaveToOmnivore/.gitignore deleted file mode 100644 index aa724b770..000000000 --- a/android/SaveToOmnivore/.gitignore +++ /dev/null @@ -1,15 +0,0 @@ -*.iml -.gradle -/local.properties -/.idea/caches -/.idea/libraries -/.idea/modules.xml -/.idea/workspace.xml -/.idea/navEditor.xml -/.idea/assetWizardSettings.xml -.DS_Store -/build -/captures -.externalNativeBuild -.cxx -local.properties diff --git a/android/SaveToOmnivore/app/.gitignore b/android/SaveToOmnivore/app/.gitignore deleted file mode 100644 index 42afabfd2..000000000 --- a/android/SaveToOmnivore/app/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/android/SaveToOmnivore/app/build.gradle b/android/SaveToOmnivore/app/build.gradle deleted file mode 100644 index 9c89431b1..000000000 --- a/android/SaveToOmnivore/app/build.gradle +++ /dev/null @@ -1,73 +0,0 @@ - -plugins { - id 'com.android.application' - id 'org.jetbrains.kotlin.android' - id("com.apollographql.apollo3").version("3.4.0") -} - -android { - compileSdk 32 - - defaultConfig { - applicationId "app.omnivore.savetoomnivore" - minSdk 23 - targetSdk 32 - versionCode 3 - versionName "0.3" - - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - vectorDrawables { - useSupportLibrary true - } - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - kotlinOptions { - jvmTarget = '1.8' - } - buildFeatures { - compose true - } - composeOptions { - kotlinCompilerExtensionVersion "1.2.0-alpha08" - } - packagingOptions { - resources { - excludes += '/META-INF/{AL2.0,LGPL2.1}' - } - } -} - -dependencies { - - implementation 'androidx.core:core-ktx:1.7.0' - implementation "androidx.compose.ui:ui:$compose_version" - implementation 'androidx.compose.material3:material3:1.0.0-alpha14' - implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" - implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1' - implementation 'androidx.activity:activity-compose:1.3.1' - implementation 'com.apollographql.apollo3:apollo-runtime:3.4.0' - implementation 'androidx.security:security-crypto-ktx:1.1.0-alpha03' - implementation 'androidx.datastore:datastore-core:1.0.0-rc01' - implementation "androidx.datastore:datastore-preferences:1.0.0" - - testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test.ext:junit:1.1.3' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' - androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" - debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" - debugImplementation "androidx.compose.ui:ui-test-manifest:$compose_version" -} - -apollo { - packageName.set("app.omnivore.generated") -} \ No newline at end of file diff --git a/android/SaveToOmnivore/app/proguard-rules.pro b/android/SaveToOmnivore/app/proguard-rules.pro deleted file mode 100644 index 481bb4348..000000000 --- a/android/SaveToOmnivore/app/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/android/SaveToOmnivore/app/src/androidTest/java/app/omnivore/savetoomnivore/ExampleInstrumentedTest.kt b/android/SaveToOmnivore/app/src/androidTest/java/app/omnivore/savetoomnivore/ExampleInstrumentedTest.kt deleted file mode 100644 index 7bda9bc5a..000000000 --- a/android/SaveToOmnivore/app/src/androidTest/java/app/omnivore/savetoomnivore/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -package app.omnivore.savetoomnivore - -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.ext.junit.runners.AndroidJUnit4 - -import org.junit.Test -import org.junit.runner.RunWith - -import org.junit.Assert.* - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("app.omnivore.savetoomnivore", appContext.packageName) - } -} \ No newline at end of file diff --git a/android/SaveToOmnivore/app/src/main/AndroidManifest.xml b/android/SaveToOmnivore/app/src/main/AndroidManifest.xml deleted file mode 100644 index bcab33f06..000000000 --- a/android/SaveToOmnivore/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/android/SaveToOmnivore/app/src/main/ic_launcher-playstore.png b/android/SaveToOmnivore/app/src/main/ic_launcher-playstore.png deleted file mode 100644 index 3f5ca0237..000000000 Binary files a/android/SaveToOmnivore/app/src/main/ic_launcher-playstore.png and /dev/null differ diff --git a/android/SaveToOmnivore/app/src/main/java/app/omnivore/savetoomnivore/AppDatastore.kt b/android/SaveToOmnivore/app/src/main/java/app/omnivore/savetoomnivore/AppDatastore.kt deleted file mode 100644 index 2ad6b5ded..000000000 --- a/android/SaveToOmnivore/app/src/main/java/app/omnivore/savetoomnivore/AppDatastore.kt +++ /dev/null @@ -1,45 +0,0 @@ -package app.omnivore.savetoomnivore - -import android.annotation.SuppressLint -import android.content.Context -import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.ui.platform.LocalContext -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences -import androidx.datastore.preferences.core.edit -import androidx.datastore.preferences.core.stringPreferencesKey -import androidx.datastore.preferences.preferencesDataStore -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map - -class AppDatastore(private val context: Context) { - private val Context.datastore: DataStore by preferencesDataStore(name = "app_datastore") - private val APIKEY = stringPreferencesKey(name = "API_KEY") - - companion object { - @SuppressLint("StaticFieldLeak") - var INSTANCE: AppDatastore? = null - fun getInstance(base: Context): AppDatastore? { - if (INSTANCE == null) { - synchronized(AppDatastore::class.java) { - INSTANCE = AppDatastore(base.applicationContext) - } - } - - return INSTANCE - } - } - - suspend fun setApiKey(apiKey: String) { - context.datastore.edit { preferences -> - preferences[APIKEY] = apiKey - } - } - - val getApiKey: Flow = context.datastore.data.map { preferences -> - preferences[APIKEY] ?: "" - } -} \ No newline at end of file diff --git a/android/SaveToOmnivore/app/src/main/java/app/omnivore/savetoomnivore/ExtensionActivity.kt b/android/SaveToOmnivore/app/src/main/java/app/omnivore/savetoomnivore/ExtensionActivity.kt deleted file mode 100644 index f0545f764..000000000 --- a/android/SaveToOmnivore/app/src/main/java/app/omnivore/savetoomnivore/ExtensionActivity.kt +++ /dev/null @@ -1,106 +0,0 @@ -package app.omnivore.savetoomnivore - -import android.app.Activity -import android.content.Context -import android.content.Intent -import android.os.Bundle -import android.text.TextUtils.isEmpty -import android.widget.Toast -import androidx.activity.ComponentActivity -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences -import androidx.datastore.preferences.preferencesDataStore -import app.omnivore.generated.SaveUrlMutation -import app.omnivore.generated.type.SaveUrlInput -import com.apollographql.apollo3.ApolloClient -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch -import java.util.* -import java.util.regex.Matcher -import java.util.regex.Pattern - - -private val Context.dataStore: DataStore by preferencesDataStore( - name = "settings" -) - -class ExtensionActivity : ComponentActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - when { - intent?.action == Intent.ACTION_SEND -> { - if ("text/plain" == intent.type) { - handleSendText(intent) // Handle text being sent - } - } - } - finish() - } - - fun saveURL(url: String) { - - GlobalScope.launch { - val apiKey = AppDatastore.getInstance(baseContext)?.getApiKey?.first() - - val apolloClient = ApolloClient.Builder() - .serverUrl("https://api-prod.omnivore.app/api/graphql") - .addHttpHeader( - "Authorization", - value = apiKey.toString() - ) - .build() - - val source = "android" - val clientRequestId = UUID.randomUUID().toString() - val response = apolloClient.mutation( - SaveUrlMutation( - SaveUrlInput( - clientRequestId = clientRequestId, - source = source, - url = url - ) - ) - ).execute() - - val success = (response.data?.saveUrl?.onSaveSuccess?.url != null) - - GlobalScope.launch(Dispatchers.Main) { - runOnUiThread(Runnable { - val message = if (success) "Saved to Omnivore" else "Error saving to Omnivore" - Toast.makeText(baseContext, message, Toast.LENGTH_LONG).show() - }) - } - } - } - - private fun handleSendText(intent: Intent) { - intent.getStringExtra(Intent.EXTRA_TEXT)?.let { - val url = getUrl(it) - if (url == null || isEmpty(url)) { - Toast.makeText(this, "Error: no URL found.", Toast.LENGTH_LONG).show() - } else { - Toast.makeText(this, "Saving to Omnivore", Toast.LENGTH_LONG).show() - saveURL(it) - } - } - } - - fun getUrl(s: String): String? { - val pattern = - Pattern.compile("[(http(s)?):\\/\\/(www\\.)?a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%_\\+.~#?&//=]*)") - try { - val matcher: Matcher = pattern.matcher(s) - if (matcher.find()) { - return s.substring(matcher.start(), matcher.end()) - } - } catch (e: Exception) { - System.out.println("exception parsing string") - System.out.println(e) - return null - } - return null - } -} diff --git a/android/SaveToOmnivore/app/src/main/java/app/omnivore/savetoomnivore/MainActivity.kt b/android/SaveToOmnivore/app/src/main/java/app/omnivore/savetoomnivore/MainActivity.kt deleted file mode 100644 index aaaff30e9..000000000 --- a/android/SaveToOmnivore/app/src/main/java/app/omnivore/savetoomnivore/MainActivity.kt +++ /dev/null @@ -1,63 +0,0 @@ -package app.omnivore.savetoomnivore - -import android.content.Context -import android.content.SharedPreferences -import android.os.Build -import android.os.Bundle -import android.provider.Settings -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.annotation.RequiresApi -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.material3.OutlinedTextField -import kotlinx.coroutines.launch -import androidx.lifecycle.lifecycleScope -import kotlinx.coroutines.GlobalScope - -class MainActivity : ComponentActivity() { - - @RequiresApi(Build.VERSION_CODES.M) - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - setContent { - MaterialTheme { - // A surface container using the 'background' color from the theme - Surface( - modifier = Modifier.fillMaxSize(), - color = MaterialTheme.colorScheme.background - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.fillMaxWidth() - ) { - Text("Save to Omnivore") - var apiKey by remember { mutableStateOf("") } - OutlinedTextField( - value = apiKey, - onValueChange = { - apiKey = it - GlobalScope.launch { - setApiKey(apiKey) - } - }, - label = { Text("API Key") } - ) - Text("Get an API key from https://omnivore.app/settings/api") - } - } - } - } - } - - private suspend fun setApiKey(apiKey: String) { - AppDatastore.getInstance(base = baseContext)?.setApiKey(apiKey) - } -} diff --git a/android/SaveToOmnivore/app/src/main/java/app/omnivore/savetoomnivore/ui/theme/Color.kt b/android/SaveToOmnivore/app/src/main/java/app/omnivore/savetoomnivore/ui/theme/Color.kt deleted file mode 100644 index babd26191..000000000 --- a/android/SaveToOmnivore/app/src/main/java/app/omnivore/savetoomnivore/ui/theme/Color.kt +++ /dev/null @@ -1,11 +0,0 @@ -package app.omnivore.savetoomnivore.ui.theme - -import androidx.compose.ui.graphics.Color - -val Purple80 = Color(0xFFD0BCFF) -val PurpleGrey80 = Color(0xFFCCC2DC) -val Pink80 = Color(0xFFEFB8C8) - -val Purple40 = Color(0xFF6650a4) -val PurpleGrey40 = Color(0xFF625b71) -val Pink40 = Color(0xFF7D5260) \ No newline at end of file diff --git a/android/SaveToOmnivore/app/src/main/java/app/omnivore/savetoomnivore/ui/theme/Theme.kt b/android/SaveToOmnivore/app/src/main/java/app/omnivore/savetoomnivore/ui/theme/Theme.kt deleted file mode 100644 index 6dc8511dd..000000000 --- a/android/SaveToOmnivore/app/src/main/java/app/omnivore/savetoomnivore/ui/theme/Theme.kt +++ /dev/null @@ -1,68 +0,0 @@ -package app.omnivore.savetoomnivore.ui.theme - -import android.app.Activity -import android.os.Build -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.dynamicDarkColorScheme -import androidx.compose.material3.dynamicLightColorScheme -import androidx.compose.material3.lightColorScheme -import androidx.compose.runtime.Composable -import androidx.compose.runtime.SideEffect -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalView -import androidx.core.view.ViewCompat - -private val DarkColorScheme = darkColorScheme( - primary = Purple80, - secondary = PurpleGrey80, - tertiary = Pink80 -) - -private val LightColorScheme = lightColorScheme( - primary = Purple40, - secondary = PurpleGrey40, - tertiary = Pink40 - - /* Other default colors to override - background = Color(0xFFFFFBFE), - surface = Color(0xFFFFFBFE), - onPrimary = Color.White, - onSecondary = Color.White, - onTertiary = Color.White, - onBackground = Color(0xFF1C1B1F), - onSurface = Color(0xFF1C1B1F), - */ -) - -@Composable -fun SaveToOmnivoreTheme( - darkTheme: Boolean = isSystemInDarkTheme(), - // Dynamic color is available on Android 12+ - dynamicColor: Boolean = true, - content: @Composable () -> Unit -) { - val colorScheme = when { - dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { - val context = LocalContext.current - if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) - } - darkTheme -> DarkColorScheme - else -> LightColorScheme - } - val view = LocalView.current - if (!view.isInEditMode) { - SideEffect { - (view.context as Activity).window.statusBarColor = colorScheme.primary.toArgb() - ViewCompat.getWindowInsetsController(view)?.isAppearanceLightStatusBars = darkTheme - } - } - - MaterialTheme( - colorScheme = colorScheme, - typography = Typography, - content = content - ) -} \ No newline at end of file diff --git a/android/SaveToOmnivore/app/src/main/java/app/omnivore/savetoomnivore/ui/theme/Type.kt b/android/SaveToOmnivore/app/src/main/java/app/omnivore/savetoomnivore/ui/theme/Type.kt deleted file mode 100644 index be24a8304..000000000 --- a/android/SaveToOmnivore/app/src/main/java/app/omnivore/savetoomnivore/ui/theme/Type.kt +++ /dev/null @@ -1,34 +0,0 @@ -package app.omnivore.savetoomnivore.ui.theme - -import androidx.compose.material3.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 - -// Set of Material typography styles to start with -val Typography = Typography( - bodyLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 16.sp, - lineHeight = 24.sp, - letterSpacing = 0.5.sp - ) - /* Other default text styles to override - titleLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 22.sp, - lineHeight = 28.sp, - letterSpacing = 0.sp - ), - labelSmall = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Medium, - fontSize = 11.sp, - lineHeight = 16.sp, - letterSpacing = 0.5.sp - ) - */ -) \ No newline at end of file diff --git a/android/SaveToOmnivore/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/android/SaveToOmnivore/app/src/main/res/drawable-v24/ic_launcher_foreground.xml deleted file mode 100644 index 2b068d114..000000000 --- a/android/SaveToOmnivore/app/src/main/res/drawable-v24/ic_launcher_foreground.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/android/SaveToOmnivore/app/src/main/res/drawable/ic_launcher_background.xml b/android/SaveToOmnivore/app/src/main/res/drawable/ic_launcher_background.xml deleted file mode 100644 index 07d5da9cb..000000000 --- a/android/SaveToOmnivore/app/src/main/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,170 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/android/SaveToOmnivore/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/android/SaveToOmnivore/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png deleted file mode 100644 index 84f3ca04b..000000000 Binary files a/android/SaveToOmnivore/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png and /dev/null differ diff --git a/android/SaveToOmnivore/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/SaveToOmnivore/app/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index 624d9e310..000000000 Binary files a/android/SaveToOmnivore/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/android/SaveToOmnivore/app/src/main/res/values/colors.xml b/android/SaveToOmnivore/app/src/main/res/values/colors.xml deleted file mode 100644 index f8c6127d3..000000000 --- a/android/SaveToOmnivore/app/src/main/res/values/colors.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - #FFBB86FC - #FF6200EE - #FF3700B3 - #FF03DAC5 - #FF018786 - #FF000000 - #FFFFFFFF - \ No newline at end of file diff --git a/android/SaveToOmnivore/app/src/main/res/values/strings.xml b/android/SaveToOmnivore/app/src/main/res/values/strings.xml deleted file mode 100644 index e537d2ef1..000000000 --- a/android/SaveToOmnivore/app/src/main/res/values/strings.xml +++ /dev/null @@ -1,3 +0,0 @@ - - Omnivore - \ No newline at end of file diff --git a/android/SaveToOmnivore/app/src/main/res/values/themes.xml b/android/SaveToOmnivore/app/src/main/res/values/themes.xml deleted file mode 100644 index 1a1372a95..000000000 --- a/android/SaveToOmnivore/app/src/main/res/values/themes.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - diff --git a/android/SaveToOmnivore/app/src/main/res/xml/backup_rules.xml b/android/SaveToOmnivore/app/src/main/res/xml/backup_rules.xml deleted file mode 100644 index fa0f996d2..000000000 --- a/android/SaveToOmnivore/app/src/main/res/xml/backup_rules.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - \ No newline at end of file diff --git a/android/SaveToOmnivore/app/src/main/res/xml/data_extraction_rules.xml b/android/SaveToOmnivore/app/src/main/res/xml/data_extraction_rules.xml deleted file mode 100644 index 9ee9997b0..000000000 --- a/android/SaveToOmnivore/app/src/main/res/xml/data_extraction_rules.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/android/SaveToOmnivore/app/src/test/java/app/omnivore/savetoomnivore/ExampleUnitTest.kt b/android/SaveToOmnivore/app/src/test/java/app/omnivore/savetoomnivore/ExampleUnitTest.kt deleted file mode 100644 index 332df63c4..000000000 --- a/android/SaveToOmnivore/app/src/test/java/app/omnivore/savetoomnivore/ExampleUnitTest.kt +++ /dev/null @@ -1,17 +0,0 @@ -package app.omnivore.savetoomnivore - -import org.junit.Test - -import org.junit.Assert.* - -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } -} \ No newline at end of file diff --git a/android/SaveToOmnivore/build.gradle b/android/SaveToOmnivore/build.gradle deleted file mode 100644 index caac89d71..000000000 --- a/android/SaveToOmnivore/build.gradle +++ /dev/null @@ -1,14 +0,0 @@ -buildscript { - ext { - compose_version = '1.1.0-beta01' - } -}// Top-level build file where you can add configuration options common to all sub-projects/modules. -plugins { - id 'com.android.application' version '7.2.1' apply false - id 'com.android.library' version '7.2.1' apply false - id 'org.jetbrains.kotlin.android' version '1.6.20' apply false -} - -task clean(type: Delete) { - delete rootProject.buildDir -} \ No newline at end of file diff --git a/android/SaveToOmnivore/gradle.properties b/android/SaveToOmnivore/gradle.properties deleted file mode 100644 index cd0519bb2..000000000 --- a/android/SaveToOmnivore/gradle.properties +++ /dev/null @@ -1,23 +0,0 @@ -# Project-wide Gradle settings. -# IDE (e.g. Android Studio) users: -# Gradle settings configured through the IDE *will override* -# any settings specified in this file. -# For more details on how to configure your build environment visit -# http://www.gradle.org/docs/current/userguide/build_environment.html -# Specifies the JVM arguments used for the daemon process. -# The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 -# When configured, Gradle will run in incubating parallel mode. -# This option should only be used with decoupled projects. More details, visit -# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects -# org.gradle.parallel=true -# AndroidX package structure to make it clearer which packages are bundled with the -# Android operating system, and which are packaged with your app"s APK -# https://developer.android.com/topic/libraries/support-library/androidx-rn -android.useAndroidX=true -# Kotlin code style for this project: "official" or "obsolete": -kotlin.code.style=official -# Enables namespacing of each library's R class so that its R class includes only the -# resources declared in the library itself and none from the library's dependencies, -# thereby reducing the size of the R class for that library -android.nonTransitiveRClass=true \ No newline at end of file diff --git a/android/SaveToOmnivore/gradle/wrapper/gradle-wrapper.jar b/android/SaveToOmnivore/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index e708b1c02..000000000 Binary files a/android/SaveToOmnivore/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/android/SaveToOmnivore/gradle/wrapper/gradle-wrapper.properties b/android/SaveToOmnivore/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 9e79af52e..000000000 --- a/android/SaveToOmnivore/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -#Tue Jul 19 16:31:46 PDT 2022 -distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip -distributionPath=wrapper/dists -zipStorePath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME diff --git a/android/SaveToOmnivore/gradlew b/android/SaveToOmnivore/gradlew deleted file mode 100755 index 4f906e0c8..000000000 --- a/android/SaveToOmnivore/gradlew +++ /dev/null @@ -1,185 +0,0 @@ -#!/usr/bin/env sh - -# -# Copyright 2015 the original author or authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# - -############################################################################## -## -## Gradle start up script for UN*X -## -############################################################################## - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn () { - echo "$*" -} - -die () { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=`expr $i + 1` - done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" - -exec "$JAVACMD" "$@" diff --git a/android/SaveToOmnivore/gradlew.bat b/android/SaveToOmnivore/gradlew.bat deleted file mode 100644 index ac1b06f93..000000000 --- a/android/SaveToOmnivore/gradlew.bat +++ /dev/null @@ -1,89 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/android/SaveToOmnivore/settings.gradle b/android/SaveToOmnivore/settings.gradle deleted file mode 100644 index 40d2999e2..000000000 --- a/android/SaveToOmnivore/settings.gradle +++ /dev/null @@ -1,16 +0,0 @@ -pluginManagement { - repositories { - gradlePluginPortal() - google() - mavenCentral() - } -} -dependencyResolutionManagement { - repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) - repositories { - google() - mavenCentral() - } -} -rootProject.name = "SaveToOmnivore" -include ':app' diff --git a/packages/api/.env.example b/packages/api/.env.example index 6e90843b6..78aa2387a 100644 --- a/packages/api/.env.example +++ b/packages/api/.env.example @@ -12,6 +12,7 @@ GATEWAY_URL=http://localhost:4000/api IMAGE_PROXY_URL=http://localhost:8080 IMAGE_PROXY_SECRET_KEY=some-secret GAUTH_IOS_CLIENT_ID= +GAUTH_ANDROID_CLIENT_ID= GAUTH_CLIENT_ID='notset' GAUTH_SECRET='notset' GCP_PROJECT_ID=omnivore-local diff --git a/packages/api/.env.test b/packages/api/.env.test index 16fd0aaf8..bfdce2355 100644 --- a/packages/api/.env.test +++ b/packages/api/.env.test @@ -12,6 +12,7 @@ GATEWAY_URL=http://localhost:4000/api IMAGE_PROXY_URL=http://localhost:8080 IMAGE_PROXY_SECRET_KEY=some-secret GAUTH_IOS_CLIENT_ID= +GAUTH_ANDROID_CLIENT_ID= GAUTH_CLIENT_ID='notset' GAUTH_SECRET='notset' GCP_PROJECT_ID=omnivore-local diff --git a/packages/api/src/routers/auth/google_auth.ts b/packages/api/src/routers/auth/google_auth.ts index e6c78eafb..c414dcf5c 100644 --- a/packages/api/src/routers/auth/google_auth.ts +++ b/packages/api/src/routers/auth/google_auth.ts @@ -57,16 +57,22 @@ export const validateGoogleUser = async ( } } -const googleMobileClient = new OAuth2Client(env.google.auth.iosClientId) -const googleWebClient = new OAuth2Client(env.google.auth.clientId) +const iosClientId = env.google.auth.iosClientId +const webClientId = env.google.auth.clientId + +const googleWebClient = new OAuth2Client(webClientId) +const googleIOSClient = new OAuth2Client(iosClientId) export async function decodeGoogleToken( - idToken: string + idToken: string, + isAndroid: boolean ): Promise { try { + const googleMobileClient = isAndroid ? googleWebClient : googleIOSClient + const loginTicket = await googleMobileClient.verifyIdToken({ idToken, - audience: env.google.auth.iosClientId, + audience: isAndroid ? webClientId : iosClientId, }) const email = loginTicket.getPayload()?.email diff --git a/packages/api/src/routers/auth/mobile/mobile_auth_router.ts b/packages/api/src/routers/auth/mobile/mobile_auth_router.ts index 138aae9c8..50890aaab 100644 --- a/packages/api/src/routers/auth/mobile/mobile_auth_router.ts +++ b/packages/api/src/routers/auth/mobile/mobile_auth_router.ts @@ -16,8 +16,9 @@ export function mobileAuthRouter() { const router = express.Router() router.post('/sign-in', async (req, res) => { - const { token, provider } = req.body - const payload = await createMobileSignInResponse(token, provider) + const { token, provider, source } = req.body + const isAndroid = source === 'ANDROID' + const payload = await createMobileSignInResponse(isAndroid, token, provider) res.status(payload.statusCode).json(payload.json) }) @@ -39,8 +40,14 @@ export function mobileAuthRouter() { }) router.post('/sign-up', async (req, res) => { - const { token, provider, name } = req.body - const payload = await createMobileSignUpResponse(token, provider, name) + const { token, provider, name, source } = req.body + const isAndroid = source === 'ANDROID' + const payload = await createMobileSignUpResponse( + isAndroid, + token, + provider, + name + ) res.status(payload.statusCode).json(payload.json) }) diff --git a/packages/api/src/routers/auth/mobile/sign_in.ts b/packages/api/src/routers/auth/mobile/sign_in.ts index 699c38aa0..8feb36d41 100644 --- a/packages/api/src/routers/auth/mobile/sign_in.ts +++ b/packages/api/src/routers/auth/mobile/sign_in.ts @@ -15,12 +15,13 @@ import { StatusType } from '../../../datalayer/user/model' import { comparePassword } from '../../../utils/auth' export async function createMobileSignInResponse( + isAndroid: boolean, token?: string, provider?: AuthProvider ): Promise { try { if (token && provider === 'GOOGLE') { - const decodedTokenResult = await decodeGoogleToken(token) + const decodedTokenResult = await decodeGoogleToken(token, isAndroid) return createAuthResponsePayload(provider, decodedTokenResult) } diff --git a/packages/api/src/routers/auth/mobile/sign_up.ts b/packages/api/src/routers/auth/mobile/sign_up.ts index ef36f481c..fea03ef97 100644 --- a/packages/api/src/routers/auth/mobile/sign_up.ts +++ b/packages/api/src/routers/auth/mobile/sign_up.ts @@ -13,13 +13,14 @@ import { hashPassword } from '../../../utils/auth' import { createUser } from '../../../services/create_user' export async function createMobileSignUpResponse( + isAndroid: boolean, token?: string, provider?: AuthProvider, name?: string ): Promise { try { if (token && provider === 'GOOGLE') { - const decodedTokenResult = await decodeGoogleToken(token) + const decodedTokenResult = await decodeGoogleToken(token, isAndroid) return createSignUpResponsePayload( provider, decodedTokenResult, diff --git a/packages/api/src/util.ts b/packages/api/src/util.ts index 1ca344d02..ea61b3ba2 100755 --- a/packages/api/src/util.ts +++ b/packages/api/src/util.ts @@ -30,6 +30,7 @@ interface BackendEnv { google: { auth: { iosClientId: string + androidClientId: string clientId: string secret: string } @@ -127,6 +128,7 @@ const nullableEnvVars = [ 'PREVIEW_GENERATION_SERVICE_URL', 'GCS_UPLOAD_SA_KEY_FILE_PATH', 'GAUTH_IOS_CLIENT_ID', + 'GAUTH_ANDROID_CLIENT_ID', 'GAUTH_CLIENT_ID', 'GAUTH_SECRET', 'SEGMENT_WRITE_KEY', @@ -203,6 +205,7 @@ export function getEnv(): BackendEnv { const google = { auth: { iosClientId: parse('GAUTH_IOS_CLIENT_ID'), + androidClientId: parse('GAUTH_ANDROID_CLIENT_ID'), clientId: parse('GAUTH_CLIENT_ID'), secret: parse('GAUTH_SECRET'), },