Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
a306875
feat: accept allowance offers automatically on the allowee's wallet
ovitrif Sep 30, 2026
d28ef24
chore: rename changelog fragment
ovitrif Sep 30, 2026
f7fcb4c
Merge remote-tracking branch 'origin/feat/paykit-allowances' into fea…
ovitrif Oct 1, 2026
fd5fa4a
Merge remote-tracking branch 'origin/feat/paykit-allowances' into fea…
ovitrif Oct 1, 2026
cf33010
test: align offer acceptance tests with identity-bound allowances
ovitrif Oct 1, 2026
13f314b
Merge remote-tracking branch 'origin/feat/paykit-allowances' into fea…
ovitrif Oct 1, 2026
662119f
Merge remote-tracking branch 'origin/feat/paykit-allowances' into fea…
ovitrif Oct 2, 2026
4232db0
chore: sync allowance auto-accept with feat/paykit-allowances
ovitrif Oct 19, 2026
b307f3c
Merge remote-tracking branch 'origin/feat/paykit-allowances' into fea…
ovitrif Oct 19, 2026
1cf2db1
Merge remote-tracking branch 'origin/feat/paykit-allowances' into fea…
ovitrif Oct 19, 2026
9341f62
Merge remote-tracking branch 'origin/feat/paykit-allowances' into fea…
ovitrif Oct 19, 2026
4099a5d
Merge remote-tracking branch 'origin/feat/paykit-allowances' into fea…
ovitrif Oct 19, 2026
5620831
Merge remote-tracking branch 'origin/feat/paykit-allowances' into fea…
ovitrif Oct 19, 2026
445ef4d
Merge remote-tracking branch 'origin/feat/paykit-allowances' into fea…
ovitrif Oct 19, 2026
2d8d4e5
fix: accept allowance offers only as checked and while the session lasts
ovitrif Oct 19, 2026
83cf764
Merge remote-tracking branch 'origin/feat/paykit-allowances' into fea…
ovitrif Oct 20, 2026
88ac116
test: present an allowee's ask by its contact-scoped entry id
ovitrif Oct 20, 2026
9b64319
test: drop a duplicated verify left by the base merge
ovitrif Oct 20, 2026
94675c2
Merge remote-tracking branch 'origin/feat/paykit-allowances' into fea…
ovitrif Oct 21, 2026
8b12c40
Merge remote-tracking branch 'origin/feat/paykit-allowances' into fea…
ovitrif Oct 21, 2026
5a05e8e
Merge remote-tracking branch 'origin/feat/paykit-allowances' into fea…
ovitrif Oct 21, 2026
3bcb434
Merge remote-tracking branch 'origin/feat/paykit-allowances' into fea…
ovitrif Oct 21, 2026
ce2f9bf
Merge remote-tracking branch 'origin/feat/paykit-allowances' into fea…
ovitrif Oct 21, 2026
8b68316
Merge remote-tracking branch 'origin/feat/paykit-allowances' into fea…
ovitrif Oct 21, 2026
0c7f503
Merge remote-tracking branch 'origin/feat/paykit-allowances' into fea…
ovitrif Oct 21, 2026
03d08af
Merge remote-tracking branch 'origin/feat/paykit-allowances' into fea…
ovitrif Oct 21, 2026
209ecea
Merge remote-tracking branch 'origin/feat/paykit-allowances' into fea…
ovitrif Oct 21, 2026
035b01a
Merge remote-tracking branch 'origin/feat/paykit-allowances' into fea…
ovitrif Oct 21, 2026
60d82cc
feat: translate the allowance added notification
ovitrif Oct 21, 2026
e45f4ec
fix: announce an offer accepted as the app goes to the background
ovitrif Oct 22, 2026
8113f8e
Merge remote-tracking branch 'origin/master' into feat/allowance-auto…
ovitrif Oct 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions app/src/main/java/to/bitkit/repositories/PaykitAllowance.kt
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,13 @@ data class PaykitAllowance(

val isAnswerable: Boolean get() = lifecycleState == AllowanceLifecycleState.PROPOSED && !isProposedByMe

/**
* A received proposal whose proposer took the allower role, which leaves this wallet the allowee. The money and the
* decision are the allower's, so Bitkit accepts it without asking. A received proposal that makes this wallet the
* allower (a payee asking for an allowance) is not an offer and waits for the user's decision.
*/
val isOfferFromAllower: Boolean get() = isAnswerable && role == Role.ALLOWEE

fun status(now: Instant): Status = when (lifecycleState) {
AllowanceLifecycleState.PROPOSED -> if (isProposedByMe) Status.AWAITING_ANSWER else Status.AWAITING_MY_ANSWER
AllowanceLifecycleState.ACCEPTED -> when {
Expand Down
57 changes: 48 additions & 9 deletions app/src/main/java/to/bitkit/repositories/PaykitAllowanceRepo.kt
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ class PaykitAllowanceRepo @Inject constructor(
private val refreshMutex = Mutex()
private val publishLock = Any()
private val isProcessingRequests = AtomicBoolean(false)
private val isAcceptingOffers = AtomicBoolean(false)
private val updates = AtomicInteger(0)

/** True while allowances are refreshing or requests are being admitted, when coverage may still change. */
Expand Down Expand Up @@ -272,12 +273,47 @@ class PaykitAllowanceRepo @Inject constructor(
}
}

suspend fun accept(entryId: String): Result<Unit> = respond(entryId) {
val proposalStreamItemId = it.proposalStreamItemId ?: throw PaykitAllowanceError.Unavailable
paykitSdkService.acceptAllowance(it.counterparty, it.allowanceId, proposalStreamItemId)
suspend fun accept(entryId: String): Result<Unit> = respond(entry(entryId)) { acceptProposal(it) }

private suspend fun acceptProposal(allowance: PaykitAllowance): AllowanceRecord {
val proposalStreamItemId = allowance.proposalStreamItemId ?: throw PaykitAllowanceError.Unavailable
return paykitSdkService.acceptAllowance(allowance.counterparty, allowance.allowanceId, proposalStreamItemId)
}

suspend fun reject(entryId: String): Result<Unit> = respond(entryId) {
/**
* Accepts the offers that make this wallet the allowee and returns the ones now active so the caller can tell the
* user. A failed accept stays unanswered and is tried again on the next refresh. Stops accepting once
* [canContinue] turns false and still returns what it accepted, since an active allowance is never announced
* later; returns nothing to announce once the identity it started with is gone or [canAnnounce] turns false.
*/
suspend fun acceptOffersFromAllowers(
canAnnounce: () -> Boolean = { true },
canContinue: () -> Boolean = { true },
): List<PaykitAllowanceEntry> = withContext(ioDispatcher) {
val identity = activeIdentity ?: return@withContext emptyList()
val isCurrent = { activeIdentity == identity && canAnnounce() }
val offers = _entries.value.filter { entry ->
val answerable = entry.allowances.filter { it.isAnswerable }
answerable.isNotEmpty() && answerable.all { it.isOfferFromAllower }
}
if (offers.isEmpty() || !isAcceptingOffers.compareAndSet(false, true)) return@withContext emptyList()

try {
val accepted = mutableListOf<PaykitAllowanceEntry>()
for (offer in offers) {
if (!isCurrent() || !canContinue()) break
// Answers the entry that was checked, so an ask that joined it since is never accepted here.
respond(offer) { acceptProposal(it) }
.onSuccess { accepted += offer }
.onFailure { Logger.warn("Failed to accept an allowance offer", it, context = TAG) }
}
if (isCurrent()) accepted else emptyList()
Comment thread
ovitrif marked this conversation as resolved.
} finally {
isAcceptingOffers.set(false)
}
}

suspend fun reject(entryId: String): Result<Unit> = respond(entry(entryId)) {
paykitSdkService.rejectAllowance(it.counterparty, it.allowanceId)
}

Expand All @@ -296,20 +332,22 @@ class PaykitAllowanceRepo @Inject constructor(
}

private suspend fun respond(
entryId: String,
entry: PaykitAllowanceEntry?,
response: suspend (PaykitAllowance) -> AllowanceRecord,
): Result<Unit> = withContext(ioDispatcher) {
runSuspendCatching {
val identity = activeIdentity ?: throw PaykitAllowanceError.Unavailable
val entry = entry(entryId) ?: throw PaykitAllowanceError.Unavailable
if (entry == null) throw PaykitAllowanceError.Unavailable
val answerable = entry.allowances.filter { it.isAnswerable }
if (answerable.isEmpty()) throw PaykitAllowanceError.Unavailable
for (allowance in answerable) {
response(allowance)
sendQueuedMessages(allowance.counterparty)
}
val keys = entry.allowances.map { it.key }
executor.updateLocalState(identity) { it.copy(presentedProposalIds = it.presentedProposalIds + keys) }
if (activeIdentity == identity) {
executor.updateLocalState(identity) { it.copy(presentedProposalIds = it.presentedProposalIds + keys) }
}
refresh()
Unit
}
Expand All @@ -324,11 +362,12 @@ class PaykitAllowanceRepo @Inject constructor(

// region Presentation

/** The first received proposal that still needs an answer and was not shown yet. */
/** The first received proposal that needs the user's answer and was not shown yet; offers are accepted instead. */
fun proposalForPresentation(): PaykitAllowanceEntry? {
val presented = _localState.value.presentedProposalIds
return _entries.value.firstOrNull { entry ->
entry.isAnswerable && entry.allowances.none { it.key in presented }
entry.allowances.any { it.isAnswerable && !it.isOfferFromAllower } &&
Comment thread
ovitrif marked this conversation as resolved.
entry.allowances.none { it.key in presented }
}
}

Expand Down
18 changes: 18 additions & 0 deletions app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ import to.bitkit.repositories.OnchainSendBlockedError
import to.bitkit.repositories.OnchainSendNotDispatchedError
import to.bitkit.repositories.OnchainSendOutcome
import to.bitkit.repositories.OnchainSendPendingError
import to.bitkit.repositories.PaykitAllowanceEntry
import to.bitkit.repositories.PaykitAllowanceError
import to.bitkit.repositories.PaykitAllowanceEvent
import to.bitkit.repositories.PaykitAllowanceRepo
Expand Down Expand Up @@ -1189,6 +1190,11 @@ class AppViewModel @Inject constructor(
releaseCoverage()
}
return result.onSuccess {
val acceptedOffers = paykitAllowanceRepo.acceptOffersFromAllowers(
canAnnounce = { !privatePaykitRepo.isProfileDeletionInProgress.value },
canContinue = { !(onlyWhileActive && isPaymentRequestPollingStopped.value) },
)
announceAcceptedAllowanceOffers(acceptedOffers)
if (processAllowanceRequests()) paykitPaymentRequestRepo.refresh()
presentNextIncomingPaykitPaymentRequest()
}
Expand Down Expand Up @@ -1241,6 +1247,18 @@ class AppViewModel @Inject constructor(
}
}

private fun announceAcceptedAllowanceOffers(offers: List<PaykitAllowanceEntry>) {
offers.forEach { offer ->
notifyAllowanceEvent(
Comment thread
ovitrif marked this conversation as resolved.
type = Toast.ToastType.SUCCESS,
title = context.getString(R.string.subscriptions__allowance_offer_accepted_title),
description = context.getString(R.string.subscriptions__allowance_offer_accepted_description)
.replace("{name}", allowanceContactName(offer.counterparty)),
testTag = "AllowanceOfferAcceptedToast",
)
}
}

/**
* Allowance events post a system notification when allowed, as on iOS: the request sheet that opens right after a
* limit is reached would cover an in-app toast. When notifications are off they fall back to a toast.
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/res/values-ar/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1475,6 +1475,8 @@
<string name="subscriptions__allowance_monthly_allowance">الميزانية الشهرية</string>
<string name="subscriptions__allowance_monthly_limit">الحد الشهري</string>
<string name="subscriptions__allowance_no_contacts">أضف جهة اتصال أولًا. تغطي الميزانيات طلبات الدفع من جهات اتصالك.</string>
<string name="subscriptions__allowance_offer_accepted_description">{name} أعدّ لك ميزانية</string>
<string name="subscriptions__allowance_offer_accepted_title">تمت إضافة الميزانية</string>
<string name="subscriptions__allowance_offer_explanation">ستدفع محفظة {name} طلباتك ضمن هذه الحدود دون السؤال في كل مرة. يمكن لأي منكما إنهاؤها.</string>
<string name="subscriptions__allowance_offer_headline">{name} يعرض عليك\n&lt;accent&gt;ميزانية&lt;/accent&gt;</string>
<string name="subscriptions__allowance_paid_automatically">تم دفع {amount} تلقائيًا</string>
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/res/values-b+es+419/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1490,6 +1490,8 @@
<string name="subscriptions__allowance_monthly_allowance">Asignación mensual</string>
<string name="subscriptions__allowance_monthly_limit">Límite mensual</string>
<string name="subscriptions__allowance_no_contacts">Primero añada un contacto. Las asignaciones cubren las solicitudes de pago de sus contactos.</string>
<string name="subscriptions__allowance_offer_accepted_description">{name} configuró una asignación para usted</string>
<string name="subscriptions__allowance_offer_accepted_title">Asignación agregada</string>
<string name="subscriptions__allowance_offer_explanation">La billetera de {name} pagará sus solicitudes dentro de estos límites sin preguntar cada vez. Cualquiera de los dos puede finalizarla.</string>
<string name="subscriptions__allowance_offer_headline">{name} le ofrece\n&lt;accent&gt;una asignación&lt;/accent&gt;</string>
<string name="subscriptions__allowance_paid_automatically">{amount} pagado automáticamente</string>
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/res/values-ca/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1487,6 +1487,8 @@
<string name="subscriptions__allowance_monthly_allowance">Assignació mensual</string>
<string name="subscriptions__allowance_monthly_limit">Límit mensual</string>
<string name="subscriptions__allowance_no_contacts">Primer afegeix un contacte. Les assignacions cobreixen les sol·licituds de pagament dels teus contactes.</string>
<string name="subscriptions__allowance_offer_accepted_description">{name} t\'ha configurat una assignació</string>
<string name="subscriptions__allowance_offer_accepted_title">Assignació afegida</string>
<string name="subscriptions__allowance_offer_explanation">La cartera de {name} pagarà les teves sol·licituds dins d\'aquests límits sense preguntar cada vegada. Qualsevol de vosaltres hi pot posar fi.</string>
<string name="subscriptions__allowance_offer_headline">{name} t\'ofereix\n&lt;accent&gt;una assignació&lt;/accent&gt;</string>
<string name="subscriptions__allowance_paid_automatically">{amount} pagat automàticament</string>
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/res/values-cs/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1489,6 +1489,8 @@
<string name="subscriptions__allowance_monthly_allowance">Měsíční rozpočet</string>
<string name="subscriptions__allowance_monthly_limit">Měsíční limit</string>
<string name="subscriptions__allowance_no_contacts">Nejprve přidejte kontakt. Rozpočty se vztahují na žádosti o platbu od vašich kontaktů.</string>
<string name="subscriptions__allowance_offer_accepted_description">{name} vám nastavil rozpočet</string>
<string name="subscriptions__allowance_offer_accepted_title">Rozpočet přidán</string>
<string name="subscriptions__allowance_offer_explanation">Peněženka uživatele {name} bude platit vaše žádosti v rámci těchto limitů, aniž by se pokaždé ptala. Kterákoli strana jej může ukončit.</string>
<string name="subscriptions__allowance_offer_headline">{name} nabízí\n&lt;accent&gt;rozpočet&lt;/accent&gt;</string>
<string name="subscriptions__allowance_paid_automatically">{amount} zaplaceno automaticky</string>
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/res/values-de/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1488,6 +1488,8 @@
<string name="subscriptions__allowance_monthly_allowance">Monatliches Budget</string>
<string name="subscriptions__allowance_monthly_limit">Monatslimit</string>
<string name="subscriptions__allowance_no_contacts">Füge zuerst einen Kontakt hinzu. Budgets gelten für Zahlungsanforderungen deiner Kontakte.</string>
<string name="subscriptions__allowance_offer_accepted_description">{name} hat dir ein Budget eingerichtet</string>
<string name="subscriptions__allowance_offer_accepted_title">Budget hinzugefügt</string>
<string name="subscriptions__allowance_offer_explanation">Die Wallet von {name} bezahlt deine Anforderungen innerhalb dieser Limits, ohne jedes Mal nachzufragen. Ihr könnt es beide beenden.</string>
<string name="subscriptions__allowance_offer_headline">{name} bietet dir\n&lt;accent&gt;ein Budget&lt;/accent&gt;</string>
<string name="subscriptions__allowance_paid_automatically">{amount} automatisch bezahlt</string>
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/res/values-el/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1481,6 +1481,8 @@
<string name="subscriptions__allowance_monthly_allowance">Μηνιαίος προϋπολογισμός</string>
<string name="subscriptions__allowance_monthly_limit">Μηνιαίο όριο</string>
<string name="subscriptions__allowance_no_contacts">Πρόσθεσε πρώτα μια επαφή. Οι προϋπολογισμοί καλύπτουν αιτήματα πληρωμής από τις επαφές σου.</string>
<string name="subscriptions__allowance_offer_accepted_description">{name} σού όρισε έναν προϋπολογισμό</string>
<string name="subscriptions__allowance_offer_accepted_title">Ο προϋπολογισμός προστέθηκε</string>
<string name="subscriptions__allowance_offer_explanation">Το πορτοφόλι της επαφής {name} θα πληρώνει τα αιτήματά σου εντός αυτών των ορίων χωρίς να ρωτά κάθε φορά. Οποιοσδήποτε από τους δύο μπορεί να τον τερματίσει.</string>
<string name="subscriptions__allowance_offer_headline">{name} σού προσφέρει\n&lt;accent&gt;έναν προϋπολογισμό&lt;/accent&gt;</string>
<string name="subscriptions__allowance_paid_automatically">{amount} πληρώθηκαν αυτόματα</string>
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/res/values-es-rES/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1485,6 +1485,8 @@
<string name="subscriptions__allowance_monthly_allowance">Asignación mensual</string>
<string name="subscriptions__allowance_monthly_limit">Límite mensual</string>
<string name="subscriptions__allowance_no_contacts">Primero añade un contacto. Las asignaciones cubren las solicitudes de pago de tus contactos.</string>
<string name="subscriptions__allowance_offer_accepted_description">{name} te ha configurado una asignación</string>
<string name="subscriptions__allowance_offer_accepted_title">Asignación añadida</string>
<string name="subscriptions__allowance_offer_explanation">El monedero de {name} pagará tus solicitudes dentro de estos límites sin preguntar cada vez. Cualquiera de los dos puede finalizarla.</string>
<string name="subscriptions__allowance_offer_headline">{name} te ofrece\n&lt;accent&gt;una asignación&lt;/accent&gt;</string>
<string name="subscriptions__allowance_paid_automatically">{amount} pagado automáticamente</string>
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/res/values-es/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1485,6 +1485,8 @@
<string name="subscriptions__allowance_monthly_allowance">Asignación mensual</string>
<string name="subscriptions__allowance_monthly_limit">Límite mensual</string>
<string name="subscriptions__allowance_no_contacts">Primero añada un contacto. Las asignaciones cubren las solicitudes de pago de sus contactos.</string>
<string name="subscriptions__allowance_offer_accepted_description">{name} configuró una asignación para usted</string>
<string name="subscriptions__allowance_offer_accepted_title">Asignación añadida</string>
<string name="subscriptions__allowance_offer_explanation">El monedero de {name} pagará sus solicitudes dentro de estos límites sin preguntar cada vez. Cualquiera de los dos puede finalizarla.</string>
<string name="subscriptions__allowance_offer_headline">{name} le ofrece\n&lt;accent&gt;una asignación&lt;/accent&gt;</string>
<string name="subscriptions__allowance_paid_automatically">{amount} pagado automáticamente</string>
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/res/values-fr/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1486,6 +1486,8 @@
<string name="subscriptions__allowance_monthly_allowance">Allocation mensuelle</string>
<string name="subscriptions__allowance_monthly_limit">Limite mensuelle</string>
<string name="subscriptions__allowance_no_contacts">Ajoutez d\'abord un contact. Les allocations couvrent les demandes de paiement de vos contacts.</string>
<string name="subscriptions__allowance_offer_accepted_description">{name} vous a configuré une allocation</string>
<string name="subscriptions__allowance_offer_accepted_title">Allocation ajoutée</string>
<string name="subscriptions__allowance_offer_explanation">Le portefeuille de {name} paiera vos demandes dans ces limites sans demander à chaque fois. Chacun de vous peut y mettre fin.</string>
<string name="subscriptions__allowance_offer_headline">{name} vous offre\n&lt;accent&gt;une allocation&lt;/accent&gt;</string>
<string name="subscriptions__allowance_paid_automatically">{amount} payé automatiquement</string>
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/res/values-it/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1489,6 +1489,8 @@
<string name="subscriptions__allowance_monthly_allowance">Budget mensile</string>
<string name="subscriptions__allowance_monthly_limit">Limite mensile</string>
<string name="subscriptions__allowance_no_contacts">Aggiungi prima un contatto. I budget coprono le richieste di pagamento dei tuoi contatti.</string>
<string name="subscriptions__allowance_offer_accepted_description">{name} ha impostato un budget per te</string>
<string name="subscriptions__allowance_offer_accepted_title">Budget Aggiunto</string>
<string name="subscriptions__allowance_offer_explanation">Il wallet di {name} pagherà le tue richieste entro questi limiti senza chiedere ogni volta. Ognuno di voi può terminarlo.</string>
<string name="subscriptions__allowance_offer_headline">{name} ti offre\n&lt;accent&gt;un budget&lt;/accent&gt;</string>
<string name="subscriptions__allowance_paid_automatically">{amount} pagati automaticamente</string>
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/res/values-nl/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1487,6 +1487,8 @@
<string name="subscriptions__allowance_monthly_allowance">Maandelijkse toelage</string>
<string name="subscriptions__allowance_monthly_limit">Maandlimiet</string>
<string name="subscriptions__allowance_no_contacts">Voeg eerst een contact toe. Toelagen gelden voor betaalverzoeken van uw contacten.</string>
<string name="subscriptions__allowance_offer_accepted_description">{name} heeft een toelage voor u ingesteld</string>
<string name="subscriptions__allowance_offer_accepted_title">Toelage toegevoegd</string>
<string name="subscriptions__allowance_offer_explanation">De wallet van {name} betaalt uw verzoeken binnen deze limieten zonder elke keer te vragen. Beide partijen kunnen de toelage beëindigen.</string>
<string name="subscriptions__allowance_offer_headline">{name} biedt\n&lt;accent&gt;een toelage&lt;/accent&gt; aan</string>
<string name="subscriptions__allowance_paid_automatically">{amount} automatisch betaald</string>
Expand Down
Loading
Loading