diff --git a/app/src/main/java/com/ivy/wallet/network/RestClient.kt b/app/src/main/java/com/ivy/wallet/network/RestClient.kt index 17585bab4..d99ca5167 100644 --- a/app/src/main/java/com/ivy/wallet/network/RestClient.kt +++ b/app/src/main/java/com/ivy/wallet/network/RestClient.kt @@ -187,4 +187,5 @@ class RestClient private constructor( val bankIntegrationsService: BankIntegrationsService by lazy { retrofit.create(BankIntegrationsService::class.java) } + val githubService: GithubService by lazy { retrofit.create(GithubService::class.java) } } \ No newline at end of file diff --git a/app/src/main/java/com/ivy/wallet/network/request/github/OpenIssueRequest.kt b/app/src/main/java/com/ivy/wallet/network/request/github/OpenIssueRequest.kt new file mode 100644 index 000000000..21bdb36ff --- /dev/null +++ b/app/src/main/java/com/ivy/wallet/network/request/github/OpenIssueRequest.kt @@ -0,0 +1,7 @@ +package com.ivy.wallet.network.request.github + +data class OpenIssueRequest( + val title: String, + val body: String, + val labels: List +) \ No newline at end of file diff --git a/app/src/main/java/com/ivy/wallet/network/request/github/OpenIssueResponse.kt b/app/src/main/java/com/ivy/wallet/network/request/github/OpenIssueResponse.kt new file mode 100644 index 000000000..072a3d1b5 --- /dev/null +++ b/app/src/main/java/com/ivy/wallet/network/request/github/OpenIssueResponse.kt @@ -0,0 +1,5 @@ +package com.ivy.wallet.network.request.github + +data class OpenIssueResponse( + val url: String +) \ No newline at end of file diff --git a/app/src/main/java/com/ivy/wallet/network/service/GithubService.kt b/app/src/main/java/com/ivy/wallet/network/service/GithubService.kt new file mode 100644 index 000000000..8ad332c64 --- /dev/null +++ b/app/src/main/java/com/ivy/wallet/network/service/GithubService.kt @@ -0,0 +1,20 @@ +package com.ivy.wallet.network.service + +import com.ivy.wallet.network.request.github.OpenIssueRequest +import com.ivy.wallet.network.request.github.OpenIssueResponse +import retrofit2.http.Body +import retrofit2.http.POST + +interface GithubService { + companion object { + private const val BASE_URL = "https://api.github.com" + const val OPEN_ISSUE_URL = "$BASE_URL/repos/ILIYANGERMANOV/ivy-wallet/issues" + + const val LABEL_USER_REQUEST = "user request" + } + + @POST(OPEN_ISSUE_URL) + suspend fun openIssue( + @Body request: OpenIssueRequest + ): OpenIssueResponse +} \ No newline at end of file diff --git a/app/src/main/java/com/ivy/wallet/ui/IvyActivity.kt b/app/src/main/java/com/ivy/wallet/ui/IvyActivity.kt index 3de825677..8ad392c86 100644 --- a/app/src/main/java/com/ivy/wallet/ui/IvyActivity.kt +++ b/app/src/main/java/com/ivy/wallet/ui/IvyActivity.kt @@ -484,13 +484,20 @@ class IvyActivity : AppCompatActivity() { startActivity(intent) } - fun openCalculatorApp() { - //TODO: It doesn't work better implement our own calculator -// val intent = Intent().apply { -// action = Intent.ACTION_MAIN -// addCategory(Intent.CATEGORY_APP_CALCULATOR) -// } -// startActivity(intent) + private fun openUrlInDefaultBrowser(url: String) { + try { + val browserIntent = Intent(Intent.ACTION_VIEW) + browserIntent.data = Uri.parse(url) + startActivity(browserIntent) + } catch (e: Exception) { + e.printStackTrace() + e.sendToCrashlytics("Cannot open URL in browser, intent not supported.") + Toast.makeText( + this, + "No browser app found. Visit manually: $url", + Toast.LENGTH_LONG + ).show() + } } fun reviewIvyWallet(dismissReviewCard: Boolean) { diff --git a/app/src/main/java/com/ivy/wallet/ui/settings/SettingsScreen.kt b/app/src/main/java/com/ivy/wallet/ui/settings/SettingsScreen.kt index 186160f30..665b18219 100644 --- a/app/src/main/java/com/ivy/wallet/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/com/ivy/wallet/ui/settings/SettingsScreen.kt @@ -42,6 +42,7 @@ import com.ivy.wallet.ui.theme.components.IvyToolbar import com.ivy.wallet.ui.theme.modal.ChooseStartDateOfMonthModal import com.ivy.wallet.ui.theme.modal.CurrencyModal import com.ivy.wallet.ui.theme.modal.NameModal +import com.ivy.wallet.ui.theme.modal.RequestFeatureModal import java.util.* @ExperimentalFoundationApi @@ -61,6 +62,7 @@ fun BoxWithConstraintsScope.SettingsScreen(screen: Screen.Settings) { viewModel.start() } + val ivyActivity = LocalContext.current as IvyActivity val context = LocalContext.current UI( user = user, @@ -82,7 +84,14 @@ fun BoxWithConstraintsScope.SettingsScreen(screen: Screen.Settings) { viewModel.exportToCSV(context) }, onSetLockApp = viewModel::setLockApp, - onSetStartDateOfMonth = viewModel::setStartDateOfMonth + onSetStartDateOfMonth = viewModel::setStartDateOfMonth, + onRequestFeature = { title, body -> + viewModel.requestFeature( + ivyActivity = ivyActivity, + title = title, + body = body + ) + } ) } @@ -107,11 +116,13 @@ private fun BoxWithConstraintsScope.UI( onLogin: () -> Unit, onExportToCSV: () -> Unit = {}, onSetLockApp: (Boolean) -> Unit = {}, - onSetStartDateOfMonth: (Int) -> Unit = {} + onSetStartDateOfMonth: (Int) -> Unit = {}, + onRequestFeature: (String, String) -> Unit = { _, _ -> } ) { var currencyModalVisible by remember { mutableStateOf(false) } var nameModalVisible by remember { mutableStateOf(false) } var chooseStartDateOfMonthVisible by remember { mutableStateOf(false) } + var requestFeatureModalVisible by remember { mutableStateOf(false) } LazyColumn( modifier = Modifier @@ -246,6 +257,12 @@ private fun BoxWithConstraintsScope.UI( Spacer(Modifier.height(12.dp)) + RequestFeature { + requestFeatureModalVisible = true + } + + Spacer(Modifier.height(12.dp)) + ContactSupport() Spacer(Modifier.height(12.dp)) @@ -282,6 +299,14 @@ private fun BoxWithConstraintsScope.UI( ) { onSetStartDateOfMonth(it) } + + RequestFeatureModal( + visible = requestFeatureModalVisible, + dismiss = { + requestFeatureModalVisible = false + }, + onSubmit = onRequestFeature + ) } @Composable @@ -327,6 +352,20 @@ private fun StartDateOfMonth( } } +@Composable +private fun RequestFeature( + onClick: () -> Unit +) { + SettingsPrimaryButton( + icon = R.drawable.ic_custom_rocket_m, + text = "Request a feature/improvement", + backgroundGradient = Gradient.solid(IvyTheme.colors.medium), + textColor = IvyTheme.colors.pureInverse + ) { + onClick() + } +} + @Composable private fun ContactSupport() { val ivyContext = LocalIvyContext.current diff --git a/app/src/main/java/com/ivy/wallet/ui/settings/SettingsViewModel.kt b/app/src/main/java/com/ivy/wallet/ui/settings/SettingsViewModel.kt index e4b6f15b0..83e91417a 100644 --- a/app/src/main/java/com/ivy/wallet/ui/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/ivy/wallet/ui/settings/SettingsViewModel.kt @@ -14,6 +14,8 @@ import com.ivy.wallet.model.entity.User import com.ivy.wallet.network.FCMClient import com.ivy.wallet.network.RestClient import com.ivy.wallet.network.request.auth.GoogleSignInRequest +import com.ivy.wallet.network.request.github.OpenIssueRequest +import com.ivy.wallet.network.service.GithubService import com.ivy.wallet.persistence.SharedPrefs import com.ivy.wallet.persistence.dao.SettingsDao import com.ivy.wallet.persistence.dao.UserDao @@ -203,4 +205,28 @@ class SettingsViewModel @Inject constructor( _lockApp.value = lockApp } } + + fun requestFeature( + ivyActivity: IvyActivity, + title: String, + body: String + ) { + viewModelScope.launch { + try { + val response = restClient.githubService.openIssue( + OpenIssueRequest( + title = title, + body = body, + labels = listOf( + GithubService.LABEL_USER_REQUEST + ) + ) + ) + + ivyActivity.openUrlInBrowser(response.url) + } catch (e: Exception) { + e.printStackTrace() + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/ivy/wallet/ui/theme/modal/BudgetModal.kt b/app/src/main/java/com/ivy/wallet/ui/theme/modal/BudgetModal.kt index edbad3f63..6d0ea005a 100644 --- a/app/src/main/java/com/ivy/wallet/ui/theme/modal/BudgetModal.kt +++ b/app/src/main/java/com/ivy/wallet/ui/theme/modal/BudgetModal.kt @@ -133,8 +133,8 @@ fun BoxWithConstraintsScope.BudgetModal( ModalNameInput( hint = "Budget name", autoFocusKeyboard = modal?.autoFocusKeyboard ?: true, - nameTextFieldValue = nameTextFieldValue, - setNameTextFieldValue = { + textFieldValue = nameTextFieldValue, + setTextFieldValue = { nameTextFieldValue = it } ) @@ -190,8 +190,8 @@ fun ModalNameInput( hint: String, autoFocusKeyboard: Boolean, - nameTextFieldValue: TextFieldValue, - setNameTextFieldValue: (TextFieldValue) -> Unit, + textFieldValue: TextFieldValue, + setTextFieldValue: (TextFieldValue) -> Unit, ) { val nameFocus = FocusRequester() @@ -207,7 +207,7 @@ fun ModalNameInput( .padding(start = 32.dp, end = 36.dp) .focusRequester(nameFocus), underlineModifier = Modifier.padding(start = 32.dp, end = 32.dp), - value = nameTextFieldValue, + value = textFieldValue, hint = hint, keyboardOptions = KeyboardOptions( capitalization = KeyboardCapitalization.Words, @@ -221,7 +221,7 @@ fun ModalNameInput( } ), ) { newValue -> - setNameTextFieldValue(newValue) + setTextFieldValue(newValue) } } diff --git a/app/src/main/java/com/ivy/wallet/ui/theme/modal/RequestFeatureModal.kt b/app/src/main/java/com/ivy/wallet/ui/theme/modal/RequestFeatureModal.kt new file mode 100644 index 000000000..9747d3135 --- /dev/null +++ b/app/src/main/java/com/ivy/wallet/ui/theme/modal/RequestFeatureModal.kt @@ -0,0 +1,110 @@ +package com.ivy.wallet.ui.theme.modal + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.ivy.wallet.base.selectEndTextFieldValue +import com.ivy.wallet.ui.IvyAppPreview +import com.ivy.wallet.ui.theme.Gray +import com.ivy.wallet.ui.theme.components.IvyDescriptionTextField +import java.util.* + +@Composable +fun BoxWithConstraintsScope.RequestFeatureModal( + id: UUID = UUID.randomUUID(), + visible: Boolean, + + dismiss: () -> Unit, + onSubmit: (title: String, body: String) -> Unit +) { + var title by remember(id) { + mutableStateOf(selectEndTextFieldValue("")) + } + var body by remember(id) { + mutableStateOf(selectEndTextFieldValue("")) + } + + + IvyModal( + id = id, + visible = visible, + dismiss = dismiss, + PrimaryAction = { + ModalSet( + label = "Submit", + enabled = title.text.isNotBlank() + ) { + onSubmit( + title.text, + body.text + ) + } + } + ) { + Spacer(Modifier.height(32.dp)) + + ModalTitle(text = "Request a feature") + + Spacer(Modifier.height(24.dp)) + + ModalNameInput( + hint = "What do you want?", + autoFocusKeyboard = true, + textFieldValue = title, + setTextFieldValue = { + title = it + } + ) + + Spacer(Modifier.height(16.dp)) + + IvyDescriptionTextField( + modifier = Modifier + .padding(horizontal = 32.dp) + .fillMaxWidth(), + keyboardOptions = KeyboardOptions( + autoCorrect = true, + capitalization = KeyboardCapitalization.Sentences, + keyboardType = KeyboardType.Text, + imeAction = ImeAction.Default + ), + keyboardActions = KeyboardActions( + onAny = { + body = body.copy( + text = StringBuilder(body.text) + .insert(body.selection.end, "\n") + .toString(), + selection = TextRange(body.selection.end + 1) + ) + } + ), + hint = "Explain it with one sentence. (supports markdown)", + hintColor = Gray, + value = body, + ) { + body = it + } + + Spacer(Modifier.height(24.dp)) + } +} + +@Preview +@Composable +private fun Preview() { + IvyAppPreview { + RequestFeatureModal( + visible = true, + dismiss = {}, + onSubmit = { _, _ -> } + ) + } +} \ No newline at end of file