@@ -4,38 +4,56 @@ import android.net.Uri
44import androidx.compose.foundation.text.input.TextFieldState
55import androidx.core.text.trimmedLength
66import androidx.lifecycle.viewModelScope
7- import com.flipcash.app.core.AppRoute
87import com.flipcash.app.core.bill.Bill
98import com.flipcash.app.core.tokens.CurrencyCreatorStep
109import com.flipcash.app.currencycreator.internal.components.CurrencyCreatorTopBarController
1110import com.flipcash.app.userflags.UserFlagsCoordinator
1211import com.flipcash.libs.coroutines.DispatcherProvider
1312import com.getcode.opencode.model.financial.Fiat
14- import com.getcode.opencode.model.financial.Token
1513import com.getcode.opencode.model.financial.toFiat
1614import com.getcode.opencode.model.ui.TokenBillCustomizations
1715import com.flipcash.app.core.data.Loadable
18- import com.flipcash.app.core.data.isLoaded
19- import com.flipcash.app.core.tokens.SwapPurpose
16+ import com.flipcash.app.core.extensions.flatMapResult
17+ import com.flipcash.app.core.extensions.onResult
2018import com.flipcash.app.payments.PurchaseMethod
2119import com.flipcash.app.payments.PurchaseMethodController
20+ import com.flipcash.features.currencycreator.R
21+ import com.flipcash.services.controllers.ModerationController
22+ import com.flipcash.services.models.ImageModerationError
23+ import com.flipcash.services.models.ModerationResult
24+ import com.flipcash.services.models.TextModerationError
25+ import com.getcode.manager.BottomBarManager
26+ import com.getcode.opencode.controllers.CurrencyController
27+ import com.getcode.opencode.model.core.errors.CheckTokenAvailabilityError
2228import com.getcode.util.resources.ContentReader
29+ import com.getcode.util.resources.ResourceHelper
2330import com.getcode.view.BaseViewModel2
2431import com.getcode.view.LoadingSuccessState
2532import dagger.hilt.android.lifecycle.HiltViewModel
33+ import kotlinx.coroutines.delay
2634import kotlinx.coroutines.flow.distinctUntilChanged
2735import kotlinx.coroutines.flow.filterIsInstance
2836import kotlinx.coroutines.flow.flowOn
2937import kotlinx.coroutines.flow.launchIn
3038import kotlinx.coroutines.flow.map
3139import kotlinx.coroutines.flow.mapNotNull
3240import kotlinx.coroutines.flow.onEach
41+ import kotlinx.coroutines.launch
3342import javax.inject.Inject
3443
44+ internal data class ModerationAttestations (
45+ val name : ModerationResult .Attestation ? = null ,
46+ val icon : ModerationResult .Attestation ? = null ,
47+ val description : ModerationResult .Attestation ? = null ,
48+ )
49+
3550@HiltViewModel
3651internal class CurrencyCreatorViewModel @Inject constructor(
3752 dispatchers : DispatcherProvider ,
3853 userFlags : UserFlagsCoordinator ,
54+ moderationController : ModerationController ,
55+ currencyController : CurrencyController ,
56+ resources : ResourceHelper ,
3957 val contentReader : ContentReader ,
4058 val purchaseMethodController : PurchaseMethodController ,
4159) : BaseViewModel2<CurrencyCreatorViewModel.State, CurrencyCreatorViewModel.Event>(
@@ -55,6 +73,7 @@ internal class CurrencyCreatorViewModel @Inject constructor(
5573 val bill : Bill ? = null ,
5674 val purchaseAmount : Fiat = 20 .toFiat(),
5775 val processingState : LoadingSuccessState = LoadingSuccessState (),
76+ val attestations : ModerationAttestations = ModerationAttestations (),
5877 ) {
5978 val hasName: Boolean
6079 get() = nameFieldState.text.isNotBlank()
@@ -77,6 +96,13 @@ internal class CurrencyCreatorViewModel @Inject constructor(
7796 return (index + 1 ).toFloat() / PROGRESS_STEPS .size
7897 }
7998
99+ val hasAllAttestations: Boolean
100+ get() {
101+ return attestations.name != null
102+ && attestations.icon != null
103+ && attestations.description != null
104+ }
105+
80106 private companion object {
81107 private const val MAX_DESCRIPTION = 500
82108
@@ -93,6 +119,14 @@ internal class CurrencyCreatorViewModel @Inject constructor(
93119 internal sealed interface Event {
94120 data class OnStepChanged (val step : CurrencyCreatorStep ) : Event
95121
122+ data object CheckName : Event
123+ data object CheckDescription : Event
124+ data object CheckImage : Event
125+
126+ data class OnNameApproved (val attestation : ModerationResult .Attestation ): Event
127+ data class OnDescriptionApproved (val attestation : ModerationResult .Attestation ): Event
128+ data class OnImageApproved (val attestation : ModerationResult .Attestation ): Event
129+
96130 data class OnIconSelected (val image : Uri ) : Event
97131 data class OnIconCached (val image : Uri ) : Event
98132
@@ -116,12 +150,160 @@ internal class CurrencyCreatorViewModel @Inject constructor(
116150 eventFlow
117151 .filterIsInstance<Event .OnIconSelected >()
118152 .mapNotNull { event ->
119- contentReader.copyToCache(event.image, " currency_icon_${System .nanoTime()} " )
153+ contentReader.copyToCache(
154+ uri = event.image,
155+ fileName = " currency_icon_${System .nanoTime()} " ,
156+ maxSize = 500
157+ )
120158 }
121159 .flowOn(dispatchers.IO )
122160 .onEach { cached -> dispatchEvent(Event .OnIconCached (cached)) }
123161 .launchIn(viewModelScope)
124162
163+ eventFlow
164+ .filterIsInstance<Event .CheckName >()
165+ .map { stateFlow.value.nameFieldState.text.toString() }
166+ .onEach { dispatchEvent(Event .UpdateProcessingState (loading = true )) }
167+ .map { moderationController.moderateText(it) }
168+ .flatMapResult { result ->
169+ when (result.flaggedCategory) {
170+ ModerationResult .FlaggedCategory .NONE -> {
171+ currencyController.checkTokenAvailability(result.text)
172+ .map { result.attestation }
173+ }
174+ else -> Result .failure(TextModerationError .Flagged (result.flaggedCategory))
175+ }
176+ }
177+ .onResult(
178+ onSuccess = { attestation ->
179+ viewModelScope.launch {
180+ dispatchEvent(Event .UpdateProcessingState (success = true ))
181+ delay(500 )
182+ dispatchEvent(Event .OnNameApproved (attestation))
183+ dispatchEvent(Event .UpdateProcessingState ())
184+ }
185+ },
186+ onError = { cause ->
187+ dispatchEvent(Event .UpdateProcessingState ())
188+ when (cause) {
189+ is TextModerationError .Flagged ,
190+ is TextModerationError .Denied -> {
191+ BottomBarManager .showAlert(
192+ title = resources.getString(R .string.error_title_nameNotAllowed),
193+ message = resources.getString(R .string.error_description_nameNotAllowed)
194+ )
195+ }
196+ is CheckTokenAvailabilityError .Unavailable -> {
197+ BottomBarManager .showAlert(
198+ title = resources.getString(R .string.error_title_nameAlreadyTaken),
199+ message = resources.getString(R .string.error_description_nameAlreadyTaken)
200+ )
201+ }
202+ else -> {
203+ BottomBarManager .showError(
204+ title = resources.getString(R .string.error_title_nameCheckFailed),
205+ message = resources.getString(R .string.error_description_nameCheckFailed),
206+ )
207+ }
208+ }
209+ }
210+ )
211+ .launchIn(viewModelScope)
212+
213+ eventFlow
214+ .filterIsInstance<Event .OnIconCached >()
215+ .onEach { dispatchEvent(Event .CheckImage ) }
216+ .launchIn(viewModelScope)
217+
218+ eventFlow
219+ .filterIsInstance<Event .CheckImage >()
220+ .mapNotNull { stateFlow.value.icon.dataOrNull }
221+ .onEach { dispatchEvent(Event .UpdateProcessingState (loading = true )) }
222+ .map { moderationController.moderateImage(it) }
223+ .flatMapResult { result ->
224+ when (result.flaggedCategory) {
225+ ModerationResult .FlaggedCategory .NONE -> Result .success(result.attestation)
226+ else -> Result .failure(ImageModerationError .Flagged (result.flaggedCategory))
227+ }
228+ }
229+ .onResult(
230+ onSuccess = { attestation ->
231+ viewModelScope.launch {
232+ dispatchEvent(Event .UpdateProcessingState (success = true ))
233+ delay(500 )
234+ dispatchEvent(Event .OnImageApproved (attestation))
235+ dispatchEvent(Event .UpdateProcessingState ())
236+ }
237+ },
238+ onError = { cause ->
239+ dispatchEvent(Event .UpdateProcessingState ())
240+ stateFlow.value.icon.dataOrNull?.let { contentReader.removeFromCache(it) }
241+ when (cause) {
242+ is ImageModerationError .Flagged ,
243+ is ImageModerationError .Denied -> {
244+ BottomBarManager .showAlert(
245+ title = resources.getString(R .string.error_title_imageNotAllowed),
246+ message = resources.getString(R .string.error_description_nameNotAllowed)
247+ )
248+ }
249+ is ImageModerationError .UnsupportedFormat -> {
250+ BottomBarManager .showAlert(
251+ title = resources.getString(R .string.error_title_imageNotSupported),
252+ message = resources.getString(R .string.error_description_imageNotSupported)
253+ )
254+ }
255+ else -> {
256+ BottomBarManager .showError(
257+ title = resources.getString(R .string.error_title_moderationFailed),
258+ message = resources.getString(R .string.error_description_moderationFailed),
259+ )
260+ }
261+ }
262+ }
263+ )
264+ .launchIn(viewModelScope)
265+
266+ eventFlow
267+ .filterIsInstance<Event .CheckDescription >()
268+ .map { stateFlow.value.descriptionFieldState.text.toString() }
269+ .onEach { dispatchEvent(Event .UpdateProcessingState (loading = true )) }
270+ .map { moderationController.moderateText(it) }
271+ .flatMapResult { result ->
272+ when (result.flaggedCategory) {
273+ ModerationResult .FlaggedCategory .NONE -> Result .success(result.attestation)
274+ else -> Result .failure(TextModerationError .Flagged (result.flaggedCategory))
275+ }
276+ }
277+ .onResult(
278+ onSuccess = { attestation ->
279+ viewModelScope.launch {
280+ dispatchEvent(Event .UpdateProcessingState (success = true ))
281+ delay(500 )
282+ dispatchEvent(Event .OnDescriptionApproved (attestation))
283+ dispatchEvent(Event .UpdateProcessingState ())
284+ }
285+ },
286+ onError = { cause ->
287+ dispatchEvent(Event .UpdateProcessingState ())
288+ when (cause) {
289+ is TextModerationError .Flagged ,
290+ is TextModerationError .Denied -> {
291+ BottomBarManager .showAlert(
292+ title = resources.getString(R .string.error_title_descriptionNotAllowed),
293+ message = resources.getString(R .string.error_description_descriptionNotAllowed)
294+ )
295+ }
296+ else -> {
297+ BottomBarManager .showError(
298+ title = resources.getString(R .string.error_title_moderationFailed),
299+ message = resources.getString(R .string.error_description_moderationFailed),
300+ )
301+ }
302+ }
303+ }
304+ )
305+ .launchIn(viewModelScope)
306+
125307 eventFlow
126308 .filterIsInstance<Event .Purchase >()
127309 .onEach { purchaseMethodController.present() }
@@ -195,6 +377,21 @@ internal class CurrencyCreatorViewModel @Inject constructor(
195377 }
196378
197379 is Event .Purchase -> { state -> state }
380+ is Event .CheckName -> { state -> state }
381+ is Event .CheckDescription -> { state -> state }
382+ is Event .CheckImage -> { state -> state }
383+ is Event .OnNameApproved -> { state ->
384+ val attestations = state.attestations
385+ state.copy(attestations = attestations.copy(name = event.attestation))
386+ }
387+ is Event .OnDescriptionApproved -> { state ->
388+ val attestations = state.attestations
389+ state.copy(attestations = attestations.copy(description = event.attestation))
390+ }
391+ is Event .OnImageApproved -> { state ->
392+ val attestations = state.attestations
393+ state.copy(attestations = attestations.copy(icon = event.attestation))
394+ }
198395 }
199396 }
200397 }
0 commit comments