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