diff --git a/settings.gradle b/settings.gradle index 03ffe346a5..9032bc088f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -50,6 +50,7 @@ include ':sphinx:application:data:concepts:repositories:concept-repository-dashb include ':sphinx:application:data:concepts:repositories:concept-repository-lightning' include ':sphinx:application:data:concepts:repositories:concept-repository-media' include ':sphinx:application:data:concepts:repositories:concept-repository-message' +include ':sphinx:application:data:concepts:repositories:concept-repository-subscription' include ':sphinx:application:data:features:feature-repository' include ':sphinx:application:data:features:feature-repository-android' @@ -137,6 +138,7 @@ include ':sphinx:screens-detail:podcast-player:podcast-player' include ':sphinx:screens-detail:qr-code:qr-code' include ':sphinx:screens-detail:scanner:scanner' include ':sphinx:screens-detail:scanner:scanner-view-model-coordinator' +include ':sphinx:screens-detail:subscription:subscription' include ':sphinx:screens-detail:support-ticket:support-ticket' include ':sphinx:screens-detail:transactions:transactions' include ':sphinx:screens-detail:tribe-members-list:tribe-members-list' diff --git a/sphinx/activity/main/activitymain/build.gradle b/sphinx/activity/main/activitymain/build.gradle index e2e36d29d5..589c7e8dc4 100644 --- a/sphinx/activity/main/activitymain/build.gradle +++ b/sphinx/activity/main/activitymain/build.gradle @@ -68,6 +68,7 @@ dependencies { api project(path: ':sphinx:screens-detail:podcast-player:podcast-player') api project(path: ':sphinx:screens-detail:qr-code:qr-code') api project(path: ':sphinx:screens-detail:scanner:scanner') + api project(path: ':sphinx:screens-detail:subscription:subscription') api project(path: ':sphinx:screens-detail:support-ticket:support-ticket') api project(path: ':sphinx:screens-detail:transactions:transactions') api project(path: ':sphinx:screens-detail:tribe-members-list:tribe-members-list') diff --git a/sphinx/activity/main/activitymain/src/main/java/chat/sphinx/activitymain/di/NavigationModule.kt b/sphinx/activity/main/activitymain/src/main/java/chat/sphinx/activitymain/di/NavigationModule.kt index b1f541c229..c1c7dbd64b 100644 --- a/sphinx/activity/main/activitymain/src/main/java/chat/sphinx/activitymain/di/NavigationModule.kt +++ b/sphinx/activity/main/activitymain/src/main/java/chat/sphinx/activitymain/di/NavigationModule.kt @@ -34,6 +34,7 @@ import chat.sphinx.profile.navigation.ProfileNavigator import chat.sphinx.qr_code.navigation.QRCodeNavigator import chat.sphinx.scanner.navigation.ScannerNavigator import chat.sphinx.splash.navigation.SplashNavigator +import chat.sphinx.subscription.navigation.SubscriptionNavigator import chat.sphinx.support_ticket.navigation.SupportTicketNavigator import chat.sphinx.transactions.navigation.TransactionsNavigator import chat.sphinx.tribe_detail.navigation.TribeDetailNavigator @@ -226,6 +227,12 @@ internal object NavigationModule { ): ProfileNavigator = profileNavigatorImpl + @Provides + fun provideSubscriptionNavigator( + subscriptionNavigatorImpl: SubscriptionNavigatorImpl + ): SubscriptionNavigator = + subscriptionNavigatorImpl + @Provides fun provideSupportTicketNavigator( supportTicketNavigatorImpl: SupportTicketNavigatorImpl diff --git a/sphinx/activity/main/activitymain/src/main/java/chat/sphinx/activitymain/navigation/navigators/detail/EditContactNavigatorImpl.kt b/sphinx/activity/main/activitymain/src/main/java/chat/sphinx/activitymain/navigation/navigators/detail/EditContactNavigatorImpl.kt index 4a238dbc9d..d0e5d63a8a 100644 --- a/sphinx/activity/main/activitymain/src/main/java/chat/sphinx/activitymain/navigation/navigators/detail/EditContactNavigatorImpl.kt +++ b/sphinx/activity/main/activitymain/src/main/java/chat/sphinx/activitymain/navigation/navigators/detail/EditContactNavigatorImpl.kt @@ -3,6 +3,8 @@ package chat.sphinx.activitymain.navigation.navigators.detail import chat.sphinx.activitymain.navigation.drivers.DetailNavigationDriver import chat.sphinx.edit_contact.navigation.EditContactNavigator import chat.sphinx.qr_code.navigation.ToQRCodeDetail +import chat.sphinx.subscription.navigation.ToSubscriptionDetail +import chat.sphinx.wrapper_common.dashboard.ContactId import javax.inject.Inject internal class EditContactNavigatorImpl @Inject constructor( @@ -22,4 +24,10 @@ internal class EditContactNavigatorImpl @Inject constructor( ) ) } + + override suspend fun toSubscribeDetailScreen(contactId: ContactId) { + navigationDriver.submitNavigationRequest( + ToSubscriptionDetail(contactId) + ) + } } diff --git a/sphinx/activity/main/activitymain/src/main/java/chat/sphinx/activitymain/navigation/navigators/detail/SubscriptionNavigatorImpl.kt b/sphinx/activity/main/activitymain/src/main/java/chat/sphinx/activitymain/navigation/navigators/detail/SubscriptionNavigatorImpl.kt new file mode 100644 index 0000000000..c8ee70b5cc --- /dev/null +++ b/sphinx/activity/main/activitymain/src/main/java/chat/sphinx/activitymain/navigation/navigators/detail/SubscriptionNavigatorImpl.kt @@ -0,0 +1,14 @@ +package chat.sphinx.activitymain.navigation.navigators.detail + +import chat.sphinx.activitymain.navigation.drivers.DetailNavigationDriver +import chat.sphinx.subscription.navigation.SubscriptionNavigator +import javax.inject.Inject + +internal class SubscriptionNavigatorImpl @Inject constructor( + detailDriver: DetailNavigationDriver, +): SubscriptionNavigator(detailDriver) { + + override suspend fun closeDetailScreen() { + (navigationDriver as DetailNavigationDriver).closeDetailScreen() + } +} diff --git a/sphinx/activity/main/activitymain/src/main/res/navigation/main_detail_nav_graph.xml b/sphinx/activity/main/activitymain/src/main/res/navigation/main_detail_nav_graph.xml index 535c1ee228..e7f2698cff 100644 --- a/sphinx/activity/main/activitymain/src/main/res/navigation/main_detail_nav_graph.xml +++ b/sphinx/activity/main/activitymain/src/main/res/navigation/main_detail_nav_graph.xml @@ -16,6 +16,7 @@ + diff --git a/sphinx/application/common/resources/src/main/res/drawable/custom_radio_button.xml b/sphinx/application/common/resources/src/main/res/drawable/custom_radio_button.xml new file mode 100644 index 0000000000..80ee2f3251 --- /dev/null +++ b/sphinx/application/common/resources/src/main/res/drawable/custom_radio_button.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sphinx/application/common/resources/src/main/res/drawable/ic_check_box_off.xml b/sphinx/application/common/resources/src/main/res/drawable/ic_check_box_off.xml new file mode 100644 index 0000000000..fc7725a72b --- /dev/null +++ b/sphinx/application/common/resources/src/main/res/drawable/ic_check_box_off.xml @@ -0,0 +1,9 @@ + + + diff --git a/sphinx/application/common/resources/src/main/res/drawable/ic_check_box_on.xml b/sphinx/application/common/resources/src/main/res/drawable/ic_check_box_on.xml new file mode 100644 index 0000000000..b7ec917b66 --- /dev/null +++ b/sphinx/application/common/resources/src/main/res/drawable/ic_check_box_on.xml @@ -0,0 +1,9 @@ + + + diff --git a/sphinx/application/common/wrappers/wrapper-common/src/main/java/chat/sphinx/wrapper_common/DateTime.kt b/sphinx/application/common/wrappers/wrapper-common/src/main/java/chat/sphinx/wrapper_common/DateTime.kt index 87f1e83951..68ecdda015 100644 --- a/sphinx/application/common/wrappers/wrapper-common/src/main/java/chat/sphinx/wrapper_common/DateTime.kt +++ b/sphinx/application/common/wrappers/wrapper-common/src/main/java/chat/sphinx/wrapper_common/DateTime.kt @@ -15,6 +15,11 @@ import java.util.* inline fun String.toDateTime(): DateTime = DateTime(DateTime.getFormatRelay().parse(this)) +@Suppress("NOTHING_TO_INLINE") +@Throws(ParseException::class) +inline fun String.toDateTimeWithFormat(format: SimpleDateFormat): DateTime = + DateTime(format.parse(this)) + @Suppress("NOTHING_TO_INLINE") inline fun Long.toDateTime(): DateTime = DateTime(Date(this)) @@ -95,6 +100,7 @@ value class DateTime(val value: Date) { private const val FORMAT_EEE_DD = "EEE dd" private const val FORMAT_EEE_MM_DD_H_MM_A = "EEE MMM dd, h:mm a" private const val FORMAT_DD_MMM_HH_MM = "dd MMM, HH:mm" + private const val FORMAT_MMM_DD_YYYY = "MMM dd, yyyy" private const val SIX_DAYS_IN_MILLISECONDS = 518_400_000L @@ -241,6 +247,21 @@ value class DateTime(val value: Date) { formatEEEdd = it } } + + @Volatile + private var formatMMMddyyyy: SimpleDateFormat? = null + fun getFormatMMMddyyyy(timeZone: TimeZone = TimeZone.getDefault()): SimpleDateFormat = + formatMMMddyyyy?.also { + it.timeZone = timeZone + } ?: synchronized(this) { + formatMMMddyyyy?.also { + it.timeZone = timeZone + } ?: SimpleDateFormat(FORMAT_MMM_DD_YYYY, Locale.getDefault()) + .also { + it.timeZone = timeZone + formatMMMddyyyy = it + } + } } override fun toString(): String { diff --git a/sphinx/application/common/wrappers/wrapper-common/src/main/java/chat/sphinx/wrapper_common/dashboard/ContactId.kt b/sphinx/application/common/wrappers/wrapper-common/src/main/java/chat/sphinx/wrapper_common/dashboard/ContactId.kt index 1e7f7e9d72..6516f1f63d 100644 --- a/sphinx/application/common/wrappers/wrapper-common/src/main/java/chat/sphinx/wrapper_common/dashboard/ContactId.kt +++ b/sphinx/application/common/wrappers/wrapper-common/src/main/java/chat/sphinx/wrapper_common/dashboard/ContactId.kt @@ -9,7 +9,9 @@ inline fun Long.toContactId(): ContactId? = } @JvmInline -value class ContactId(override val value: Long): DashboardItemId { +value class ContactId( + override val value: Long + ): DashboardItemId { companion object { const val NULL_CONTACT_ID = Long.MAX_VALUE } diff --git a/sphinx/application/common/wrappers/wrapper-common/src/main/java/chat/sphinx/wrapper_common/subscription/Cron.kt b/sphinx/application/common/wrappers/wrapper-common/src/main/java/chat/sphinx/wrapper_common/subscription/Cron.kt new file mode 100644 index 0000000000..cd2206c6f5 --- /dev/null +++ b/sphinx/application/common/wrappers/wrapper-common/src/main/java/chat/sphinx/wrapper_common/subscription/Cron.kt @@ -0,0 +1,10 @@ +package chat.sphinx.wrapper_common.subscription + +@JvmInline +value class Cron(val value: String) { + init { + require(value.isNotEmpty()) { + "Subscription Cron cannot be empty" + } + } +} \ No newline at end of file diff --git a/sphinx/application/common/wrappers/wrapper-common/src/main/java/chat/sphinx/wrapper_common/subscription/EndNumber.kt b/sphinx/application/common/wrappers/wrapper-common/src/main/java/chat/sphinx/wrapper_common/subscription/EndNumber.kt new file mode 100644 index 0000000000..723526699c --- /dev/null +++ b/sphinx/application/common/wrappers/wrapper-common/src/main/java/chat/sphinx/wrapper_common/subscription/EndNumber.kt @@ -0,0 +1,10 @@ +package chat.sphinx.wrapper_common.subscription + +@JvmInline +value class EndNumber(val value: Long) { + init { + require(value >= 0) { + "EndNumber must be greater than or equal to 0" + } + } +} diff --git a/sphinx/application/common/wrappers/wrapper-common/src/main/java/chat/sphinx/wrapper_common/subscription/SubscriptionCount.kt b/sphinx/application/common/wrappers/wrapper-common/src/main/java/chat/sphinx/wrapper_common/subscription/SubscriptionCount.kt new file mode 100644 index 0000000000..b54aaf6354 --- /dev/null +++ b/sphinx/application/common/wrappers/wrapper-common/src/main/java/chat/sphinx/wrapper_common/subscription/SubscriptionCount.kt @@ -0,0 +1,10 @@ +package chat.sphinx.wrapper_common.subscription + +@JvmInline +value class SubscriptionCount(val value: Long) { + init { + require(value >= 0) { + "SubscriptionCount must be greater than or equal to 0" + } + } +} \ No newline at end of file diff --git a/sphinx/application/common/wrappers/wrapper-subscription/src/main/java/chat/sphinx/wrapper_subscription/Subscription.kt b/sphinx/application/common/wrappers/wrapper-subscription/src/main/java/chat/sphinx/wrapper_subscription/Subscription.kt new file mode 100644 index 0000000000..a6b3950d8d --- /dev/null +++ b/sphinx/application/common/wrappers/wrapper-subscription/src/main/java/chat/sphinx/wrapper_subscription/Subscription.kt @@ -0,0 +1,25 @@ +package chat.sphinx.wrapper_subscription + +import chat.sphinx.wrapper_common.DateTime +import chat.sphinx.wrapper_common.dashboard.ChatId +import chat.sphinx.wrapper_common.dashboard.ContactId +import chat.sphinx.wrapper_common.lightning.Sat +import chat.sphinx.wrapper_common.subscription.Cron +import chat.sphinx.wrapper_common.subscription.EndNumber +import chat.sphinx.wrapper_common.subscription.SubscriptionCount +import chat.sphinx.wrapper_common.subscription.SubscriptionId + +data class Subscription( + val id: SubscriptionId, + val cron: Cron, + val amount: Sat, + val endNumber: EndNumber?, + val count: SubscriptionCount, + val endDate: DateTime?, + val ended: Boolean, + val paused: Boolean, + val createdAt: DateTime, + val updatedAt: DateTime, + val chatId: ChatId, + val contactId: ContactId +) \ No newline at end of file diff --git a/sphinx/application/data/concepts/concept-coredb/src/main/sqldelight/chat/sphinx/concept_coredb/SphinxDatabase.sq b/sphinx/application/data/concepts/concept-coredb/src/main/sqldelight/chat/sphinx/concept_coredb/SphinxDatabase.sq index af7ca59cab..dd1d44898b 100644 --- a/sphinx/application/data/concepts/concept-coredb/src/main/sqldelight/chat/sphinx/concept_coredb/SphinxDatabase.sq +++ b/sphinx/application/data/concepts/concept-coredb/src/main/sqldelight/chat/sphinx/concept_coredb/SphinxDatabase.sq @@ -25,6 +25,10 @@ import chat.sphinx.wrapper_common.lightning.LightningRouteHint; import chat.sphinx.wrapper_common.lightning.Sat; import chat.sphinx.wrapper_common.message.MessageId; import chat.sphinx.wrapper_common.message.MessageUUID; +import chat.sphinx.wrapper_common.subscription.Cron; +import chat.sphinx.wrapper_common.subscription.EndNumber; +import chat.sphinx.wrapper_common.subscription.SubscriptionCount; +import chat.sphinx.wrapper_common.subscription.SubscriptionId; import chat.sphinx.wrapper_contact.ContactAlias; import chat.sphinx.wrapper_contact.ContactStatus; import chat.sphinx.wrapper_contact.DeviceId; @@ -741,3 +745,102 @@ WHERE id = ?; messageMediaDeleteByChatId: DELETE FROM messageMediaDbo WHERE chat_id = ?; + +CREATE TABLE subscriptionDbo( + id INTEGER AS SubscriptionId NOT NULL PRIMARY KEY, + cron TEXT AS Cron NOT NULL, + amount INTEGER AS Sat NOT NULL, + end_number INTEGER AS EndNumber, + count INTEGER AS SubscriptionCount NOT NULL, + end_date INTEGER AS DateTime, + ended INTEGER AS Boolean DEFAULT 0 NOT NULL, + paused INTEGER AS Boolean DEFAULT 0 NOT NULL, + created_at INTEGER AS DateTime NOT NULL, + updated_at INTEGER AS DateTime NOT NULL, + chat_id INTEGER AS ChatId NOT NULL, + contact_id INTEGER AS ContactId NOT NULL +); + +subscriptionGetById: +SELECT * +FROM subscriptionDbo +WHERE id = ?; + +subscriptionGetLastActiveByContactId: +SELECT * +FROM subscriptionDbo +WHERE ended = 0 AND contact_id = ? +ORDER BY id DESC +LIMIT 1; + +subscriptionGetAllByChatId: +SELECT * +FROM subscriptionDbo +WHERE chat_id = ?; + +subscriptionGetAll: +SELECT * +FROM subscriptionDbo; + +subscriptionUpsert { + UPDATE subscriptionDbo + SET cron = :cron, + amount = :amount, + end_number = :end_number, + count = :count, + end_date = :end_date, + ended = :ended, + paused = :paused, + created_at = :created_at, + updated_at = :updated_at, + chat_id = :chat_id, + contact_id = :contact_id + WHERE id = :id; + + INSERT OR IGNORE INTO subscriptionDbo( + id, + cron, + amount, + end_number, + count, + end_date, + ended, + paused, + created_at, + updated_at, + chat_id, + contact_id + ) + VALUES ( + :id, + :cron, + :amount, + :end_number, + :count, + :end_date, + :ended, + :paused, + :created_at, + :updated_at, + :chat_id, + :contact_id + ); +} + +subscriptionDeleteById: +DELETE FROM subscriptionDbo +WHERE id = ?; + +subscriptionUpdatePaused: +UPDATE subscriptionDbo +SET paused = :paused +WHERE id = ?; + +subscriptionUpdateEnded: +UPDATE subscriptionDbo +SET ended = :ended +WHERE id = ?; + +subscriptionDeleteByContactId: +DELETE FROM subscriptionDbo +WHERE contact_id = ?; \ No newline at end of file diff --git a/sphinx/application/data/concepts/concept-coredb/src/main/sqldelight/migrations/2.sqm b/sphinx/application/data/concepts/concept-coredb/src/main/sqldelight/migrations/2.sqm index e831162ca7..a90eb3bd56 100644 --- a/sphinx/application/data/concepts/concept-coredb/src/main/sqldelight/migrations/2.sqm +++ b/sphinx/application/data/concepts/concept-coredb/src/main/sqldelight/migrations/2.sqm @@ -1,3 +1,26 @@ +import chat.sphinx.wrapper_common.DateTime; +import chat.sphinx.wrapper_common.dashboard.ChatId; +import chat.sphinx.wrapper_common.dashboard.ContactId; +import chat.sphinx.wrapper_common.lightning.Sat; +import chat.sphinx.wrapper_common.subscription.Cron; +import chat.sphinx.wrapper_common.subscription.EndNumber; +import chat.sphinx.wrapper_common.subscription.SubscriptionCount; +import chat.sphinx.wrapper_common.subscription.SubscriptionId; import chat.sphinx.wrapper_message.MessageMUID; -ALTER TABLE messageDbo ADD COLUMN muid TEXT AS MessageMUID; \ No newline at end of file +CREATE TABLE subscriptionDbo( + id INTEGER AS SubscriptionId NOT NULL PRIMARY KEY, + cron TEXT AS Cron NOT NULL, + amount INTEGER AS Sat NOT NULL, + end_number INTEGER AS EndNumber, + count INTEGER AS SubscriptionCount NOT NULL, + end_date INTEGER AS DateTime, + ended INTEGER AS Boolean DEFAULT 0 NOT NULL, + paused INTEGER AS Boolean DEFAULT 0 NOT NULL, + created_at INTEGER AS DateTime NOT NULL, + updated_at INTEGER AS DateTime NOT NULL, + chat_id INTEGER AS ChatId NOT NULL, + contact_id INTEGER AS ContactId NOT NULL +); + +ALTER TABLE messageDbo ADD COLUMN muid TEXT AS MessageMUID; diff --git a/sphinx/application/data/concepts/repositories/concept-repository-subscription/.gitignore b/sphinx/application/data/concepts/repositories/concept-repository-subscription/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/sphinx/application/data/concepts/repositories/concept-repository-subscription/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/sphinx/application/data/concepts/repositories/concept-repository-subscription/build.gradle b/sphinx/application/data/concepts/repositories/concept-repository-subscription/build.gradle new file mode 100644 index 0000000000..324e9fa260 --- /dev/null +++ b/sphinx/application/data/concepts/repositories/concept-repository-subscription/build.gradle @@ -0,0 +1,12 @@ +plugins { + id 'java-library' + id 'kotlin' +} + +dependencies { + api project(path: ':sphinx:application:common:kotlin-response') + api project(path: ':sphinx:application:common:wrappers:wrapper-subscription') + + implementation deps.kotlin.coroutinesCore + implementation project(path: ':kotlin:crypto:crypto-common') +} diff --git a/sphinx/application/data/concepts/repositories/concept-repository-subscription/src/main/java/chat/sphinx/concept_repository_subscription/SubscriptionRepository.kt b/sphinx/application/data/concepts/repositories/concept-repository-subscription/src/main/java/chat/sphinx/concept_repository_subscription/SubscriptionRepository.kt new file mode 100644 index 0000000000..6969f73035 --- /dev/null +++ b/sphinx/application/data/concepts/repositories/concept-repository-subscription/src/main/java/chat/sphinx/concept_repository_subscription/SubscriptionRepository.kt @@ -0,0 +1,49 @@ +package chat.sphinx.concept_repository_subscription + +import chat.sphinx.kotlin_response.Response +import chat.sphinx.kotlin_response.ResponseError +import chat.sphinx.wrapper_common.dashboard.ChatId +import chat.sphinx.wrapper_common.dashboard.ContactId +import chat.sphinx.wrapper_common.lightning.Sat +import chat.sphinx.wrapper_common.subscription.EndNumber +import chat.sphinx.wrapper_common.subscription.SubscriptionId +import chat.sphinx.wrapper_subscription.Subscription +import kotlinx.coroutines.flow.Flow + + +interface SubscriptionRepository { + fun getActiveSubscriptionByContactId( + contactId: ContactId + ): Flow + + suspend fun createSubscription( + amount: Sat, + interval: String, + contactId: ContactId, + chatId: ChatId?, + endDate: String?, + endNumber: EndNumber? + ): Response + + suspend fun updateSubscription( + id: SubscriptionId, + amount: Sat, + interval: String, + contactId: ContactId, + chatId: ChatId?, + endDate: String?, + endNumber: EndNumber? + ): Response + + suspend fun restartSubscription( + subscriptionId: SubscriptionId + ): Response + + suspend fun pauseSubscription( + subscriptionId: SubscriptionId + ): Response + + suspend fun deleteSubscription( + subscriptionId: SubscriptionId + ): Response +} diff --git a/sphinx/application/data/features/feature-coredb/src/main/java/chat/sphinx/feature_coredb/CoreDBImpl.kt b/sphinx/application/data/features/feature-coredb/src/main/java/chat/sphinx/feature_coredb/CoreDBImpl.kt index f1e42000c0..7177181ec7 100644 --- a/sphinx/application/data/features/feature-coredb/src/main/java/chat/sphinx/feature_coredb/CoreDBImpl.kt +++ b/sphinx/application/data/features/feature-coredb/src/main/java/chat/sphinx/feature_coredb/CoreDBImpl.kt @@ -6,19 +6,21 @@ import chat.sphinx.conceptcoredb.* import chat.sphinx.feature_coredb.adapters.chat.* import chat.sphinx.feature_coredb.adapters.common.* import chat.sphinx.feature_coredb.adapters.contact.* -import chat.sphinx.feature_coredb.adapters.contact.ContactAliasAdapter -import chat.sphinx.feature_coredb.adapters.contact.ContactOwnerAdapter -import chat.sphinx.feature_coredb.adapters.contact.LightningNodeAliasAdapter -import chat.sphinx.feature_coredb.adapters.contact.LightningRouteHintAdapter -import chat.sphinx.feature_coredb.adapters.contact.PrivatePhotoAdapter import chat.sphinx.feature_coredb.adapters.invite.InviteStringAdapter -import chat.sphinx.feature_coredb.adapters.media.* +import chat.sphinx.feature_coredb.adapters.media.MediaKeyAdapter +import chat.sphinx.feature_coredb.adapters.media.MediaKeyDecryptedAdapter +import chat.sphinx.feature_coredb.adapters.media.MediaTokenAdapter +import chat.sphinx.feature_coredb.adapters.media.MediaTypeAdapter import chat.sphinx.feature_coredb.adapters.message.* +import chat.sphinx.feature_coredb.adapters.subscription.CronAdapter +import chat.sphinx.feature_coredb.adapters.subscription.EndNumberAdapter +import chat.sphinx.feature_coredb.adapters.subscription.SubscriptionCountAdapter import com.squareup.moshi.Moshi import com.squareup.sqldelight.db.SqlDriver import io.matthewnelson.concept_encryption_key.EncryptionKey -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collect abstract class CoreDBImpl(private val moshi: Moshi): CoreDB() { @@ -145,6 +147,18 @@ abstract class CoreDBImpl(private val moshi: Moshi): CoreDB() { media_tokenAdapter = MediaTokenAdapter(), local_fileAdapter = FileAdapter.getInstance(), ), + subscriptionDboAdapter = SubscriptionDbo.Adapter( + idAdapter = SubscriptionIdAdapter.getInstance(), + cronAdapter = CronAdapter(), + amountAdapter = SatAdapter.getInstance(), + end_numberAdapter = EndNumberAdapter(), + countAdapter = SubscriptionCountAdapter(), + end_dateAdapter = DateTimeAdapter.getInstance(), + created_atAdapter = DateTimeAdapter.getInstance(), + updated_atAdapter = DateTimeAdapter.getInstance(), + chat_idAdapter = ChatIdAdapter.getInstance(), + contact_idAdapter = ContactIdAdapter.getInstance() + ) ).sphinxDatabaseQueries } } diff --git a/sphinx/application/data/features/feature-coredb/src/main/java/chat/sphinx/feature_coredb/adapters/common/CommonAdapters.kt b/sphinx/application/data/features/feature-coredb/src/main/java/chat/sphinx/feature_coredb/adapters/common/CommonAdapters.kt index 99cb556ccb..5817648ef5 100644 --- a/sphinx/application/data/features/feature-coredb/src/main/java/chat/sphinx/feature_coredb/adapters/common/CommonAdapters.kt +++ b/sphinx/application/data/features/feature-coredb/src/main/java/chat/sphinx/feature_coredb/adapters/common/CommonAdapters.kt @@ -9,6 +9,7 @@ import chat.sphinx.wrapper_common.lightning.LightningPaymentHash import chat.sphinx.wrapper_common.lightning.LightningPaymentRequest import chat.sphinx.wrapper_common.lightning.Sat import chat.sphinx.wrapper_common.message.MessageId +import chat.sphinx.wrapper_common.subscription.SubscriptionId import com.squareup.sqldelight.ColumnAdapter import java.io.File @@ -317,3 +318,24 @@ internal class SeenAdapter private constructor(): ColumnAdapter { return value.value.toLong() } } + +internal class SubscriptionIdAdapter private constructor(): ColumnAdapter { + + companion object { + @Volatile + private var instance: SubscriptionIdAdapter? = null + fun getInstance(): SubscriptionIdAdapter = + instance ?: synchronized(this) { + instance ?: SubscriptionIdAdapter() + .also { instance = it } + } + } + + override fun decode(databaseValue: Long): SubscriptionId { + return SubscriptionId(databaseValue) + } + + override fun encode(value: SubscriptionId): Long { + return value.value + } +} diff --git a/sphinx/application/data/features/feature-coredb/src/main/java/chat/sphinx/feature_coredb/adapters/subscription/SubscriptionAdapters.kt b/sphinx/application/data/features/feature-coredb/src/main/java/chat/sphinx/feature_coredb/adapters/subscription/SubscriptionAdapters.kt new file mode 100644 index 0000000000..fa67cbb3f0 --- /dev/null +++ b/sphinx/application/data/features/feature-coredb/src/main/java/chat/sphinx/feature_coredb/adapters/subscription/SubscriptionAdapters.kt @@ -0,0 +1,36 @@ +package chat.sphinx.feature_coredb.adapters.subscription + +import chat.sphinx.wrapper_common.subscription.Cron +import chat.sphinx.wrapper_common.subscription.EndNumber +import chat.sphinx.wrapper_common.subscription.SubscriptionCount +import com.squareup.sqldelight.ColumnAdapter + +internal class CronAdapter: ColumnAdapter { + override fun decode(databaseValue: String): Cron { + return Cron(databaseValue) + } + + override fun encode(value: Cron): String { + return value.value + } +} + +internal class EndNumberAdapter: ColumnAdapter { + override fun decode(databaseValue: Long): EndNumber { + return EndNumber(databaseValue) + } + + override fun encode(value: EndNumber): Long { + return value.value + } +} + +internal class SubscriptionCountAdapter: ColumnAdapter { + override fun decode(databaseValue: Long): SubscriptionCount { + return SubscriptionCount(databaseValue) + } + + override fun encode(value: SubscriptionCount): Long { + return value.value + } +} \ No newline at end of file diff --git a/sphinx/application/data/features/feature-repository-android/src/main/java/chat/sphinx/feature_repository_android/SphinxRepositoryAndroid.kt b/sphinx/application/data/features/feature-repository-android/src/main/java/chat/sphinx/feature_repository_android/SphinxRepositoryAndroid.kt index bc052ca2d1..5b798a3bd6 100644 --- a/sphinx/application/data/features/feature-repository-android/src/main/java/chat/sphinx/feature_repository_android/SphinxRepositoryAndroid.kt +++ b/sphinx/application/data/features/feature-repository-android/src/main/java/chat/sphinx/feature_repository_android/SphinxRepositoryAndroid.kt @@ -5,15 +5,15 @@ import androidx.paging.PagingSource import chat.sphinx.concept_coredb.CoreDB import chat.sphinx.concept_crypto_rsa.RSA import chat.sphinx.concept_meme_server.MemeServerTokenHandler -import chat.sphinx.concept_network_query_meme_server.NetworkQueryMemeServer import chat.sphinx.concept_network_query_chat.NetworkQueryChat import chat.sphinx.concept_network_query_contact.NetworkQueryContact import chat.sphinx.concept_network_query_invite.NetworkQueryInvite import chat.sphinx.concept_network_query_lightning.NetworkQueryLightning +import chat.sphinx.concept_network_query_meme_server.NetworkQueryMemeServer import chat.sphinx.concept_network_query_message.NetworkQueryMessage +import chat.sphinx.concept_network_query_subscription.NetworkQuerySubscription import chat.sphinx.concept_network_query_verify_external.NetworkQueryAuthorizeExternal import chat.sphinx.concept_paging.PageSourceWrapper -import chat.sphinx.concept_relay.RelayDataHandler import chat.sphinx.concept_repository_dashboard.DashboardItem import chat.sphinx.concept_repository_dashboard_android.RepositoryDashboardAndroid import chat.sphinx.concept_socket_io.SocketIOManager @@ -49,6 +49,7 @@ class SphinxRepositoryAndroid( networkQueryMessage: NetworkQueryMessage, networkQueryInvite: NetworkQueryInvite, networkQueryAuthorizeExternal: NetworkQueryAuthorizeExternal, + networkQuerySubscription: NetworkQuerySubscription, rsa: RSA, socketIOManager: SocketIOManager, LOG: SphinxLogger, @@ -68,6 +69,7 @@ class SphinxRepositoryAndroid( networkQueryMessage, networkQueryInvite, networkQueryAuthorizeExternal, + networkQuerySubscription, rsa, socketIOManager, LOG, diff --git a/sphinx/application/data/features/feature-repository/build.gradle b/sphinx/application/data/features/feature-repository/build.gradle index a172102090..55abc71627 100644 --- a/sphinx/application/data/features/feature-repository/build.gradle +++ b/sphinx/application/data/features/feature-repository/build.gradle @@ -23,6 +23,7 @@ dependencies { api project(path: ':sphinx:application:data:concepts:repositories:concept-repository-lightning') api project(path: ':sphinx:application:data:concepts:repositories:concept-repository-media') api project(path: ':sphinx:application:data:concepts:repositories:concept-repository-message') + api project(path: ':sphinx:application:data:concepts:repositories:concept-repository-subscription') api project(path: ':sphinx:application:network:concepts:queries:concept-network-query-meme-server') api project(path: ':sphinx:application:network:concepts:queries:concept-network-query-chat') diff --git a/sphinx/application/data/features/feature-repository/src/main/java/chat/sphinx/feature_repository/SphinxRepository.kt b/sphinx/application/data/features/feature-repository/src/main/java/chat/sphinx/feature_repository/SphinxRepository.kt index e048965d1c..1a1738c117 100644 --- a/sphinx/application/data/features/feature-repository/src/main/java/chat/sphinx/feature_repository/SphinxRepository.kt +++ b/sphinx/application/data/features/feature-repository/src/main/java/chat/sphinx/feature_repository/SphinxRepository.kt @@ -20,6 +20,10 @@ import chat.sphinx.concept_network_query_message.NetworkQueryMessage import chat.sphinx.concept_network_query_message.model.MessageDto import chat.sphinx.concept_network_query_message.model.PostMessageDto import chat.sphinx.concept_network_query_message.model.PostPaymentDto +import chat.sphinx.concept_network_query_subscription.NetworkQuerySubscription +import chat.sphinx.concept_network_query_subscription.model.PostSubscriptionDto +import chat.sphinx.concept_network_query_subscription.model.PutSubscriptionDto +import chat.sphinx.concept_network_query_subscription.model.SubscriptionDto import chat.sphinx.concept_network_query_verify_external.NetworkQueryAuthorizeExternal import chat.sphinx.concept_repository_chat.ChatRepository import chat.sphinx.concept_repository_chat.model.CreateTribe @@ -32,6 +36,7 @@ import chat.sphinx.concept_repository_message.MessageRepository import chat.sphinx.concept_repository_message.model.AttachmentInfo import chat.sphinx.concept_repository_message.model.SendMessage import chat.sphinx.concept_repository_message.model.SendPayment +import chat.sphinx.concept_repository_subscription.SubscriptionRepository import chat.sphinx.concept_socket_io.SocketIOManager import chat.sphinx.concept_socket_io.SphinxSocketIOMessage import chat.sphinx.concept_socket_io.SphinxSocketIOMessageListener @@ -41,6 +46,7 @@ import chat.sphinx.feature_repository.mappers.contact.ContactDboPresenterMapper import chat.sphinx.feature_repository.mappers.invite.InviteDboPresenterMapper import chat.sphinx.feature_repository.mappers.mapListFrom import chat.sphinx.feature_repository.mappers.message.MessageDboPresenterMapper +import chat.sphinx.feature_repository.mappers.subscription.SubscriptionDboPresenterMapper import chat.sphinx.feature_repository.model.MessageDboWrapper import chat.sphinx.feature_repository.model.MessageMediaDboWrapper import chat.sphinx.feature_repository.util.* @@ -59,6 +65,8 @@ import chat.sphinx.wrapper_common.dashboard.toChatId import chat.sphinx.wrapper_common.invite.InviteStatus import chat.sphinx.wrapper_common.lightning.* import chat.sphinx.wrapper_common.message.* +import chat.sphinx.wrapper_common.subscription.EndNumber +import chat.sphinx.wrapper_common.subscription.SubscriptionId import chat.sphinx.wrapper_contact.* import chat.sphinx.wrapper_invite.Invite import chat.sphinx.wrapper_io_utils.InputStreamProvider @@ -75,6 +83,7 @@ import chat.sphinx.wrapper_relay.AuthorizationToken import chat.sphinx.wrapper_relay.RelayUrl import chat.sphinx.wrapper_rsa.RsaPrivateKey import chat.sphinx.wrapper_rsa.RsaPublicKey +import chat.sphinx.wrapper_subscription.Subscription import com.squareup.moshi.Moshi import com.squareup.sqldelight.runtime.coroutines.asFlow import com.squareup.sqldelight.runtime.coroutines.mapToList @@ -110,6 +119,7 @@ abstract class SphinxRepository( private val networkQueryMessage: NetworkQueryMessage, private val networkQueryInvite: NetworkQueryInvite, private val networkQueryAuthorizeExternal: NetworkQueryAuthorizeExternal, + private val networkQuerySubscription: NetworkQuerySubscription, private val rsa: RSA, private val socketIOManager: SocketIOManager, protected val LOG: SphinxLogger, @@ -117,6 +127,7 @@ abstract class SphinxRepository( ContactRepository, LightningRepository, MessageRepository, + SubscriptionRepository, RepositoryDashboard, RepositoryMedia, CoroutineDispatchers by dispatchers, @@ -3626,4 +3637,234 @@ abstract class SphinxRepository( return response } + + /*** + * Subscriptions + */ + + private val subscriptionLock = Mutex() + private val subscriptionDboPresenterMapper: SubscriptionDboPresenterMapper by lazy { + SubscriptionDboPresenterMapper(dispatchers) + } + + override fun getActiveSubscriptionByContactId(contactId: ContactId): Flow = flow { + emitAll( + coreDB.getSphinxDatabaseQueries().subscriptionGetLastActiveByContactId(contactId) + .asFlow() + .mapToOneOrNull(io) + .map { it?.let { subscriptionDboPresenterMapper.mapFrom(it) } } + .distinctUntilChanged() + ) + } + + override suspend fun createSubscription( + amount: Sat, + interval: String, + contactId: ContactId, + chatId: ChatId?, + endDate: String?, + endNumber: EndNumber? + ): Response { + var response: Response? = null + + applicationScope.launch(mainImmediate) { + networkQuerySubscription.postSubscription( + PostSubscriptionDto( + amount = amount.value, + contact_id = contactId.value, + chat_id = chatId?.value, + interval = interval, + end_number = endNumber?.value, + end_date = endDate + ) + ).collect { loadResponse -> + @Exhaustive + when (loadResponse) { + is LoadResponse.Loading -> {} + is Response.Error -> { + response = loadResponse + } + is Response.Success -> { + response = loadResponse + val queries = coreDB.getSphinxDatabaseQueries() + + subscriptionLock.withLock { + withContext(io) { + queries.transaction { + upsertSubscription( + loadResponse.value, + queries + ) + } + } + } + } + } + } + }.join() + + return response ?: Response.Error(ResponseError(("Failed to create subscription"))) + } + + override suspend fun updateSubscription( + id: SubscriptionId, + amount: Sat, + interval: String, + contactId: ContactId, + chatId: ChatId?, + endDate: String?, + endNumber: EndNumber? + ): Response { + var response: Response? = null + + applicationScope.launch(mainImmediate) { + + networkQuerySubscription.putSubscription( + id, + PutSubscriptionDto( + amount = amount.value, + contact_id = contactId.value, + chat_id = chatId?.value, + interval = interval, + end_number = endNumber?.value, + end_date = endDate + ) + ).collect { loadResponse -> + @Exhaustive + when (loadResponse) { + is LoadResponse.Loading -> {} + is Response.Error -> { + response = loadResponse + } + is Response.Success -> { + response = loadResponse + val queries = coreDB.getSphinxDatabaseQueries() + + subscriptionLock.withLock { + withContext(io) { + queries.transaction { + upsertSubscription( + loadResponse.value, + queries + ) + } + } + } + } + } + } + }.join() + + return response ?: Response.Error(ResponseError(("Failed to update subscription"))) + } + + override suspend fun restartSubscription( + subscriptionId: SubscriptionId + ): Response { + var response: Response? = null + + applicationScope.launch(mainImmediate) { + + networkQuerySubscription.putRestartSubscription( + subscriptionId + ).collect { loadResponse -> + @Exhaustive + when (loadResponse) { + is LoadResponse.Loading -> {} + is Response.Error -> { + response = loadResponse + } + is Response.Success -> { + response = loadResponse + val queries = coreDB.getSphinxDatabaseQueries() + + subscriptionLock.withLock { + withContext(io) { + queries.transaction { + upsertSubscription( + loadResponse.value, + queries + ) + } + } + } + } + } + } + }.join() + + return response ?: Response.Error(ResponseError(("Failed to restart subscription"))) + } + + override suspend fun pauseSubscription( + subscriptionId: SubscriptionId + ): Response { + var response: Response? = null + + applicationScope.launch(mainImmediate) { + + networkQuerySubscription.putPauseSubscription( + subscriptionId + ).collect { loadResponse -> + @Exhaustive + when (loadResponse) { + is LoadResponse.Loading -> {} + is Response.Error -> { + response = loadResponse + } + is Response.Success -> { + response = loadResponse + val queries = coreDB.getSphinxDatabaseQueries() + + subscriptionLock.withLock { + withContext(io) { + queries.transaction { + upsertSubscription( + loadResponse.value, + queries + ) + } + } + } + } + } + } + }.join() + + return response ?: Response.Error(ResponseError(("Failed to pause subscription"))) + } + + override suspend fun deleteSubscription( + subscriptionId: SubscriptionId + ): Response { + var response: Response? = null + + applicationScope.launch(mainImmediate) { + networkQuerySubscription.deleteSubscription( + subscriptionId + ).collect { loadResponse -> + @Exhaustive + when (loadResponse) { + is LoadResponse.Loading -> {} + is Response.Error -> { + response = loadResponse + } + is Response.Success -> { + response = loadResponse + val queries = coreDB.getSphinxDatabaseQueries() + + subscriptionLock.withLock { + withContext(io) { + queries.transaction { + deleteSubscriptionById(subscriptionId, queries) + } + } + } + } + } + } + }.join() + + return response ?: Response.Error(ResponseError(("Failed to delete subscription"))) + } } diff --git a/sphinx/application/data/features/feature-repository/src/main/java/chat/sphinx/feature_repository/mappers/subscription/SubscriptionDboPresenterMapper.kt b/sphinx/application/data/features/feature-repository/src/main/java/chat/sphinx/feature_repository/mappers/subscription/SubscriptionDboPresenterMapper.kt new file mode 100644 index 0000000000..5f051a41d6 --- /dev/null +++ b/sphinx/application/data/features/feature-repository/src/main/java/chat/sphinx/feature_repository/mappers/subscription/SubscriptionDboPresenterMapper.kt @@ -0,0 +1,50 @@ +package chat.sphinx.feature_repository.mappers.subscription + +import chat.sphinx.conceptcoredb.SubscriptionDbo +import chat.sphinx.feature_repository.mappers.ClassMapper +import chat.sphinx.wrapper_subscription.Subscription +import io.matthewnelson.concept_coroutines.CoroutineDispatchers + +@Suppress("NOTHING_TO_INLINE") +inline fun SubscriptionDbo.toSubscription(): Subscription = + Subscription( + id, + cron, + amount, + end_number, + count, + end_date, + ended, + paused, + created_at, + updated_at, + chat_id, + contact_id + ) + +internal class SubscriptionDboPresenterMapper( + dispatchers: CoroutineDispatchers +): ClassMapper(dispatchers) { + override suspend fun mapFrom(value: SubscriptionDbo): Subscription { + return value.toSubscription() + } + + override suspend fun mapTo(value: Subscription): SubscriptionDbo { + value.apply { + return SubscriptionDbo( + id, + cron, + amount, + endNumber, + count, + endDate, + ended, + paused, + createdAt, + updatedAt, + chatId, + contactId + ) + } + } +} diff --git a/sphinx/application/data/features/feature-repository/src/main/java/chat/sphinx/feature_repository/util/Extensions.kt b/sphinx/application/data/features/feature-repository/src/main/java/chat/sphinx/feature_repository/util/Extensions.kt index 3ca6fbf4dc..383dc6e296 100644 --- a/sphinx/application/data/features/feature-repository/src/main/java/chat/sphinx/feature_repository/util/Extensions.kt +++ b/sphinx/application/data/features/feature-repository/src/main/java/chat/sphinx/feature_repository/util/Extensions.kt @@ -6,11 +6,12 @@ import chat.sphinx.concept_network_query_contact.model.ContactDto import chat.sphinx.concept_network_query_invite.model.InviteDto import chat.sphinx.concept_network_query_lightning.model.balance.BalanceDto import chat.sphinx.concept_network_query_message.model.MessageDto +import chat.sphinx.concept_network_query_subscription.model.SubscriptionDto import chat.sphinx.conceptcoredb.SphinxDatabaseQueries import chat.sphinx.wrapper_chat.* import chat.sphinx.wrapper_common.* -import chat.sphinx.wrapper_common.dashboard.ChatId import chat.sphinx.wrapper_common.chat.ChatUUID +import chat.sphinx.wrapper_common.dashboard.ChatId import chat.sphinx.wrapper_common.dashboard.ContactId import chat.sphinx.wrapper_common.dashboard.InviteId import chat.sphinx.wrapper_common.invite.InviteStatus @@ -20,6 +21,10 @@ import chat.sphinx.wrapper_common.invite.toInviteStatus import chat.sphinx.wrapper_common.lightning.* import chat.sphinx.wrapper_common.message.MessageId import chat.sphinx.wrapper_common.message.toMessageUUID +import chat.sphinx.wrapper_common.subscription.Cron +import chat.sphinx.wrapper_common.subscription.EndNumber +import chat.sphinx.wrapper_common.subscription.SubscriptionCount +import chat.sphinx.wrapper_common.subscription.SubscriptionId import chat.sphinx.wrapper_contact.* import chat.sphinx.wrapper_invite.InviteString import chat.sphinx.wrapper_lightning.NodeBalance @@ -390,6 +395,7 @@ inline fun TransactionCallbacks.deleteContactById( queries.contactDeleteById(contactId) queries.inviteDeleteByContactId(contactId) queries.dashboardDeleteById(contactId) + queries.subscriptionDeleteByContactId(contactId) } @Suppress("NOTHING_TO_INLINE") @@ -400,3 +406,29 @@ inline fun TransactionCallbacks.deleteMessageById( queries.messageDeleteById(messageId) queries.messageMediaDeleteById(messageId) } + +@Suppress("NOTHING_TO_INLINE") +inline fun TransactionCallbacks.upsertSubscription(subscriptionDto: SubscriptionDto, queries: SphinxDatabaseQueries) { + queries.subscriptionUpsert( + id = SubscriptionId(subscriptionDto.id), + amount = Sat(subscriptionDto.amount), + contact_id = ContactId(subscriptionDto.contact_id), + chat_id = ChatId(subscriptionDto.chat_id), + count = SubscriptionCount(subscriptionDto.count.toLong()), + cron = Cron(subscriptionDto.cron), + end_date = subscriptionDto.end_date?.toDateTime(), + end_number = subscriptionDto.end_number?.let { EndNumber(it.toLong()) }, + created_at = subscriptionDto.created_at.toDateTime(), + updated_at = subscriptionDto.updated_at.toDateTime(), + ended = subscriptionDto.endedActual, + paused = subscriptionDto.pausedActual, + ) +} + +@Suppress("NOTHING_TO_INLINE") +inline fun TransactionCallbacks.deleteSubscriptionById( + subscriptionId: SubscriptionId, + queries: SphinxDatabaseQueries +) { + queries.subscriptionDeleteById(subscriptionId) +} \ No newline at end of file diff --git a/sphinx/application/network/concepts/queries/concept-network-query-subscription/src/main/java/chat/sphinx/concept_network_query_subscription/NetworkQuerySubscription.kt b/sphinx/application/network/concepts/queries/concept-network-query-subscription/src/main/java/chat/sphinx/concept_network_query_subscription/NetworkQuerySubscription.kt index 130ffce2e0..c1deb74ae1 100644 --- a/sphinx/application/network/concepts/queries/concept-network-query-subscription/src/main/java/chat/sphinx/concept_network_query_subscription/NetworkQuerySubscription.kt +++ b/sphinx/application/network/concepts/queries/concept-network-query-subscription/src/main/java/chat/sphinx/concept_network_query_subscription/NetworkQuerySubscription.kt @@ -1,8 +1,10 @@ package chat.sphinx.concept_network_query_subscription +import chat.sphinx.concept_network_query_subscription.model.PostSubscriptionDto +import chat.sphinx.concept_network_query_subscription.model.PutSubscriptionDto import chat.sphinx.concept_network_query_subscription.model.SubscriptionDto -import chat.sphinx.kotlin_response.ResponseError import chat.sphinx.kotlin_response.LoadResponse +import chat.sphinx.kotlin_response.ResponseError import chat.sphinx.wrapper_common.dashboard.ContactId import chat.sphinx.wrapper_common.subscription.SubscriptionId import chat.sphinx.wrapper_relay.AuthorizationToken @@ -31,17 +33,36 @@ abstract class NetworkQuerySubscription { /////////// /// PUT /// /////////// -// app.put('/subscription/:id', subcriptions.editSubscription) -// app.put('/subscription/:id/pause', subcriptions.pauseSubscription) -// app.put('/subscription/:id/restart', subcriptions.restartSubscription) + abstract fun putSubscription( + subscriptionId: SubscriptionId, + putSubscriptionDto: PutSubscriptionDto, + relayData: Pair? = null + ): Flow> + + abstract fun putPauseSubscription( + subscriptionId: SubscriptionId, + relayData: Pair? = null + ): Flow> + + abstract fun putRestartSubscription( + subscriptionId: SubscriptionId, + relayData: Pair? = null + ): Flow> //////////// /// POST /// //////////// // app.post('/subscriptions', subcriptions.createSubscription) + abstract fun postSubscription( + postSubscriptionDto: PostSubscriptionDto, + relayData: Pair? = null + ): Flow> ////////////// /// DELETE /// ////////////// -// app.delete('/subscription/:id', subcriptions.deleteSubscription) + abstract fun deleteSubscription( + subscriptionId: SubscriptionId, + relayData: Pair? = null + ): Flow> } \ No newline at end of file diff --git a/sphinx/application/network/concepts/queries/concept-network-query-subscription/src/main/java/chat/sphinx/concept_network_query_subscription/model/PostSubscriptionDto.kt b/sphinx/application/network/concepts/queries/concept-network-query-subscription/src/main/java/chat/sphinx/concept_network_query_subscription/model/PostSubscriptionDto.kt new file mode 100644 index 0000000000..8f4c2c1fde --- /dev/null +++ b/sphinx/application/network/concepts/queries/concept-network-query-subscription/src/main/java/chat/sphinx/concept_network_query_subscription/model/PostSubscriptionDto.kt @@ -0,0 +1,13 @@ +package chat.sphinx.concept_network_query_subscription.model + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class PostSubscriptionDto( + val amount: Long, + val contact_id: Long, + val chat_id: Long?, + val end_number: Long?, + val end_date: String?, + val interval: String +) diff --git a/sphinx/application/network/concepts/queries/concept-network-query-subscription/src/main/java/chat/sphinx/concept_network_query_subscription/model/PutSubscriptionDto.kt b/sphinx/application/network/concepts/queries/concept-network-query-subscription/src/main/java/chat/sphinx/concept_network_query_subscription/model/PutSubscriptionDto.kt new file mode 100644 index 0000000000..31091dcba5 --- /dev/null +++ b/sphinx/application/network/concepts/queries/concept-network-query-subscription/src/main/java/chat/sphinx/concept_network_query_subscription/model/PutSubscriptionDto.kt @@ -0,0 +1,13 @@ +package chat.sphinx.concept_network_query_subscription.model + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class PutSubscriptionDto( + val amount: Long, + val contact_id: Long, + val chat_id: Long?, + val end_number: Long?, + val end_date: String?, + val interval: String +) diff --git a/sphinx/application/network/features/queries/feature-network-query-subscription/src/main/java/chat/sphinx/feature_network_query_subscription/NetworkQuerySubscriptionImpl.kt b/sphinx/application/network/features/queries/feature-network-query-subscription/src/main/java/chat/sphinx/feature_network_query_subscription/NetworkQuerySubscriptionImpl.kt index ae2e69ed73..485e3fff09 100644 --- a/sphinx/application/network/features/queries/feature-network-query-subscription/src/main/java/chat/sphinx/feature_network_query_subscription/NetworkQuerySubscriptionImpl.kt +++ b/sphinx/application/network/features/queries/feature-network-query-subscription/src/main/java/chat/sphinx/feature_network_query_subscription/NetworkQuerySubscriptionImpl.kt @@ -1,10 +1,13 @@ package chat.sphinx.feature_network_query_subscription import chat.sphinx.concept_network_query_subscription.NetworkQuerySubscription +import chat.sphinx.concept_network_query_subscription.model.PostSubscriptionDto +import chat.sphinx.concept_network_query_subscription.model.PutSubscriptionDto import chat.sphinx.concept_network_query_subscription.model.SubscriptionDto import chat.sphinx.concept_network_relay_call.NetworkRelayCall -import chat.sphinx.feature_network_query_subscription.model.GetSubscriptionRelayResponse +import chat.sphinx.feature_network_query_subscription.model.DeleteSubscriptionRelayResponse import chat.sphinx.feature_network_query_subscription.model.GetSubscriptionsRelayResponse +import chat.sphinx.feature_network_query_subscription.model.SubscriptionRelayResponse import chat.sphinx.kotlin_response.LoadResponse import chat.sphinx.kotlin_response.ResponseError import chat.sphinx.wrapper_common.dashboard.ContactId @@ -51,7 +54,7 @@ class NetworkQuerySubscriptionImpl( relayData: Pair? ): Flow> = networkRelayCall.relayGet( - responseJsonClass = GetSubscriptionRelayResponse::class.java, + responseJsonClass = SubscriptionRelayResponse::class.java, relayEndpoint = "$ENDPOINT_SUBSCRIPTION/${subscriptionId.value}", relayData = relayData ) @@ -69,17 +72,69 @@ class NetworkQuerySubscriptionImpl( /////////// /// PUT /// /////////// -// app.put('/subscription/:id', subcriptions.editSubscription) -// app.put('/subscription/:id/pause', subcriptions.pauseSubscription) -// app.put('/subscription/:id/restart', subcriptions.restartSubscription) + override fun putSubscription( + subscriptionId: SubscriptionId, + putSubscriptionDto: PutSubscriptionDto, + relayData: Pair? + ): Flow> = + networkRelayCall.relayPut( + responseJsonClass = SubscriptionRelayResponse::class.java, + relayEndpoint = "$ENDPOINT_SUBSCRIPTION/${subscriptionId.value}", + requestBodyJsonClass = PutSubscriptionDto::class.java, + requestBody = putSubscriptionDto, + relayData = relayData + ) + + override fun putPauseSubscription( + subscriptionId: SubscriptionId, + relayData: Pair? + ): Flow> = + networkRelayCall.relayPut( + responseJsonClass = SubscriptionRelayResponse::class.java, + relayEndpoint = "$ENDPOINT_SUBSCRIPTION/${subscriptionId.value}/pause", + requestBodyJsonClass = Map::class.java, + requestBody = mapOf(Pair("", "")), + relayData = relayData + ) + + override fun putRestartSubscription( + subscriptionId: SubscriptionId, + relayData: Pair? + ): Flow> = + networkRelayCall.relayPut( + responseJsonClass = SubscriptionRelayResponse::class.java, + relayEndpoint = "$ENDPOINT_SUBSCRIPTION/${subscriptionId.value}/restart", + requestBodyJsonClass = Map::class.java, + requestBody = mapOf(Pair("", "")), + relayData = relayData + ) //////////// /// POST /// //////////// -// app.post('/subscriptions', subcriptions.createSubscription) + override fun postSubscription( + postSubscriptionDto: PostSubscriptionDto, + relayData: Pair? + ): Flow> = + networkRelayCall.relayPost( + responseJsonClass = SubscriptionRelayResponse::class.java, + relayEndpoint = ENDPOINT_SUBSCRIPTIONS, + requestBodyJsonClass = PostSubscriptionDto::class.java, + requestBody = postSubscriptionDto, + relayData = relayData + ) ////////////// /// DELETE /// ////////////// -// app.delete('/subscription/:id', subcriptions.deleteSubscription) + override fun deleteSubscription( + subscriptionId: SubscriptionId, + relayData: Pair? + ): Flow> = + networkRelayCall.relayDelete( + responseJsonClass = DeleteSubscriptionRelayResponse::class.java, + relayEndpoint = "$ENDPOINT_SUBSCRIPTION/${subscriptionId.value}", + requestBody = null, + relayData = relayData + ) } diff --git a/sphinx/application/network/features/queries/feature-network-query-subscription/src/main/java/chat/sphinx/feature_network_query_subscription/model/DeleteSubscriptionRelayResponse.kt b/sphinx/application/network/features/queries/feature-network-query-subscription/src/main/java/chat/sphinx/feature_network_query_subscription/model/DeleteSubscriptionRelayResponse.kt new file mode 100644 index 0000000000..ab812cf96e --- /dev/null +++ b/sphinx/application/network/features/queries/feature-network-query-subscription/src/main/java/chat/sphinx/feature_network_query_subscription/model/DeleteSubscriptionRelayResponse.kt @@ -0,0 +1,11 @@ +package chat.sphinx.feature_network_query_subscription.model + +import chat.sphinx.concept_network_relay_call.RelayResponse +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class DeleteSubscriptionRelayResponse( + override val success: Boolean, + override val response: Any?, + override val error: String? +): RelayResponse() diff --git a/sphinx/application/network/features/queries/feature-network-query-subscription/src/main/java/chat/sphinx/feature_network_query_subscription/model/GetSubscriptionRelayResponse.kt b/sphinx/application/network/features/queries/feature-network-query-subscription/src/main/java/chat/sphinx/feature_network_query_subscription/model/SubscriptionRelayResponse.kt similarity index 91% rename from sphinx/application/network/features/queries/feature-network-query-subscription/src/main/java/chat/sphinx/feature_network_query_subscription/model/GetSubscriptionRelayResponse.kt rename to sphinx/application/network/features/queries/feature-network-query-subscription/src/main/java/chat/sphinx/feature_network_query_subscription/model/SubscriptionRelayResponse.kt index 37b127e3cf..8447157e6e 100644 --- a/sphinx/application/network/features/queries/feature-network-query-subscription/src/main/java/chat/sphinx/feature_network_query_subscription/model/GetSubscriptionRelayResponse.kt +++ b/sphinx/application/network/features/queries/feature-network-query-subscription/src/main/java/chat/sphinx/feature_network_query_subscription/model/SubscriptionRelayResponse.kt @@ -5,7 +5,7 @@ import chat.sphinx.concept_network_relay_call.RelayResponse import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) -data class GetSubscriptionRelayResponse( +data class SubscriptionRelayResponse( override val success: Boolean, override val response: SubscriptionDto?, override val error: String? diff --git a/sphinx/application/sphinx/src/main/java/chat/sphinx/di/RepositoryModule.kt b/sphinx/application/sphinx/src/main/java/chat/sphinx/di/RepositoryModule.kt index 583d2f19ab..2283d28417 100644 --- a/sphinx/application/sphinx/src/main/java/chat/sphinx/di/RepositoryModule.kt +++ b/sphinx/application/sphinx/src/main/java/chat/sphinx/di/RepositoryModule.kt @@ -3,20 +3,21 @@ package chat.sphinx.di import android.content.Context import chat.sphinx.concept_crypto_rsa.RSA import chat.sphinx.concept_meme_server.MemeServerTokenHandler -import chat.sphinx.concept_network_query_meme_server.NetworkQueryMemeServer -import chat.sphinx.concept_repository_chat.ChatRepository -import chat.sphinx.concept_repository_message.MessageRepository import chat.sphinx.concept_network_query_chat.NetworkQueryChat import chat.sphinx.concept_network_query_contact.NetworkQueryContact import chat.sphinx.concept_network_query_invite.NetworkQueryInvite import chat.sphinx.concept_network_query_lightning.NetworkQueryLightning +import chat.sphinx.concept_network_query_meme_server.NetworkQueryMemeServer import chat.sphinx.concept_network_query_message.NetworkQueryMessage +import chat.sphinx.concept_network_query_subscription.NetworkQuerySubscription import chat.sphinx.concept_network_query_verify_external.NetworkQueryAuthorizeExternal -import chat.sphinx.concept_relay.RelayDataHandler +import chat.sphinx.concept_repository_chat.ChatRepository import chat.sphinx.concept_repository_contact.ContactRepository import chat.sphinx.concept_repository_dashboard_android.RepositoryDashboardAndroid import chat.sphinx.concept_repository_lightning.LightningRepository import chat.sphinx.concept_repository_media.RepositoryMedia +import chat.sphinx.concept_repository_message.MessageRepository +import chat.sphinx.concept_repository_subscription.SubscriptionRepository import chat.sphinx.concept_socket_io.SocketIOManager import chat.sphinx.database.SphinxCoreDBImpl import chat.sphinx.feature_coredb.CoreDBImpl @@ -126,6 +127,7 @@ object RepositoryModule { networkQueryMessage: NetworkQueryMessage, networkQueryInvite: NetworkQueryInvite, networkQueryAuthorizeExternal: NetworkQueryAuthorizeExternal, + networkQuerySubscription: NetworkQuerySubscription, socketIOManager: SocketIOManager, rsa: RSA, sphinxLogger: SphinxLogger, @@ -146,6 +148,7 @@ object RepositoryModule { networkQueryMessage, networkQueryInvite, networkQueryAuthorizeExternal, + networkQuerySubscription, rsa, socketIOManager, sphinxLogger, @@ -175,6 +178,12 @@ object RepositoryModule { ): MessageRepository = sphinxRepositoryAndroid + @Provides + fun provideSubscriptionRepository( + sphinxRepositoryAndroid: SphinxRepositoryAndroid + ): SubscriptionRepository = + sphinxRepositoryAndroid + @Provides @Suppress("UNCHECKED_CAST") fun provideRepositoryDashboardAndroid( diff --git a/sphinx/screens-detail/contact/contact-common/build.gradle b/sphinx/screens-detail/contact/contact-common/build.gradle index 33995ba1bb..c601bf180d 100644 --- a/sphinx/screens-detail/contact/contact-common/build.gradle +++ b/sphinx/screens-detail/contact/contact-common/build.gradle @@ -35,6 +35,7 @@ dependencies { api project(path: ':sphinx:activity:insetter-activity') api project(path: ':sphinx:application:data:concepts:concept-image-loader') api project(path: ':sphinx:application:data:concepts:repositories:concept-repository-contact') + api project(path: ':sphinx:application:data:concepts:repositories:concept-repository-subscription') api project(path: ':sphinx:screens-detail:common:detail-resources') api project(path: ':sphinx:application:common:resources') api project(path: ':sphinx:screens-detail:scanner:scanner-view-model-coordinator') diff --git a/sphinx/screens-detail/contact/contact-common/src/main/java/chat/sphinx/contact/ui/ContactFragment.kt b/sphinx/screens-detail/contact/contact-common/src/main/java/chat/sphinx/contact/ui/ContactFragment.kt index 623126a90b..923715cd19 100644 --- a/sphinx/screens-detail/contact/contact-common/src/main/java/chat/sphinx/contact/ui/ContactFragment.kt +++ b/sphinx/screens-detail/contact/contact-common/src/main/java/chat/sphinx/contact/ui/ContactFragment.kt @@ -8,6 +8,7 @@ import android.text.Editable import android.text.TextWatcher import android.view.View import androidx.annotation.LayoutRes +import androidx.core.content.ContextCompat import androidx.lifecycle.lifecycleScope import androidx.navigation.NavArgs import androidx.viewbinding.ViewBinding @@ -17,7 +18,7 @@ import chat.sphinx.concept_image_loader.Transformation import chat.sphinx.concept_user_colors_helper.UserColorsHelper import chat.sphinx.contact.R import chat.sphinx.contact.databinding.LayoutContactBinding -import chat.sphinx.detail_resources.databinding.LayoutDetailScreenHeaderBinding +import chat.sphinx.contact.databinding.LayoutContactDetailScreenHeaderBinding import chat.sphinx.insetter_activity.InsetterActivity import chat.sphinx.insetter_activity.addNavigationBarPadding import chat.sphinx.resources.getRandomHexCode @@ -47,7 +48,7 @@ abstract class ContactFragment< VB >(layoutId) { - abstract val headerBinding: LayoutDetailScreenHeaderBinding + abstract val headerBinding: LayoutContactDetailScreenHeaderBinding abstract val contactBinding: LayoutContactBinding abstract val userColorsHelper: UserColorsHelper @@ -78,7 +79,6 @@ abstract class ContactFragment< textViewDetailScreenHeaderName.text = getHeaderText() textViewDetailScreenHeaderNavBack.apply { - goneIfFalse(viewModel.isFromAddFriend()) setOnClickListener { lifecycleScope.launch(viewModel.mainImmediate) { @@ -92,6 +92,12 @@ abstract class ContactFragment< viewModel.navigator.closeDetailScreen() } } + + textViewDetailScreenSubscribe.setOnClickListener { + lifecycleScope.launch(viewModel.mainImmediate) { + viewModel.navigator.closeDetailScreen() + } + } } contactBinding.apply { @@ -102,17 +108,10 @@ abstract class ContactFragment< } } - editTextContactAddress.isEnabled = !viewModel.isExistingContact() - - scanAddressButton.goneIfTrue(viewModel.isExistingContact()) scanAddressButton.setOnClickListener { viewModel.requestScanner() } - buttonQrCode.goneIfFalse(viewModel.isExistingContact()) - - layoutConstraintExistingContactProfilePicture.goneIfFalse(viewModel.isExistingContact()) - editTextContactAddress.addTextChangedListener(object : TextWatcher { override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) { } @@ -180,7 +179,30 @@ abstract class ContactFragment< } is ContactSideEffect.ExistingContact -> { + headerBinding.apply { + textViewDetailScreenHeaderNavBack.gone + + textViewDetailScreenSubscribe.visible + + textViewDetailScreenSubscribe.text = if (sideEffect.subscribed) { + getString(R.string.edit_contact_header_subscribed_button) + } else { + getString(R.string.edit_contact_header_subscribe_button) + } + + textViewDetailScreenSubscribe.backgroundTintList = if (sideEffect.subscribed) { + ContextCompat.getColorStateList(root.context, R.color.secondaryText) + } else { + ContextCompat.getColorStateList(root.context, R.color.primaryBlue) + } + } + contactBinding.apply { + editTextContactAddress.isEnabled = false + + scanAddressButton.gone + buttonQrCode.visible + editTextContactNickname.setText(sideEffect.nickname) editTextContactAddress.setText(sideEffect.pubKey.value) editTextContactRouteHint.setText(sideEffect.routeHint?.value ?: "") @@ -201,6 +223,8 @@ abstract class ContactFragment< } } + layoutConstraintExistingContactProfilePicture.visible + sideEffect.photoUrl?.let { viewModel.imageLoader.load( imageViewProfilePicture, diff --git a/sphinx/screens-detail/contact/contact-common/src/main/java/chat/sphinx/contact/ui/ContactSideEffect.kt b/sphinx/screens-detail/contact/contact-common/src/main/java/chat/sphinx/contact/ui/ContactSideEffect.kt index 0d2d3009f0..b536e3e5fd 100644 --- a/sphinx/screens-detail/contact/contact-common/src/main/java/chat/sphinx/contact/ui/ContactSideEffect.kt +++ b/sphinx/screens-detail/contact/contact-common/src/main/java/chat/sphinx/contact/ui/ContactSideEffect.kt @@ -72,7 +72,8 @@ sealed class ContactSideEffect: SideEffect() { val photoUrl: PhotoUrl?, val colorKey: String?, val pubKey: LightningNodePubKey, - val routeHint: LightningRouteHint? = null + val routeHint: LightningRouteHint? = null, + val subscribed: Boolean ): ContactSideEffect() { override suspend fun execute(value: Context) {} } diff --git a/sphinx/screens-detail/contact/contact-common/src/main/java/chat/sphinx/contact/ui/ContactViewModel.kt b/sphinx/screens-detail/contact/contact-common/src/main/java/chat/sphinx/contact/ui/ContactViewModel.kt index 95d988dc7e..32622d20b7 100644 --- a/sphinx/screens-detail/contact/contact-common/src/main/java/chat/sphinx/contact/ui/ContactViewModel.kt +++ b/sphinx/screens-detail/contact/contact-common/src/main/java/chat/sphinx/contact/ui/ContactViewModel.kt @@ -7,6 +7,7 @@ import androidx.lifecycle.viewModelScope import androidx.navigation.NavArgs import chat.sphinx.concept_image_loader.ImageLoader import chat.sphinx.concept_repository_contact.ContactRepository +import chat.sphinx.concept_repository_subscription.SubscriptionRepository import chat.sphinx.concept_view_model_coordinator.ViewModelCoordinator import chat.sphinx.contact.R import chat.sphinx.contact.navigation.ContactNavigator @@ -20,6 +21,7 @@ import chat.sphinx.wrapper_contact.ContactAlias import io.matthewnelson.android_feature_viewmodel.SideEffectViewModel import io.matthewnelson.android_feature_viewmodel.submitSideEffect import io.matthewnelson.concept_coroutines.CoroutineDispatchers +import io.matthewnelson.concept_views.viewstate.ViewStateContainer import kotlinx.coroutines.Job import kotlinx.coroutines.launch @@ -28,6 +30,7 @@ abstract class ContactViewModel ( dispatchers: CoroutineDispatchers, private val app: Application, protected val contactRepository: ContactRepository, + protected val subscriptionRepository: SubscriptionRepository, protected val scannerCoordinator: ViewModelCoordinator, val imageLoader: ImageLoader ): SideEffectViewModel< @@ -120,14 +123,6 @@ abstract class ContactViewModel ( lightningRouteHint: LightningRouteHint? ) - fun isFromAddFriend(): Boolean { - return fromAddFriend - } - - fun isExistingContact(): Boolean { - return contactId != null - } - fun toQrCodeLightningNodePubKey(nodePubKey: String) { viewModelScope.launch(mainImmediate) { navigator.toQRCodeDetail( diff --git a/sphinx/screens-detail/contact/contact-common/src/main/res/layout/layout_contact.xml b/sphinx/screens-detail/contact/contact-common/src/main/res/layout/layout_contact.xml index ebe3c502bf..2a6f6942e7 100644 --- a/sphinx/screens-detail/contact/contact-common/src/main/res/layout/layout_contact.xml +++ b/sphinx/screens-detail/contact/contact-common/src/main/res/layout/layout_contact.xml @@ -58,6 +58,7 @@ android:id="@+id/layout_constraint_existing_contact_profile_picture" android:layout_width="@dimen/default_form_scan_icon_container_width" android:layout_height="match_parent" + android:visibility="gone" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintEnd_toEndOf="parent"> @@ -168,7 +169,7 @@ android:background="@drawable/ic_qr_code" android:backgroundTint="@color/secondaryText" android:layout_marginBottom="@dimen/default_form_scan_icon_bottom_margin" - android:visibility="visible" + android:visibility="gone" /> diff --git a/sphinx/screens-detail/contact/contact-common/src/main/res/layout/layout_contact_detail_screen_header.xml b/sphinx/screens-detail/contact/contact-common/src/main/res/layout/layout_contact_detail_screen_header.xml new file mode 100644 index 0000000000..c9c03f19d2 --- /dev/null +++ b/sphinx/screens-detail/contact/contact-common/src/main/res/layout/layout_contact_detail_screen_header.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + diff --git a/sphinx/screens-detail/contact/contact-common/src/main/res/values-es/strings.xml b/sphinx/screens-detail/contact/contact-common/src/main/res/values-es/strings.xml index afafce2cee..7d27442309 100644 --- a/sphinx/screens-detail/contact/contact-common/src/main/res/values-es/strings.xml +++ b/sphinx/screens-detail/contact/contact-common/src/main/res/values-es/strings.xml @@ -8,4 +8,6 @@ Clave Pública inválida Pista de Ruta inválida No se pudo guardar el contacto + Suscribirse + Suscripto \ No newline at end of file diff --git a/sphinx/screens-detail/contact/contact-common/src/main/res/values-ja/strings.xml b/sphinx/screens-detail/contact/contact-common/src/main/res/values-ja/strings.xml index 7e2a9bcdaf..dad1716ff1 100644 --- a/sphinx/screens-detail/contact/contact-common/src/main/res/values-ja/strings.xml +++ b/sphinx/screens-detail/contact/contact-common/src/main/res/values-ja/strings.xml @@ -8,4 +8,6 @@ 無効な公開鍵 無効なルートヒント 連絡先の保存に失敗しました + Subscribe + Subscribed \ No newline at end of file diff --git a/sphinx/screens-detail/contact/contact-common/src/main/res/values-zh/strings.xml b/sphinx/screens-detail/contact/contact-common/src/main/res/values-zh/strings.xml index 620d4bd1f5..6029b663dc 100644 --- a/sphinx/screens-detail/contact/contact-common/src/main/res/values-zh/strings.xml +++ b/sphinx/screens-detail/contact/contact-common/src/main/res/values-zh/strings.xml @@ -8,4 +8,6 @@ 節點住址無效 秘密通路無效 無法保存聯繫人 + Subscribe + Subscribed \ No newline at end of file diff --git a/sphinx/screens-detail/contact/contact-common/src/main/res/values/dimens.xml b/sphinx/screens-detail/contact/contact-common/src/main/res/values/dimens.xml index b9017095b6..874d5a976c 100644 --- a/sphinx/screens-detail/contact/contact-common/src/main/res/values/dimens.xml +++ b/sphinx/screens-detail/contact/contact-common/src/main/res/values/dimens.xml @@ -9,4 +9,7 @@ 230dp 25dp 10dp + + 30dp + 14sp \ No newline at end of file diff --git a/sphinx/screens-detail/contact/contact-common/src/main/res/values/strings.xml b/sphinx/screens-detail/contact/contact-common/src/main/res/values/strings.xml index 0d3b9afacd..f30249c71c 100644 --- a/sphinx/screens-detail/contact/contact-common/src/main/res/values/strings.xml +++ b/sphinx/screens-detail/contact/contact-common/src/main/res/values/strings.xml @@ -8,4 +8,6 @@ Invalid Public Key Invalid Route Hint Failed to save contact + Subscribe + Subscribed \ No newline at end of file diff --git a/sphinx/screens-detail/contact/edit-contact/src/main/java/chat/sphinx/edit_contact/navigation/EditContactNavigator.kt b/sphinx/screens-detail/contact/edit-contact/src/main/java/chat/sphinx/edit_contact/navigation/EditContactNavigator.kt index bac3a1829f..1465164a7d 100644 --- a/sphinx/screens-detail/contact/edit-contact/src/main/java/chat/sphinx/edit_contact/navigation/EditContactNavigator.kt +++ b/sphinx/screens-detail/contact/edit-contact/src/main/java/chat/sphinx/edit_contact/navigation/EditContactNavigator.kt @@ -2,8 +2,11 @@ package chat.sphinx.edit_contact.navigation import androidx.navigation.NavController import chat.sphinx.contact.navigation.ContactNavigator +import chat.sphinx.wrapper_common.dashboard.ContactId import io.matthewnelson.concept_navigation.BaseNavigationDriver abstract class EditContactNavigator( detailNavigationDriver: BaseNavigationDriver -): ContactNavigator(detailNavigationDriver) \ No newline at end of file +): ContactNavigator(detailNavigationDriver) { + abstract suspend fun toSubscribeDetailScreen(contactId: ContactId) +} \ No newline at end of file diff --git a/sphinx/screens-detail/contact/edit-contact/src/main/java/chat/sphinx/edit_contact/ui/EditContactFragment.kt b/sphinx/screens-detail/contact/edit-contact/src/main/java/chat/sphinx/edit_contact/ui/EditContactFragment.kt index 49c8ee36e3..ffddf3cffb 100644 --- a/sphinx/screens-detail/contact/edit-contact/src/main/java/chat/sphinx/edit_contact/ui/EditContactFragment.kt +++ b/sphinx/screens-detail/contact/edit-contact/src/main/java/chat/sphinx/edit_contact/ui/EditContactFragment.kt @@ -1,14 +1,18 @@ package chat.sphinx.edit_contact.ui +import android.os.Bundle +import android.view.View import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope import by.kirich1409.viewbindingdelegate.viewBinding import chat.sphinx.concept_user_colors_helper.UserColorsHelper import chat.sphinx.contact.databinding.LayoutContactBinding +import chat.sphinx.contact.databinding.LayoutContactDetailScreenHeaderBinding import chat.sphinx.contact.ui.ContactFragment -import chat.sphinx.detail_resources.databinding.LayoutDetailScreenHeaderBinding import chat.sphinx.edit_contact.R import chat.sphinx.edit_contact.databinding.FragmentEditContactBinding import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch import javax.inject.Inject @AndroidEntryPoint @@ -27,8 +31,8 @@ internal class EditContactFragment : ContactFragment< override val viewModel: EditContactViewModel by viewModels() override val binding: FragmentEditContactBinding by viewBinding(FragmentEditContactBinding::bind) - override val headerBinding: LayoutDetailScreenHeaderBinding by viewBinding( - LayoutDetailScreenHeaderBinding::bind, R.id.include_edit_contact_header + override val headerBinding: LayoutContactDetailScreenHeaderBinding by viewBinding( + LayoutContactDetailScreenHeaderBinding::bind, R.id.include_edit_contact_header ) override val contactBinding: LayoutContactBinding by viewBinding( LayoutContactBinding::bind, R.id.include_edit_contact_layout @@ -37,4 +41,16 @@ internal class EditContactFragment : ContactFragment< override fun getHeaderText(): String = getString(R.string.edit_contact_header_name) override fun getSaveButtonText(): String = getString(R.string.save_contact_button) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + headerBinding.apply { + textViewDetailScreenSubscribe.setOnClickListener { + lifecycleScope.launch(viewModel.mainImmediate) { + viewModel.toSubscriptionDetailScreen() + } + } + } + } } diff --git a/sphinx/screens-detail/contact/edit-contact/src/main/java/chat/sphinx/edit_contact/ui/EditContactViewModel.kt b/sphinx/screens-detail/contact/edit-contact/src/main/java/chat/sphinx/edit_contact/ui/EditContactViewModel.kt index 16f0eeb201..c9f4451b83 100644 --- a/sphinx/screens-detail/contact/edit-contact/src/main/java/chat/sphinx/edit_contact/ui/EditContactViewModel.kt +++ b/sphinx/screens-detail/contact/edit-contact/src/main/java/chat/sphinx/edit_contact/ui/EditContactViewModel.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import chat.sphinx.concept_image_loader.ImageLoader import chat.sphinx.concept_repository_contact.ContactRepository +import chat.sphinx.concept_repository_subscription.SubscriptionRepository import chat.sphinx.concept_view_model_coordinator.ViewModelCoordinator import chat.sphinx.contact.ui.ContactSideEffect import chat.sphinx.contact.ui.ContactViewModel @@ -35,12 +36,14 @@ internal class EditContactViewModel @Inject constructor( app: Application, scannerCoordinator: ViewModelCoordinator, contactRepository: ContactRepository, + subscriptionRepository: SubscriptionRepository, imageLoader: ImageLoader, ): ContactViewModel( editContactNavigator, dispatchers, app, contactRepository, + subscriptionRepository, scannerCoordinator, imageLoader ) @@ -57,13 +60,19 @@ internal class EditContactViewModel @Inject constructor( contactRepository.getContactById(contactId).firstOrNull().let { contact -> if (contact != null) { contact.nodePubKey?.let { lightningNodePubKey -> + + val subscription = subscriptionRepository.getActiveSubscriptionByContactId( + contactId + ).firstOrNull() + submitSideEffect( ContactSideEffect.ExistingContact( contact.alias?.value, contact.photoUrl, contact.getColorKey(), lightningNodePubKey, - contact.routeHint + contact.routeHint, + subscription != null ) ) } @@ -72,6 +81,10 @@ internal class EditContactViewModel @Inject constructor( } } + suspend fun toSubscriptionDetailScreen() { + (navigator as EditContactNavigator).toSubscribeDetailScreen(contactId) + } + override fun saveContact( contactAlias: ContactAlias, lightningNodePubKey: LightningNodePubKey, diff --git a/sphinx/screens-detail/contact/edit-contact/src/main/res/layout/fragment_edit_contact.xml b/sphinx/screens-detail/contact/edit-contact/src/main/res/layout/fragment_edit_contact.xml index a0185460b5..7a8652e13a 100644 --- a/sphinx/screens-detail/contact/edit-contact/src/main/res/layout/fragment_edit_contact.xml +++ b/sphinx/screens-detail/contact/edit-contact/src/main/res/layout/fragment_edit_contact.xml @@ -10,7 +10,7 @@ diff --git a/sphinx/screens-detail/contact/new-contact/src/main/java/chat/sphinx/new_contact/ui/NewContactFragment.kt b/sphinx/screens-detail/contact/new-contact/src/main/java/chat/sphinx/new_contact/ui/NewContactFragment.kt index f71fa91398..fd0491e027 100644 --- a/sphinx/screens-detail/contact/new-contact/src/main/java/chat/sphinx/new_contact/ui/NewContactFragment.kt +++ b/sphinx/screens-detail/contact/new-contact/src/main/java/chat/sphinx/new_contact/ui/NewContactFragment.kt @@ -4,8 +4,8 @@ import androidx.fragment.app.viewModels import by.kirich1409.viewbindingdelegate.viewBinding import chat.sphinx.concept_user_colors_helper.UserColorsHelper import chat.sphinx.contact.databinding.LayoutContactBinding +import chat.sphinx.contact.databinding.LayoutContactDetailScreenHeaderBinding import chat.sphinx.contact.ui.ContactFragment -import chat.sphinx.detail_resources.databinding.LayoutDetailScreenHeaderBinding import chat.sphinx.new_contact.R import chat.sphinx.new_contact.databinding.FragmentNewContactBinding import dagger.hilt.android.AndroidEntryPoint @@ -28,9 +28,10 @@ internal class NewContactFragment: ContactFragment< override val viewModel: NewContactViewModel by viewModels() override val binding: FragmentNewContactBinding by viewBinding(FragmentNewContactBinding::bind) - override val headerBinding: LayoutDetailScreenHeaderBinding by viewBinding( - LayoutDetailScreenHeaderBinding::bind, R.id.include_new_contact_header + override val headerBinding: LayoutContactDetailScreenHeaderBinding by viewBinding( + LayoutContactDetailScreenHeaderBinding::bind, R.id.include_new_contact_header ) + override val contactBinding: LayoutContactBinding by viewBinding( LayoutContactBinding::bind, R.id.include_new_contact_layout ) diff --git a/sphinx/screens-detail/contact/new-contact/src/main/java/chat/sphinx/new_contact/ui/NewContactViewModel.kt b/sphinx/screens-detail/contact/new-contact/src/main/java/chat/sphinx/new_contact/ui/NewContactViewModel.kt index cd4afdf87c..99b0f07793 100644 --- a/sphinx/screens-detail/contact/new-contact/src/main/java/chat/sphinx/new_contact/ui/NewContactViewModel.kt +++ b/sphinx/screens-detail/contact/new-contact/src/main/java/chat/sphinx/new_contact/ui/NewContactViewModel.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope import chat.sphinx.concept_image_loader.ImageLoader import chat.sphinx.concept_repository_contact.ContactRepository +import chat.sphinx.concept_repository_subscription.SubscriptionRepository import chat.sphinx.concept_view_model_coordinator.ViewModelCoordinator import chat.sphinx.contact.ui.ContactSideEffect import chat.sphinx.contact.ui.ContactViewModel @@ -38,12 +39,14 @@ internal class NewContactViewModel @Inject constructor( app: Application, scannerCoordinator: ViewModelCoordinator, contactRepository: ContactRepository, + subscriptionRepository: SubscriptionRepository, imageLoader: ImageLoader ): ContactViewModel( newContactNavigator, dispatchers, app, contactRepository, + subscriptionRepository, scannerCoordinator, imageLoader, ) { diff --git a/sphinx/screens-detail/contact/new-contact/src/main/res/layout/fragment_new_contact.xml b/sphinx/screens-detail/contact/new-contact/src/main/res/layout/fragment_new_contact.xml index 499ca1ab22..7b7193ee3d 100644 --- a/sphinx/screens-detail/contact/new-contact/src/main/res/layout/fragment_new_contact.xml +++ b/sphinx/screens-detail/contact/new-contact/src/main/res/layout/fragment_new_contact.xml @@ -10,7 +10,7 @@ diff --git a/sphinx/screens-detail/subscription/subscription/.gitignore b/sphinx/screens-detail/subscription/subscription/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/sphinx/screens-detail/subscription/subscription/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/sphinx/screens-detail/subscription/subscription/build.gradle b/sphinx/screens-detail/subscription/subscription/build.gradle new file mode 100644 index 0000000000..48b9b2d099 --- /dev/null +++ b/sphinx/screens-detail/subscription/subscription/build.gradle @@ -0,0 +1,47 @@ +plugins { + id 'com.android.library' + id 'dagger.hilt.android.plugin' + id 'androidx.navigation.safeargs' + id 'kotlin-android' + id 'kotlin-kapt' +} + +android { + compileSdkVersion versions.compileSdk + buildToolsVersion versions.buildTools + + buildFeatures.viewBinding = true + defaultConfig { + minSdkVersion versions.minSdk + targetSdkVersion versions.compileSdk + versionCode VERSION_CODE.toInteger() + versionName VERSION_NAME + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + testInstrumentationRunnerArguments disableAnalytics: 'true' + consumerProguardFiles "consumer-rules.pro" + } + + buildTypes { + release { + minifyEnabled false + } + } +} + +dependencies { + implementation fileTree(dir: "libs", include: ["*.jar"]) + // KotlinAndroid + implementation project(path: ':android:features:android-feature-screens') + // Sphinx + implementation project(path: ':sphinx:activity:insetter-activity') + implementation project(path: ':sphinx:application:common:logger') + implementation project(path: ':sphinx:application:common:wrappers:wrapper-common') + implementation project(path: ':sphinx:screens-detail:common:detail-resources') + implementation project(path: ':sphinx:application:data:concepts:repositories:concept-repository-subscription') + + implementation deps.androidx.lifecycle.hilt + implementation deps.google.hilt + + kapt kaptDeps.google.hilt +} diff --git a/sphinx/screens-detail/subscription/subscription/consumer-rules.pro b/sphinx/screens-detail/subscription/subscription/consumer-rules.pro new file mode 100644 index 0000000000..e69de29bb2 diff --git a/sphinx/screens-detail/subscription/subscription/proguard-rules.pro b/sphinx/screens-detail/subscription/subscription/proguard-rules.pro new file mode 100644 index 0000000000..481bb43481 --- /dev/null +++ b/sphinx/screens-detail/subscription/subscription/proguard-rules.pro @@ -0,0 +1,21 @@ +# 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/sphinx/screens-detail/subscription/subscription/src/main/AndroidManifest.xml b/sphinx/screens-detail/subscription/subscription/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..8ee36966cf --- /dev/null +++ b/sphinx/screens-detail/subscription/subscription/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/sphinx/screens-detail/subscription/subscription/src/main/java/chat/sphinx/subscription/navigation/SubscriptionNavigator.kt b/sphinx/screens-detail/subscription/subscription/src/main/java/chat/sphinx/subscription/navigation/SubscriptionNavigator.kt new file mode 100644 index 0000000000..87c1590a0e --- /dev/null +++ b/sphinx/screens-detail/subscription/subscription/src/main/java/chat/sphinx/subscription/navigation/SubscriptionNavigator.kt @@ -0,0 +1,16 @@ +package chat.sphinx.subscription.navigation + +import androidx.navigation.NavController +import io.matthewnelson.android_feature_navigation.requests.PopBackStack +import io.matthewnelson.concept_navigation.BaseNavigationDriver +import io.matthewnelson.concept_navigation.Navigator + +abstract class SubscriptionNavigator( + detailNavigationDriver: BaseNavigationDriver +): Navigator(detailNavigationDriver) { + abstract suspend fun closeDetailScreen() + + suspend fun popBackStack() { + navigationDriver.submitNavigationRequest(PopBackStack()) + } +} \ No newline at end of file diff --git a/sphinx/screens-detail/subscription/subscription/src/main/java/chat/sphinx/subscription/navigation/ToSubscriptionDetail.kt b/sphinx/screens-detail/subscription/subscription/src/main/java/chat/sphinx/subscription/navigation/ToSubscriptionDetail.kt new file mode 100644 index 0000000000..64cb36f664 --- /dev/null +++ b/sphinx/screens-detail/subscription/subscription/src/main/java/chat/sphinx/subscription/navigation/ToSubscriptionDetail.kt @@ -0,0 +1,23 @@ +package chat.sphinx.subscription.navigation + +import androidx.navigation.NavController +import chat.sphinx.detail_resources.DetailNavOptions +import chat.sphinx.subscription.R +import chat.sphinx.subscription.ui.SubscriptionFragmentArgs +import chat.sphinx.wrapper_common.dashboard.ContactId +import io.matthewnelson.concept_navigation.NavigationRequest + +class ToSubscriptionDetail( + private val contactId: ContactId +): NavigationRequest() { + override fun navigate(controller: NavController) { + controller.navigate( + R.id.subscription_nav_graph, + SubscriptionFragmentArgs.Builder(contactId.value).build().toBundle(), + DetailNavOptions.default.apply { + setEnterAnim(io.matthewnelson.android_feature_navigation.R.anim.slide_in_left) + setPopExitAnim(io.matthewnelson.android_feature_navigation.R.anim.slide_out_right) + }.build() + ) + } +} diff --git a/sphinx/screens-detail/subscription/subscription/src/main/java/chat/sphinx/subscription/ui/SavingSubscriptionViewState.kt b/sphinx/screens-detail/subscription/subscription/src/main/java/chat/sphinx/subscription/ui/SavingSubscriptionViewState.kt new file mode 100644 index 0000000000..41ab7cd879 --- /dev/null +++ b/sphinx/screens-detail/subscription/subscription/src/main/java/chat/sphinx/subscription/ui/SavingSubscriptionViewState.kt @@ -0,0 +1,11 @@ +package chat.sphinx.subscription.ui + +import io.matthewnelson.concept_views.viewstate.ViewState + +internal sealed class SavingSubscriptionViewState: ViewState() { + object Idle: SavingSubscriptionViewState() + + object SavingSubscription: SavingSubscriptionViewState() + + object SavingSubscriptionFailed: SavingSubscriptionViewState() +} \ No newline at end of file diff --git a/sphinx/screens-detail/subscription/subscription/src/main/java/chat/sphinx/subscription/ui/SubscriptionFragment.kt b/sphinx/screens-detail/subscription/subscription/src/main/java/chat/sphinx/subscription/ui/SubscriptionFragment.kt new file mode 100644 index 0000000000..1d5c881c2d --- /dev/null +++ b/sphinx/screens-detail/subscription/subscription/src/main/java/chat/sphinx/subscription/ui/SubscriptionFragment.kt @@ -0,0 +1,270 @@ +package chat.sphinx.subscription.ui + +import android.app.DatePickerDialog +import android.content.Context +import android.os.Bundle +import android.view.View +import androidx.core.content.ContextCompat +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import by.kirich1409.viewbindingdelegate.viewBinding +import chat.sphinx.insetter_activity.InsetterActivity +import chat.sphinx.insetter_activity.addNavigationBarPadding +import chat.sphinx.subscription.R +import chat.sphinx.subscription.databinding.FragmentSubscriptionBinding +import chat.sphinx.wrapper_common.DateTime +import chat.sphinx.wrapper_common.eeemmddhmma +import chat.sphinx.wrapper_common.lightning.Sat +import chat.sphinx.wrapper_common.lightning.asFormattedString +import chat.sphinx.wrapper_common.lightning.toSat +import chat.sphinx.wrapper_common.toDateTime +import chat.sphinx.wrapper_common.toDateTimeWithFormat +import dagger.hilt.android.AndroidEntryPoint +import io.matthewnelson.android_feature_screens.ui.sideeffect.SideEffectFragment +import io.matthewnelson.android_feature_screens.util.gone +import io.matthewnelson.android_feature_screens.util.visible +import io.matthewnelson.concept_views.viewstate.collect +import kotlinx.coroutines.launch +import java.text.SimpleDateFormat +import java.util.* +import javax.annotation.meta.Exhaustive + +@AndroidEntryPoint +internal class SubscriptionFragment: SideEffectFragment< + Context, + SubscriptionSideEffect, + SubscriptionViewState, + SubscriptionViewModel, + FragmentSubscriptionBinding + >(R.layout.fragment_subscription) +{ + override val viewModel: SubscriptionViewModel by viewModels() + override val binding: FragmentSubscriptionBinding by viewBinding(FragmentSubscriptionBinding::bind) + + private val calendar = Calendar.getInstance() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + calendar.timeZone = TimeZone.getTimeZone(DateTime.UTC) + + binding.apply { + + textViewDetailScreenHeaderName.text = getString(R.string.subscription_header_name) + + textViewDetailScreenHeaderNavBack.apply navBack@ { + this@navBack.visible + this@navBack.setOnClickListener { + lifecycleScope.launch { viewModel.navigator.popBackStack() } + } + } + + textViewDetailSubscriptionDelete.setOnClickListener { + viewModel.deleteSubscription() + } + + switchSubscriptionEnablement.setOnClickListener { + // Toggle checked status + switchSubscriptionEnablement.isChecked = !switchSubscriptionEnablement.isChecked + + if (switchSubscriptionEnablement.isChecked) { + // We are about to pause the subscription ask for confirmation + viewModel.pauseSubscription() + } else { + // We should restart the subscription + viewModel.restartSubscription() + } + } + + editTextSubscriptionPayUntil.setOnClickListener { + val datePickerDialog = DatePickerDialog( + root.context, + { _, year, month, dayOfMonth -> + calendar.set(Calendar.YEAR, year) + calendar.set(Calendar.MONTH, month) + calendar.set(Calendar.DAY_OF_MONTH, dayOfMonth) + + editTextSubscriptionPayUntil.setText( + DateTime.getFormatMMMddyyyy( + TimeZone.getTimeZone(DateTime.UTC) + ).format(calendar.time) + ) + }, + calendar.get(Calendar.YEAR), + calendar.get(Calendar.MONTH), + calendar.get(Calendar.DAY_OF_MONTH), + ) + datePickerDialog.datePicker.minDate = Date().time + datePickerDialog.show() + } + + radioGroupSubscriptionAmount.setOnCheckedChangeListenerWithInputInteraction( + radioButtonSubscriptionAmountCustom, editTextSubscriptionCustomAmount + ) + + radioGroupSubscriptionEndRule.setOnCheckedChangeListenerWithInputInteraction( + radioButtonSubscriptionMakeQuantity, editTextSubscriptionMakeQuantity + ) + + radioGroupSubscriptionEndRule.setOnCheckedChangeListenerWithDatePickerInputInteraction( + radioButtonSubscriptionPayUntil, editTextSubscriptionPayUntil + ) + + buttonSubscriptionSave.setOnClickListener { + + val amount: Sat? = when (radioGroupSubscriptionAmount.checkedRadioButtonId) { + R.id.radio_button_subscription_amount_500_sats -> { + Sat(500) + } + R.id.radio_button_subscription_amount_1000_sats -> { + Sat(1000) + } + R.id.radio_button_subscription_amount_2000_sats -> { + Sat(2000) + } + R.id.radio_button_subscription_amount_custom -> { + editTextSubscriptionCustomAmount.text?.toString()?.toLongOrNull()?.let { + Sat(it) + } + } + else -> null + } + + val cron: String? = when (radioGroupSubscriptionTimeInterval.checkedRadioButtonId) { + R.id.radio_button_subscription_daily -> { + SubscriptionViewModel.DAILY_INTERVAL + } + R.id.radio_button_subscription_weekly -> { + SubscriptionViewModel.WEEKLY_INTERVAL + } + R.id.radio_button_subscription_monthly -> { + SubscriptionViewModel.MONTHLY_INTERVAL + } + else -> null + } + + var endNumber: Long? = null + val endDate: DateTime? = when (radioGroupSubscriptionEndRule.checkedRadioButtonId) { + R.id.radio_button_subscription_make_quantity -> { + editTextSubscriptionMakeQuantity.text?.toString()?.let { + endNumber = it.toLongOrNull() + } + null + } + R.id.radio_button_subscription_pay_until -> { + calendar.timeInMillis.toDateTime() + } + else -> null + } + + viewModel.saveSubscription( + amount, + cron, + endDate, + endNumber + ) + } + (requireActivity() as InsetterActivity).addNavigationBarPadding(layoutConstraintSubscription) + } + } + + override suspend fun onViewStateFlowCollect(viewState: SubscriptionViewState) { + when(viewState) { + is SubscriptionViewState.Idle -> { + // Setup for new subscription + binding.apply { + progressBarSubscriptionSave.gone + textViewDetailSubscriptionDelete.gone + layoutConstraintSubscriptionEnablement.gone + buttonSubscriptionSave.text = getString(R.string.subscribe) + } + } + is SubscriptionViewState.SubscriptionLoaded -> { + binding.apply { + progressBarSubscriptionSave.gone + textViewDetailSubscriptionDelete.visible + layoutConstraintSubscriptionEnablement.visible + buttonSubscriptionSave.text = getString(R.string.update_subscription) + + switchSubscriptionEnablement.isChecked = viewState.isActive + + when (viewState.amount) { + 500L -> { + radioButtonSubscriptionAmount500Sats.isChecked = true + } + 1000L -> { + radioButtonSubscriptionAmount1000Sats.isChecked = true + } + 2000L -> { + radioButtonSubscriptionAmount2000Sats.isChecked = true + } + else -> { + radioButtonSubscriptionAmountCustom.isChecked = true + + editTextSubscriptionCustomAmount.setText( + viewState.amount.toString() + ) + } + } + + when (viewState.timeInterval) { + SubscriptionViewModel.DAILY_INTERVAL -> { + radioButtonSubscriptionDaily.isChecked = true + } + SubscriptionViewModel.MONTHLY_INTERVAL -> { + radioButtonSubscriptionMonthly.isChecked = true + } + SubscriptionViewModel.WEEKLY_INTERVAL -> { + radioButtonSubscriptionWeekly.isChecked = true + } + } + + // Populate End Rule + when { + viewState.endNumber != null -> { + radioButtonSubscriptionMakeQuantity.isChecked = true + + editTextSubscriptionMakeQuantity.setText( + viewState.endNumber!!.toString() + ) + } + viewState.endDate != null -> { + radioButtonSubscriptionPayUntil.isChecked = true + + editTextSubscriptionPayUntil.setText( + DateTime.getFormatMMMddyyyy( + TimeZone.getTimeZone(DateTime.UTC) + ).format(viewState.endDate!!.value) + ) + + calendar.time = viewState.endDate!!.value + } + } + } + } + } + } + + override fun subscribeToViewStateFlow() { + super.subscribeToViewStateFlow() + + onStopSupervisor.scope.launch(viewModel.mainImmediate) { + viewModel.savingSubscriptionViewStateContainer.collect { viewState -> + @Exhaustive + when (viewState) { + is SavingSubscriptionViewState.Idle -> {} + is SavingSubscriptionViewState.SavingSubscription -> { + binding.progressBarSubscriptionSave.visible + } + is SavingSubscriptionViewState.SavingSubscriptionFailed -> { + binding.progressBarSubscriptionSave.gone + } + } + } + } + } + + override suspend fun onSideEffectCollect(sideEffect: SubscriptionSideEffect) { + sideEffect.execute(requireActivity()) + } +} diff --git a/sphinx/screens-detail/subscription/subscription/src/main/java/chat/sphinx/subscription/ui/SubscriptionSideEffect.kt b/sphinx/screens-detail/subscription/subscription/src/main/java/chat/sphinx/subscription/ui/SubscriptionSideEffect.kt new file mode 100644 index 0000000000..333e1b68d4 --- /dev/null +++ b/sphinx/screens-detail/subscription/subscription/src/main/java/chat/sphinx/subscription/ui/SubscriptionSideEffect.kt @@ -0,0 +1,35 @@ +package chat.sphinx.subscription.ui + +import android.app.AlertDialog +import android.content.Context +import chat.sphinx.resources.SphinxToastUtils +import chat.sphinx.subscription.R +import io.matthewnelson.android_feature_toast_utils.show +import io.matthewnelson.concept_views.sideeffect.SideEffect + +internal sealed class SubscriptionSideEffect: SideEffect() { + class Notify( + private val msg: String, + private val notificationLengthLong: Boolean = true + ): SubscriptionSideEffect() { + override suspend fun execute(value: Context) { + SphinxToastUtils(toastLengthLong = notificationLengthLong).show(value, msg) + } + } + + class AlertConfirmDeleteSubscription( + private val callback: () -> Unit + ): SubscriptionSideEffect() { + + override suspend fun execute(value: Context) { + val builder = AlertDialog.Builder(value) + builder.setTitle(value.getString(R.string.delete_subscription)) + builder.setMessage(value.getString(R.string.are_you_sure_you_want_to_delete_subscription)) + builder.setNegativeButton(android.R.string.cancel) { _,_ -> } + builder.setPositiveButton(android.R.string.ok) { _, _ -> + callback() + } + builder.show() + } + } +} diff --git a/sphinx/screens-detail/subscription/subscription/src/main/java/chat/sphinx/subscription/ui/SubscriptionViewModel.kt b/sphinx/screens-detail/subscription/subscription/src/main/java/chat/sphinx/subscription/ui/SubscriptionViewModel.kt new file mode 100644 index 0000000000..044a1e99ea --- /dev/null +++ b/sphinx/screens-detail/subscription/subscription/src/main/java/chat/sphinx/subscription/ui/SubscriptionViewModel.kt @@ -0,0 +1,241 @@ +package chat.sphinx.subscription.ui + +import android.app.Application +import android.content.Context +import androidx.lifecycle.SavedStateHandle +import androidx.lifecycle.viewModelScope +import chat.sphinx.concept_repository_subscription.SubscriptionRepository +import chat.sphinx.kotlin_response.Response +import chat.sphinx.subscription.R +import chat.sphinx.subscription.navigation.SubscriptionNavigator +import chat.sphinx.wrapper_common.DateTime +import chat.sphinx.wrapper_common.dashboard.ContactId +import chat.sphinx.wrapper_common.lightning.Sat +import chat.sphinx.wrapper_common.subscription.EndNumber +import dagger.hilt.android.lifecycle.HiltViewModel +import io.matthewnelson.android_feature_navigation.util.navArgs +import io.matthewnelson.android_feature_viewmodel.SideEffectViewModel +import io.matthewnelson.android_feature_viewmodel.submitSideEffect +import io.matthewnelson.concept_coroutines.CoroutineDispatchers +import io.matthewnelson.concept_views.viewstate.ViewStateContainer +import kotlinx.coroutines.flow.* +import kotlinx.coroutines.launch +import java.util.* +import javax.inject.Inject + +@HiltViewModel +internal class SubscriptionViewModel @Inject constructor( + val app: Application, + dispatchers: CoroutineDispatchers, + savedStateHandle: SavedStateHandle, + private val subscriptionRepository: SubscriptionRepository, + val navigator: SubscriptionNavigator +): SideEffectViewModel< + Context, + SubscriptionSideEffect, + SubscriptionViewState + >(dispatchers, SubscriptionViewState.Idle) +{ + private val args: SubscriptionFragmentArgs by savedStateHandle.navArgs() + + companion object { + const val DAILY_INTERVAL: String = "daily" + const val WEEKLY_INTERVAL: String = "weekly" + const val MONTHLY_INTERVAL: String = "MONTHLY" + } + + private inner class SubscriptionViewStateContainer: ViewStateContainer(SubscriptionViewState.Idle) { + override val viewStateFlow: StateFlow by lazy { + flow { + subscriptionRepository.getActiveSubscriptionByContactId(ContactId(args.argContactId)).collect { subscription -> + emit( + if (subscription != null) { + val timeInterval = if (subscription.cron.value.endsWith("* * *")) { + DAILY_INTERVAL + } else if (subscription.cron.value.endsWith("* *")) { + MONTHLY_INTERVAL + } else { + WEEKLY_INTERVAL + } + + SubscriptionViewState.SubscriptionLoaded( + isActive = !subscription.paused, + amount = subscription.amount.value, + timeInterval = timeInterval, + endNumber = subscription.endNumber?.value, + endDate = subscription.endDate + ) + } else { + SubscriptionViewState.Idle + } + ) + } + }.stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5_000), + SubscriptionViewState.Idle, + ) + } + } + + override val viewStateContainer: ViewStateContainer by lazy { + SubscriptionViewStateContainer() + } + + val savingSubscriptionViewStateContainer: ViewStateContainer by lazy { + ViewStateContainer(SavingSubscriptionViewState.Idle) + } + + fun saveSubscription( + amount: Sat?, + interval: String?, + endDate: DateTime?, + endNumber: Long? + ) { + viewModelScope.launch(mainImmediate) { + + if (amount == null) { + submitSideEffect( + SubscriptionSideEffect.Notify( + app.getString(R.string.amount_is_required) + ) + ) + return@launch + } + + if (interval == null) { + submitSideEffect( + SubscriptionSideEffect.Notify( + app.getString(R.string.time_interval_is_required) + ) + ) + return@launch + } + + if (endNumber == null && endDate == null) { + submitSideEffect( + SubscriptionSideEffect.Notify( + app.getString(R.string.please_set_either_the_number_of_payments_to_make_or_end_date) + ) + ) + return@launch + } + + savingSubscriptionViewStateContainer.updateViewState( + SavingSubscriptionViewState.SavingSubscription + ) + + subscriptionRepository.getActiveSubscriptionByContactId( + ContactId(args.argContactId) + ).firstOrNull().let { subscription -> + val loadResponse = if (subscription == null) { + subscriptionRepository.createSubscription( + amount = amount, + interval = interval, + contactId = ContactId(args.argContactId), + chatId = null, + endDate = endDate?.let { DateTime.getFormatMMMddyyyy(TimeZone.getTimeZone("UTC")).format(it.value) }, + endNumber = endNumber?.let { EndNumber(it) } + ) + } else { + subscriptionRepository.updateSubscription( + id = subscription.id, + amount = amount, + interval = interval, + contactId = ContactId(args.argContactId), + chatId = subscription.chatId, + endDate = endDate?.let { DateTime.getFormatMMMddyyyy(TimeZone.getTimeZone("UTC")).format(it.value) }, + endNumber = endNumber?.let { EndNumber(it) } + ) + } + + when (loadResponse) { + is Response.Error -> { + submitSideEffect( + SubscriptionSideEffect.Notify(app.getString(R.string.failed_to_save_subscription)) + ) + savingSubscriptionViewStateContainer.updateViewState( + SavingSubscriptionViewState.SavingSubscriptionFailed + ) + } + is Response.Success -> { + navigator.popBackStack() + } + } + } + } + } + + fun deleteSubscription() { + viewModelScope.launch(mainImmediate) { + submitSideEffect( + SubscriptionSideEffect.AlertConfirmDeleteSubscription() { + viewModelScope.launch(mainImmediate) { + subscriptionRepository.getActiveSubscriptionByContactId(ContactId(args.argContactId)).firstOrNull().let { subscription -> + if (subscription == null) { + navigator.popBackStack() + } else { + when(subscriptionRepository.deleteSubscription(subscription.id)) { + is Response.Error -> { + submitSideEffect( + SubscriptionSideEffect.Notify(app.getString(R.string.failed_to_delete_subscription)) + ) + } + is Response.Success -> { + navigator.popBackStack() + } + } + } + } + } + } + ) + } + } + + fun pauseSubscription() { + viewModelScope.launch(mainImmediate) { + subscriptionRepository.getActiveSubscriptionByContactId(ContactId(args.argContactId)).firstOrNull().let { subscription -> + if (subscription == null) { + submitSideEffect( + SubscriptionSideEffect.Notify(app.getString(R.string.failed_to_pause_subscription)) + ) + } else { + when (subscriptionRepository.pauseSubscription(subscription.id)) { + is Response.Error -> { + submitSideEffect( + SubscriptionSideEffect.Notify(app.getString(R.string.failed_to_pause_subscription)) + ) + } + is Response.Success -> { + submitSideEffect( + SubscriptionSideEffect.Notify(app.getString(R.string.successfully_paused_subscription)) + ) + } + } + } + } + } + } + + fun restartSubscription() { + viewModelScope.launch(mainImmediate) { + subscriptionRepository.getActiveSubscriptionByContactId(ContactId(args.argContactId)).firstOrNull().let { subscription -> + if (subscription == null) { + submitSideEffect( + SubscriptionSideEffect.Notify(app.getString(R.string.failed_to_restart_subscription)) + ) + } else { + when (subscriptionRepository.restartSubscription(subscription.id)) { + is Response.Error -> { + submitSideEffect( + SubscriptionSideEffect.Notify(app.getString(R.string.failed_to_restart_subscription)) + ) + } + is Response.Success -> {} + } + } + } + } + } +} diff --git a/sphinx/screens-detail/subscription/subscription/src/main/java/chat/sphinx/subscription/ui/SubscriptionViewState.kt b/sphinx/screens-detail/subscription/subscription/src/main/java/chat/sphinx/subscription/ui/SubscriptionViewState.kt new file mode 100644 index 0000000000..2ee8c7607c --- /dev/null +++ b/sphinx/screens-detail/subscription/subscription/src/main/java/chat/sphinx/subscription/ui/SubscriptionViewState.kt @@ -0,0 +1,17 @@ +package chat.sphinx.subscription.ui + +import chat.sphinx.wrapper_common.DateTime +import chat.sphinx.wrapper_subscription.Subscription +import io.matthewnelson.concept_views.viewstate.ViewState + +internal sealed class SubscriptionViewState: ViewState() { + object Idle: SubscriptionViewState() + + class SubscriptionLoaded( + val isActive: Boolean, + val amount: Long, + val timeInterval: String, + val endNumber: Long?, + val endDate: DateTime?, + ) : SubscriptionViewState() +} diff --git a/sphinx/screens-detail/subscription/subscription/src/main/java/chat/sphinx/subscription/ui/widgets/SphinxRadioGroup.kt b/sphinx/screens-detail/subscription/subscription/src/main/java/chat/sphinx/subscription/ui/widgets/SphinxRadioGroup.kt new file mode 100644 index 0000000000..b7260c4afa --- /dev/null +++ b/sphinx/screens-detail/subscription/subscription/src/main/java/chat/sphinx/subscription/ui/widgets/SphinxRadioGroup.kt @@ -0,0 +1,339 @@ +package chat.sphinx.subscription.ui.widgets + +import android.content.Context +import android.text.TextUtils +import android.util.AttributeSet +import android.view.View +import android.view.ViewGroup +import android.view.accessibility.AccessibilityNodeInfo +import android.view.inputmethod.InputMethodManager +import android.widget.CompoundButton +import android.widget.EditText +import android.widget.RadioButton +import androidx.annotation.IdRes +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.content.ContextCompat +import chat.sphinx.subscription.ui.widgets.SphinxRadioGroup + +class SphinxRadioGroup @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, + defStyleRes: Int = 0 +) : ConstraintLayout(context, attrs, defStyleAttr, defStyleRes) { + /** + * + * Returns the identifier of the selected radio button in this group. + * Upon empty selection, the returned value is -1. + * + * @return the unique id of the selected radio button in this group + * + * @see .check + * @see .clearCheck + * @attr ref android.R.styleable#RadioGroup_checkedButton + */ + // holds the checked id; the selection is empty by default + @get:IdRes + var checkedRadioButtonId = -1 + private set + + // tracks children radio buttons checked state + private var mChildOnCheckedChangeListener: CompoundButton.OnCheckedChangeListener? = null + + // when true, mOnCheckedChangeListener discards events + private var mProtectFromCheckedChange = false + private var mOnCheckedChangeListener: OnCheckedChangeListener? = null + private var mPassThroughListener: PassThroughHierarchyChangeListener? = null + + // Indicates whether the child was set from resources or dynamically, so it can be used + // to sanitize autofill requests. + private fun init() { + mChildOnCheckedChangeListener = CheckedStateTracker() + mPassThroughListener = PassThroughHierarchyChangeListener() + super.setOnHierarchyChangeListener(mPassThroughListener) + } + + /** + * {@inheritDoc} + */ + override fun setOnHierarchyChangeListener(listener: OnHierarchyChangeListener) { + // the user listener is delegated to our pass-through listener + mPassThroughListener!!.mOnHierarchyChangeListener = listener + } + + /** + * {@inheritDoc} + */ + override fun onFinishInflate() { + super.onFinishInflate() + + // checks the appropriate radio button as requested in the XML file + if (checkedRadioButtonId != -1) { + mProtectFromCheckedChange = true + setCheckedStateForView(checkedRadioButtonId, true) + mProtectFromCheckedChange = false + setCheckedId(checkedRadioButtonId) + } + } + + override fun addView(child: View, index: Int, params: ViewGroup.LayoutParams) { + if (child is RadioButton) { + val button = child + if (button.isChecked) { + mProtectFromCheckedChange = true + if (checkedRadioButtonId != -1) { + setCheckedStateForView(checkedRadioButtonId, false) + } + mProtectFromCheckedChange = false + setCheckedId(button.id) + } + } + super.addView(child, index, params) + } + + /** + * + * Sets the selection to the radio button whose identifier is passed in + * parameter. Using -1 as the selection identifier clears the selection; + * such an operation is equivalent to invoking [.clearCheck]. + * + * @param id the unique id of the radio button to select in this group + * + * @see .getCheckedRadioButtonId + * @see .clearCheck + */ + fun check(@IdRes id: Int) { + // don't even bother + if (id != -1 && id == checkedRadioButtonId) { + return + } + if (checkedRadioButtonId != -1) { + setCheckedStateForView(checkedRadioButtonId, false) + } + if (id != -1) { + setCheckedStateForView(id, true) + } + setCheckedId(id) + } + + private fun setCheckedId(@IdRes id: Int) { + checkedRadioButtonId = id + if (mOnCheckedChangeListener != null) { + mOnCheckedChangeListener!!.onCheckedChanged(this, checkedRadioButtonId) + } + } + + private fun setCheckedStateForView(viewId: Int, checked: Boolean) { + val checkedView = findViewById(viewId) + if (checkedView != null && checkedView is RadioButton) { + checkedView.isChecked = checked + } + } + + /** + * + * Clears the selection. When the selection is cleared, no radio button + * in this group is selected and [.getCheckedRadioButtonId] returns + * null. + * + * @see .check + * @see .getCheckedRadioButtonId + */ + fun clearCheck() { + check(-1) + } + + /** + * + * Register a callback to be invoked when the checked radio button + * changes in this group. + * + * @param listener the callback to call on checked state change + */ + fun setOnCheckedChangeListener(listener: OnCheckedChangeListener?) { + mOnCheckedChangeListener = listener + } + + override fun getAccessibilityClassName(): CharSequence { + return this::class.java.name + } + + /** + * + * Interface definition for a callback to be invoked when the checked + * radio button changed in this group. + */ + interface OnCheckedChangeListener { + /** + * + * Called when the checked radio button has changed. When the + * selection is cleared, checkedId is -1. + * + * @param group the group in which the checked radio button has changed + * @param checkedId the unique identifier of the newly checked radio button + */ + fun onCheckedChanged(group: SphinxRadioGroup?, @IdRes checkedId: Int) + } + + private abstract inner class AbstractCheckedStateTracker : CompoundButton.OnCheckedChangeListener { + override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) { + // prevents from infinite recursion + if (mProtectFromCheckedChange) { + return + } + mProtectFromCheckedChange = true + if (checkedRadioButtonId != -1) { + setCheckedStateForView(checkedRadioButtonId, false) + } + mProtectFromCheckedChange = false + val id = buttonView.id + setCheckedId(id) + } + } + + private inner class CheckedStateTracker : AbstractCheckedStateTracker(){ + override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) { + super.onCheckedChanged(buttonView, isChecked) + + if (isChecked) { + // Hide soft keyboard + context?.let { + val inputMethodManager = ContextCompat.getSystemService(it, InputMethodManager::class.java) + inputMethodManager?.hideSoftInputFromWindow(buttonView.windowToken, 0) + } + } + } + } + + private inner class CheckedStateWithEditTextTracker(val editText: EditText) : AbstractCheckedStateTracker() { + override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) { + super.onCheckedChanged(buttonView, isChecked) + + editText.isEnabled = isChecked + + if (editText.isEnabled && buttonView.isPressed) { + editText.requestFocus() + context?.let { + val inputMethodManager = ContextCompat.getSystemService(it, InputMethodManager::class.java) + inputMethodManager?.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT) + } + } else { + editText.setText("") + } + } + } + + private inner class CheckedStateWithDatePickerTracker(val editText: EditText) : AbstractCheckedStateTracker() { + override fun onCheckedChanged(buttonView: CompoundButton, isChecked: Boolean) { + super.onCheckedChanged(buttonView, isChecked) + + editText.isEnabled = isChecked + + if (editText.isEnabled && buttonView.isPressed) { + editText.requestFocus() + editText.callOnClick() + } else { + editText.setText("") + } + } + } + + /** + * + * A pass-through listener acts upon the events and dispatches them + * to another listener. This allows the table layout to set its own internal + * hierarchy change listener without preventing the user to setup his. + */ + private inner class PassThroughHierarchyChangeListener : OnHierarchyChangeListener { + var mOnHierarchyChangeListener: OnHierarchyChangeListener? = null + + /** + * {@inheritDoc} + */ + override fun onChildViewAdded(parent: View, child: View) { + if (parent === this@SphinxRadioGroup && child is RadioButton) { + var id = child.getId() + // generates an id if it's missing + if (id == NO_ID) { + id = generateViewId() + child.setId(id) + } + child.setOnCheckedChangeListener( + mChildOnCheckedChangeListener + ) + } + mOnHierarchyChangeListener?.onChildViewAdded(parent, child) + } + + /** + * {@inheritDoc} + */ + override fun onChildViewRemoved(parent: View, child: View) { + if (parent === this@SphinxRadioGroup && child is RadioButton) { + child.setOnCheckedChangeListener(null) + } + mOnHierarchyChangeListener?.onChildViewRemoved(parent, child) + } + } + + override fun onInitializeAccessibilityNodeInfo(info: AccessibilityNodeInfo) { + super.onInitializeAccessibilityNodeInfo(info) + info.collectionInfo = AccessibilityNodeInfo.CollectionInfo.obtain( + visibleChildWithTextCount, + 1, false, + AccessibilityNodeInfo.CollectionInfo.SELECTION_MODE_SINGLE + ) + } + + private val visibleChildWithTextCount: Int + get() { + var count = 0 + for (i in 0 until childCount) { + if (getChildAt(i) is RadioButton) { + if (isVisibleWithText(getChildAt(i) as RadioButton)) { + count++ + } + } + } + return count + } + + fun getIndexWithinVisibleButtons(child: View?): Int { + if (child !is RadioButton) { + return -1 + } + var index = 0 + for (i in 0 until childCount) { + if (getChildAt(i) is RadioButton) { + val button = getChildAt(i) as RadioButton + if (button === child) { + return index + } + if (isVisibleWithText(button)) { + index++ + } + } + } + return -1 + } + + private fun isVisibleWithText(button: RadioButton): Boolean { + return button.visibility == VISIBLE && !TextUtils.isEmpty(button.text) + } + + /** + * {@inheritDoc} + */ + init { + importantForAccessibility = IMPORTANT_FOR_ACCESSIBILITY_YES + init() + } + + fun setOnCheckedChangeListenerWithInputInteraction(radioButton: RadioButton, editText: EditText) { + radioButton.setOnCheckedChangeListener(CheckedStateWithEditTextTracker(editText)) + } + + fun setOnCheckedChangeListenerWithDatePickerInputInteraction(radioButton: RadioButton, editText: EditText) { + radioButton.setOnCheckedChangeListener(CheckedStateWithDatePickerTracker(editText)) + } +} \ No newline at end of file diff --git a/sphinx/screens-detail/subscription/subscription/src/main/res/drawable/edit_text_background.xml b/sphinx/screens-detail/subscription/subscription/src/main/res/drawable/edit_text_background.xml new file mode 100644 index 0000000000..c9f27d6a6f --- /dev/null +++ b/sphinx/screens-detail/subscription/subscription/src/main/res/drawable/edit_text_background.xml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/sphinx/screens-detail/subscription/subscription/src/main/res/drawable/ic_calendar.xml b/sphinx/screens-detail/subscription/subscription/src/main/res/drawable/ic_calendar.xml new file mode 100644 index 0000000000..1cf969c70f --- /dev/null +++ b/sphinx/screens-detail/subscription/subscription/src/main/res/drawable/ic_calendar.xml @@ -0,0 +1,10 @@ + + + diff --git a/sphinx/screens-detail/subscription/subscription/src/main/res/layout/fragment_subscription.xml b/sphinx/screens-detail/subscription/subscription/src/main/res/layout/fragment_subscription.xml new file mode 100644 index 0000000000..aa7e3b1c30 --- /dev/null +++ b/sphinx/screens-detail/subscription/subscription/src/main/res/layout/fragment_subscription.xml @@ -0,0 +1,509 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/sphinx/screens-detail/subscription/subscription/src/main/res/navigation/subscription_nav_graph.xml b/sphinx/screens-detail/subscription/subscription/src/main/res/navigation/subscription_nav_graph.xml new file mode 100644 index 0000000000..e57a2932a8 --- /dev/null +++ b/sphinx/screens-detail/subscription/subscription/src/main/res/navigation/subscription_nav_graph.xml @@ -0,0 +1,17 @@ + + + + + + + + diff --git a/sphinx/screens-detail/subscription/subscription/src/main/res/values-es/strings.xml b/sphinx/screens-detail/subscription/subscription/src/main/res/values-es/strings.xml new file mode 100644 index 0000000000..3f6b17dd92 --- /dev/null +++ b/sphinx/screens-detail/subscription/subscription/src/main/res/values-es/strings.xml @@ -0,0 +1,32 @@ + + + PAGOS RECURRENTES + Monto + Monto: + 500 sats + 1000 sats + 2000 sats + INTERVALO DE TIEMPO + Diariament + Semanalmente + Mensualmente + FINALIZACIÓN + Realizar + Pagos + Pagar hasta + Eliminar suscripción + ¿Está seguro de que desea eliminar esta suscripción? + Monto requerido + Intervalo de tiempo requerido + Establezca la cantidad de pagos a realizar o la fecha de finalización + No se pudo guardar la suscripción + Suscripción guardada correctamente + Eliminando suscripción + No se pudo pausar la suscripción + Suscripción pausada con éxito + No se pudo reiniciar la suscripción + Suscripción reiniciada correctamente + Suscribir + Actualizar + No se pudo borrar la suscripción + \ No newline at end of file diff --git a/sphinx/screens-detail/subscription/subscription/src/main/res/values-ja/strings.xml b/sphinx/screens-detail/subscription/subscription/src/main/res/values-ja/strings.xml new file mode 100644 index 0000000000..c69f68a5ca --- /dev/null +++ b/sphinx/screens-detail/subscription/subscription/src/main/res/values-ja/strings.xml @@ -0,0 +1,32 @@ + + + 繰り返し + + カスタム金額: + 500 sats + 1000 sats + 2000 sats + 時間間隔 + 毎日 + 毎週 + 毎月 + エンドルール + 作る + 支払い + まで支払う + サブスクリプションを削除する + このサブスクリプションを削除してもよろしいですか + 金額が必要です + 時間間隔が必要です + お支払い回数または終了日を設定してください + サブスクリプションの保存に失敗しました + サブスクリプションを正常に保存しました + サブスクリプションの削除 + サブスクリプションの一時停止に失敗しました + サブスクリプションを正常に一時停止しました + サブスクリプションの再開に失敗しました + サブスクリプションが正常に再開されました + 申し込む + サブスクリプションの更新 + サブスクリプションの削除に失敗しました + \ No newline at end of file diff --git a/sphinx/screens-detail/subscription/subscription/src/main/res/values-zh/strings.xml b/sphinx/screens-detail/subscription/subscription/src/main/res/values-zh/strings.xml new file mode 100644 index 0000000000..e311a216ae --- /dev/null +++ b/sphinx/screens-detail/subscription/subscription/src/main/res/values-zh/strings.xml @@ -0,0 +1,32 @@ + + + 再次發生的 + 數量 + 自定義金額: + 500 sats + 1000 sats + 2000 sats + 時間間隔 + 日常的 + 每週 + 每月 + 結束規則 + 製作 + 付款 + 支付至 + 刪除訂閱 + 您確定要刪除此訂閱嗎 + 金額為必填項 + 需要時間間隔 + 請設置付款次數或結束日期 + 無法保存訂閱 + 已成功保存訂閱 + 刪除訂閱 + 未能暫停訂閱 + 已成功暫停訂閱 + 重新開始訂閱失敗 + 成功重啟訂閱 + 訂閱 + 更新訂閱 + 刪除訂閱失敗 + \ No newline at end of file diff --git a/sphinx/screens-detail/subscription/subscription/src/main/res/values/dimens.xml b/sphinx/screens-detail/subscription/subscription/src/main/res/values/dimens.xml new file mode 100644 index 0000000000..45579eba67 --- /dev/null +++ b/sphinx/screens-detail/subscription/subscription/src/main/res/values/dimens.xml @@ -0,0 +1,9 @@ + + + 36dp + 25dp + 25dp + 50dp + 60dp + + \ No newline at end of file diff --git a/sphinx/screens-detail/subscription/subscription/src/main/res/values/strings.xml b/sphinx/screens-detail/subscription/subscription/src/main/res/values/strings.xml new file mode 100644 index 0000000000..091e0ba406 --- /dev/null +++ b/sphinx/screens-detail/subscription/subscription/src/main/res/values/strings.xml @@ -0,0 +1,32 @@ + + + RECURRING + Amount + 500 sats + 1000 sats + 2000 sats + Custom amount: + TIME INTERVAL + Daily + Weekly + Monthly + END RULE + Make + Payments + Pay until + Delete subscription + Are you sure you want to delete this subscription? + Amount is required + Time Interval is required + Please set either the number of payments to make or end date + Failed to save subscription + Saved subscription successfully + Deleting subscription + Failed to pause subscription + Successfully paused subscription + Failed to restart subscription + Successfully restarted subscription + Subscribe + Update + Failed to delete subscription + \ No newline at end of file diff --git a/sphinx/screens-detail/subscription/subscription/src/main/res/values/styles.xml b/sphinx/screens-detail/subscription/subscription/src/main/res/values/styles.xml new file mode 100644 index 0000000000..d2775e3ed0 --- /dev/null +++ b/sphinx/screens-detail/subscription/subscription/src/main/res/values/styles.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file