From 8d6a6e74aa37d3456f8e634a021d53c3bbe56dcf Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Thu, 22 Oct 2026 12:26:39 +0200 Subject: [PATCH 01/12] feat: stop offering new lightning channels Remove Transfer To Spending, instant receive (CJIT), gift codes and the Spend card, and show wallets with Blocktank channels the date the channels close. --- .../icons/arrow-cjit.imageset/Contents.json | 12 -- .../icons/arrow-cjit.imageset/arrow-cjit.pdf | Bin 6700 -> 0 bytes .../Components/Activity/ActivityBanner.swift | 18 ++- Bitkit/Components/TabBar/TabBar.swift | 21 --- Bitkit/Components/Widgets/Suggestions.swift | 19 +-- Bitkit/MainNavView.swift | 8 - Bitkit/Models/ChannelsClosing.swift | 15 ++ Bitkit/Models/ReceiveLiquidityDecision.swift | 71 -------- .../Localization/ar.lproj/Localizable.strings | 15 +- .../Localization/ca.lproj/Localizable.strings | 17 +- .../Localization/cs.lproj/Localizable.strings | 17 +- .../Localization/de.lproj/Localizable.strings | 17 +- .../Localization/el.lproj/Localizable.strings | 17 +- .../Localization/en.lproj/Localizable.strings | 17 +- .../es-419.lproj/Localizable.strings | 17 +- .../Localization/es.lproj/Localizable.strings | 17 +- .../Localization/fr.lproj/Localizable.strings | 17 +- .../Localization/it.lproj/Localizable.strings | 17 +- .../Localization/nl.lproj/Localizable.strings | 17 +- .../Localization/pl.lproj/Localizable.strings | 17 +- .../pt-BR.lproj/Localizable.strings | 17 +- .../Localization/pt.lproj/Localizable.strings | 15 +- .../Localization/ru.lproj/Localizable.strings | 17 +- Bitkit/Utilities/LocalizeHelpers.swift | 2 +- Bitkit/ViewModels/AppViewModel.swift | 9 +- Bitkit/ViewModels/SheetViewModel.swift | 15 -- Bitkit/ViewModels/WalletViewModel.swift | 6 + Bitkit/Views/Gift/GiftError.swift | 38 ----- Bitkit/Views/Gift/GiftLoading.swift | 149 ----------------- Bitkit/Views/Gift/GiftUsed.swift | 38 ----- Bitkit/Views/Gift/GiftUsedUp.swift | 38 ----- Bitkit/Views/Home/HomeWalletView.swift | 11 ++ .../Advanced/LightningConnectionsView.swift | 4 +- Bitkit/Views/Sheets/GiftSheet.swift | 46 ------ .../Wallets/Activity/ActivityLatest.swift | 5 +- .../Views/Wallets/HardwareWalletScreen.swift | 33 +--- .../Views/Wallets/Receive/ReceiveEdit.swift | 81 +--------- Bitkit/Views/Wallets/Receive/ReceiveQr.swift | 84 +++------- .../Views/Wallets/SavingsWalletScreen.swift | 25 --- .../Views/Wallets/SpendingWalletScreen.swift | 31 +--- BitkitTests/ChannelsClosingTests.swift | 9 ++ BitkitTests/ReceiveEditTests.swift | 11 -- .../ReceiveLiquidityDecisionTests.swift | 152 ------------------ BitkitTests/TabBarReceiveTests.swift | 67 -------- changelog.d/next/1097.removed.md | 1 + 45 files changed, 132 insertions(+), 1138 deletions(-) delete mode 100644 Bitkit/Assets.xcassets/icons/arrow-cjit.imageset/Contents.json delete mode 100644 Bitkit/Assets.xcassets/icons/arrow-cjit.imageset/arrow-cjit.pdf create mode 100644 Bitkit/Models/ChannelsClosing.swift delete mode 100644 Bitkit/Views/Gift/GiftError.swift delete mode 100644 Bitkit/Views/Gift/GiftLoading.swift delete mode 100644 Bitkit/Views/Gift/GiftUsed.swift delete mode 100644 Bitkit/Views/Gift/GiftUsedUp.swift delete mode 100644 Bitkit/Views/Sheets/GiftSheet.swift create mode 100644 BitkitTests/ChannelsClosingTests.swift delete mode 100644 BitkitTests/TabBarReceiveTests.swift create mode 100644 changelog.d/next/1097.removed.md diff --git a/Bitkit/Assets.xcassets/icons/arrow-cjit.imageset/Contents.json b/Bitkit/Assets.xcassets/icons/arrow-cjit.imageset/Contents.json deleted file mode 100644 index 5bd0fcf1e..000000000 --- a/Bitkit/Assets.xcassets/icons/arrow-cjit.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "arrow-cjit.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Bitkit/Assets.xcassets/icons/arrow-cjit.imageset/arrow-cjit.pdf b/Bitkit/Assets.xcassets/icons/arrow-cjit.imageset/arrow-cjit.pdf deleted file mode 100644 index 068dca4c47e9b65f5285ca043d04301a749cc99a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6700 zcmai3Wmr^O+onNK=?+J_VTb_)>COS^9$e%W?oY1+W?XZx=QYzzuLPv%v-cL_`2w(lBR) z9mG@G!CVvOVh07dIJrUrqN3PPxW%0kHqUR`U?&F)S97QnfLRu1We;Jwb#lA7{xW5j zaB^~Rzr+4v&-+mw_<6ixq}7tZ;3J# zZslSP;Nb@e{Eesrc>jvx?BWE4*kgO7)g-{cgfk>P4(`Vh9LUIe9jzfM2}o5T9B%{t z?Xn4CkRbQ)2-=MB)dTQlq|pq;F=VI^F1i+Y`0Dc*=;)e;f%mcd1qEMGvR0rza@kaw zDAB1{{7~HF=ewCUx0Z2{b%PWIN0sJw%ES7YZEB+CC2c`OeS&tcFG$F-o*cHkgznqv z9(3s7PFB?vKA+t4;Nqc+W*6l#34vbA4fvvk$m8-pXH^<$$Gq<97pzS2Boa!BLCJb* zBoU!&VG`e-W+EC~qz)~5KdVba4xqetD!T$cU@+DZJ2N%_0qQufRDhYiV1qi2`dq`v z!_Vf?UNqq|S}C5&Q>%j#srAhCL|G&gI#me!1j!+oa1_kG#}fcM(WRR-Eb?9?`_fYT zo6d|I+rCd089yRX0GT-AhaBb7aYB!z2H<4k(yFg9#{Ee~P`mw+?fg*@7`tthr$OR) z*tG37ZRqy^=r6@drjcp`MZieM{#S3QsF3;JKIHS~eT!Zl6uyjIj|>k&=Dwe6dcPU% z*)nxf000Y99l)B40h7XrBDX*VKGx}_)JH*=iU!{&!oBzSUN!P82C?`@u#hJDgoK#X z+S~9E{zBYF+z4^P5t9)nGW1voMd>}tjs$MPV}I0Tum>>}Zb9ePvZ057A%;$8!LsEk ztq&@Vf8KW`4+?ZJeH^wT=0|a$I=d!-6`MGeAPbarpq^J~qlsGlU?Qk7C?=ax{n4l{ zAF+xY@5m=>u^-rcr0H)KUvNq%=BT|*9bH%#UdR7Ibj}tMINFv3N#nLtMq{Lj4eslV zGqVAzBn>1X50ZVOCJEPSr+`qudp<31N9*>q7S9`N?&VI`F+{HNvmlk~1FI0{cJx04 z1%aHn?s)Fl?#NS-l%2k9vEOGsD4TIbvAMd|mX-FI_Nh;wUpimjmk2NwN6?}Xc)X~2 zK`Z$nD*_SDEt~Y1l_26JXBXIv){(99^|W}ivX8uv6m7JN!kgT$^vE!rqSr1dF7bSl z=knULsWE!1)>SW>Q+?9r6tK5(BwSV3Hgi}FUy7&EcVL{3$q;U>uD!4Z$^>ySM1(dlYwedHY`eJ*=N zJ%t%=_UL%r?3;mO3^yX;S<_;)C%`0CKxO zEXowo_O@raB`X?Izw)q?q|=d3i>`L`Nsd=eXg86$z!#S%;6xnF{+MyR+I*`@I?U zorKw)cUx5#W_zYwRSxem5hLy|9SdE^%*K)=1|`2~$-T}g_+r`P))T3EtZG)ktLpcJ zH!U_z?MWmTNPp18@V&>l%vA6fjjyz>c}?B*G_LWD8}*L!#S0mYTMdWvwhNiwNgniG zX$Kx(VP9De><)Z}oy_blT)ncIeOeA$yjtR$bvzQi#g0dh!cK}#k`HPQjrQg?LicgL z&us~GO3dQvFjFWX&E}H}yKpU!+pB>;XqH=78x9pc2z^MO18+Rjlnfo`J(`s+ zTTjE#>Ryb!P(W4!b9$t&CwkED`)Zhu?dQ&{eqZe-@=;Xo7s`s^R+vsNj!#LSO|(o% zNMuxTS6s>zWx&RpuoNdR;nDxdw<_oK%#3}8lf2JmsI>Poia5a)WB@cs@^voTm^HG^ ztT@y=`Qr7%YxlhT!4t|_IN8j4Wu*;aHH+J`Da)3jhSZ+gQ@BuNd_6yG%Rxw6=%bKB zU9f}gz{KD}jBw+EySHzKVFt5g3Ev!q$ z-)uTJ$IPeAlY25IUQGPB?7gfk*KVnOdpzK{;dvZ z+9%tI2WOr8%F0740qi zo;iEp@QWws3+G&m`;E?DIWKl20-&hAL2MX%4`gr^_V#xpmeg8r&LzGDC9iPc0&x!? zv52txmhRV|hxNtE7Pb7id^y!SRPb0WRIVY1%WwGf>QHx=xBvav+U)RFw|(~ws6xdL=5CV*8kykT+tz5bf=7ZXNMeg-?oud z>; zz1(7GOYW`tM8cug@zc5O_eMg|QF10mr zN*Zfr!}s$)+|48OX{+=1+!N{FmfJPe-Hd41Du1pzFymMN+M4%6xp;c6+j30{SlI9> z*N2}QJm_?5vMp(l5W|()YVjs#xS=Q2K7=pyiM@MyIUBE@spjV`;cdimwLPgeU2ya6 ztfuRFQFqfTb6ii-EsypwOI zT-`PhXU!y8<P zQ1zr7NfiRP-_|hYy9tPonR9lV1iwZ*P<0lm0Uo5$os*PFhl)j)5yd%uZL03u4cd60 zuJGX_`}+oZ;+FH2l@Arv98W`J=_kIN=!kT6MimWU^GWfWBy`G4G_BDnO{^J^r3D`e zvxVdsd0FA^7$I0=ttBBsTDc_FY6`DB_zWg-aT(Q$L&SpDM*<(trd{k;MJ`NSNfh)1 z66MIKMM1pP0%CKi10RB%)ndrhN_hHwM>J#~!nM@8{7l(#!`f|Hsg(C7Hzi!&%D~*-Ej62zVv5_u)@@3;VI$a$7yXoktgL2BF z*9{Kah>f6(ZZB|=*tC=<=76GdUMB8#EcFM73Yy{3^XZLB7AJCB3gA8S^2l zvy5W1&F_eFo|Dn~Q!T^`J z>nuWG`S8z|_XkuKrONN0WR%)xU&gzyxXDDUm#Yh&q=$!5!ly_E#$J~y5AKp~cs)~x9O`&aHJrZYirgF`+UrZ6fgT4nph`ecBO*~2A0`2yjCZ0pRFCre# zQVg!{g4c&Rm*i9Uc8$DU()%+M^$CQ`Iwo=kN+8%+&DU0io z(fK&>>Xei+1gk2_t4(r_;R$`XxUSt=Q>dZp1(GhB9fcu!oxwtJV2*0E6}Kx!c{VD=fyJ_LNX?je z-GEegb~oLeiI@9)!o}(*d~6OT;u-2XEw|b~gw(Q|R0kQ{?2hd4NuLq~d8L_-S|?!n zPK&gY z!HYxCC+ntNf9yM2GOdTKubj-(oHCZzMb%VhC+Vx=s0UK4Qg;9r-_KjFoR0P{)EQl2 zaRacM3u?#v<1e7amRu!HNUk&<>YIi%;F9<-xRx&08tGC;51ND%M$I+)>)3%sBOj>D zryznGE1HGjTihi&8d)Dvmh#bF%=OZf_d|?5_~|=6-|cjjqrRyfRoNaHS=XCdCK4%- zoD064te~MhxPT-^q{CrY8@O-`_u?UbwkSp@X!bsS-`JZUZK_SJ=Wynvn7B#0ne+~4 zjnF=KX-+%w)ZOZJjs2noGOC_MlNOx12nBP#W97M$=ey(1bEQKGb7W1mtjHzGW}$n- zRd5P3JuNUNt=;+QLX~AaS<2E_?)a;Eyyf6P35udsH)SFMY1C%o7cKFILtoIt5D-)C zq$BlJ)tpc%hDuD(iu_~GMz$HU&JgM{!OoDU;Q=UJi|Cay(Lrb5!`*xA4?BM3&DY4{ zruYKerdLAnNKgEOYs{?N0AED{pG*W2BeuTb6m~XhwQIu&rJ1M3hA~u4k6R$5kpy)W zGY*FDP!{yNgpu|H<@IN~3|^at;R{n~#k>-p&*R8*vW;&wg%9tljMIT82HL<{ieXw2 zMSnn1g3BwYa3akgl(rKlI*|C=S9vYuK4nnoliZO{8i5ryl|}_`n)QuvK6fWI0d0cY zdD!m}8VcNOS@f5LbGKWPlju@sSyQ2gC_@V(1X+4dNtnc}XI_h~NXuG_84t*N5@GDf zK3lz?P{gyeL6!O`X9FkmO@;bdQW|=H_mj2SAsynk)i2c7bZK_FRHcG6_FqiyTeiP$ z5aG&@ShmLUZqP&neHS%a@8&nK4Mx9Q)S^YH5oaBZDtbK*_!MH}taI(9RP~6}XY^HYr^G zeFFS$iXj)>Ikm>bSjv1RcgT90%;zz$6AGKMCF|{haa*fgnxu5rq{J-a9hmVI5^GTn%0avmXG!C#Gm&@cF|F_3`69bB9N21b8Rr!_qhxASc< z#Omh&TLo$XgGf4f01N=!oZNq||Ge-CaPsr>0|h_;ppX!!07&rm>$V5^Yp|{Xb#`!d zGKV???s}2CM&WMUF697sfx>S?-ZdLPK`_MW76Sme|Hl3?gZ~rz&&Xd1W^tP>;BFIl z-Of*+u6Ne|+Jf^f@=sU4-yUegpzcs7uoKi0dK>*N&pY|gloa5W4uHE0gr9&i1a1Xj zhQfc|8{D1It;x@n@AmfB;YmSUAa)K`zsL5t4TH@M0PqG z?aKa?!2g4RZnOF?49N2@Jsv^eZL+`X@d(~}{}(23+bRB+p3uMj@eA?Y7U*|9ej$O| z-Tsb&fPDXo0pj8Lhn|ZQ1ZD?y!oK_46r^u!$IJ`j1#%m53-ADixCI1&EdK-fv$9+o n4i2|v{Z$pNXD~15-4Wi_)Y%2% 0 - - if Self.shouldOpenSpendingCjitEntry( - isSpendingWallet: navigation.currentRoute == .spendingWallet, - wallet: wallet, - hasPendingTransfersToSpending: hasPendingTransfersToSpending - ) { - // On spending wallet screen, show CJIT flow when user can't receive normally - sheets.showSheet(.receive, data: ReceiveConfig(view: .cjitAmount)) } else { sheets.showSheet(.receive) } @@ -89,14 +76,6 @@ struct TabBar: View { private func onScanPress() { sheets.showSheet(.scanner) } - - static func shouldOpenSpendingCjitEntry( - isSpendingWallet: Bool, - wallet: WalletViewModel, - hasPendingTransfersToSpending: Bool - ) -> Bool { - isSpendingWallet && !wallet.canCreateReceiveLightningInvoice(amountSats: nil) && !hasPendingTransfersToSpending - } } #Preview { diff --git a/Bitkit/Components/Widgets/Suggestions.swift b/Bitkit/Components/Widgets/Suggestions.swift index 2ce057c0e..6eb2f8ba5 100644 --- a/Bitkit/Components/Widgets/Suggestions.swift +++ b/Bitkit/Components/Widgets/Suggestions.swift @@ -20,7 +20,6 @@ enum SuggestionAction: Hashable { case secure case shop case support - case transferToSpending } /// Wallet state used to choose which suggestion cards to show and in what order. @@ -33,8 +32,8 @@ enum WalletSuggestionState { /// Ordered suggestion card IDs per wallet state (priority: first = highest). /// Max 4 cards are shown; when one is dismissed or completed, the next in this list is shown. private let suggestionOrderByState: [WalletSuggestionState: [String]] = [ - .empty: ["buyBitcoin", "transferToSpending", "hardware", "support", "backupSeedPhrase", "pin", "profile", "invite"], - .onchain: ["backupSeedPhrase", "pin", "transferToSpending", "hardware", "support", "profile", "invite", "buyBitcoin"], + .empty: ["buyBitcoin", "hardware", "support", "backupSeedPhrase", "pin", "profile", "invite"], + .onchain: ["backupSeedPhrase", "pin", "hardware", "support", "profile", "invite", "buyBitcoin"], .spending: ["quickpay", "notifications", "shop", "hardware", "profile", "support", "invite", "buyBitcoin"], ] @@ -47,14 +46,6 @@ let cards: [SuggestionCardData] = [ color: .blue24, action: .backup ), - SuggestionCardData( - id: "transferToSpending", - title: t("cards__lightning__title"), - description: t("cards__lightning__description"), - imageName: "lightning", - color: .purple24, - action: .transferToSpending - ), SuggestionCardData( id: "pin", title: t("cards__pin__title"), @@ -154,8 +145,6 @@ extension SuggestionCardData { return "shop" case .support: return "support" - case .transferToSpending: - return "lightning" } } } @@ -170,7 +159,7 @@ struct Suggestions: View { var previewCardIds: [String]? - static let previewSheetCardIds = ["backupSeedPhrase", "pin", "transferToSpending", "support"] + static let previewSheetCardIds = ["backupSeedPhrase", "pin", "buyBitcoin", "support"] @EnvironmentObject var app: AppViewModel @EnvironmentObject var navigation: NavigationViewModel @@ -373,8 +362,6 @@ struct Suggestions: View { route = .support case .hardware: sheets.showSheet(.hardwareConnect) - case .transferToSpending: - route = app.hasSeenTransferIntro ? .fundingOptions : .transferIntro } if let route { diff --git a/Bitkit/MainNavView.swift b/Bitkit/MainNavView.swift index f901371ad..569c1b64f 100644 --- a/Bitkit/MainNavView.swift +++ b/Bitkit/MainNavView.swift @@ -115,14 +115,6 @@ struct MainNavView: View { ) { config in BackupSheet(config: config) } - .sheet( - item: $sheets.giftSheetItem, - onDismiss: { - sheets.hideSheetIfActive(.gift, reason: "Gift sheet dismissed") - } - ) { - config in GiftSheet(config: config) - } .sheet( item: $sheets.connectionClosedSheetItem, onDismiss: { diff --git a/Bitkit/Models/ChannelsClosing.swift b/Bitkit/Models/ChannelsClosing.swift new file mode 100644 index 000000000..742425711 --- /dev/null +++ b/Bitkit/Models/ChannelsClosing.swift @@ -0,0 +1,15 @@ +import Foundation + +enum ChannelsClosing { + /// Date on which Blocktank closes the remaining Lightning channels; set before release. + static let CHANNELS_CLOSE_DATE = DateComponents(calendar: Calendar(identifier: .gregorian), year: 2026, month: 12, day: 15) + + static func formattedCloseDate(locale: Locale = Locale(identifier: LocalizationHelper.currentLanguageCode)) -> String { + guard let date = CHANNELS_CLOSE_DATE.date else { return "" } + let formatter = DateFormatter() + formatter.locale = locale + formatter.dateStyle = .medium + formatter.timeStyle = .none + return formatter.string(from: date) + } +} diff --git a/Bitkit/Models/ReceiveLiquidityDecision.swift b/Bitkit/Models/ReceiveLiquidityDecision.swift index b999fa998..5c4128535 100644 --- a/Bitkit/Models/ReceiveLiquidityDecision.swift +++ b/Bitkit/Models/ReceiveLiquidityDecision.swift @@ -1,16 +1,3 @@ -enum ReceiveAdditionalLiquidityAction: Equatable { - case none - case chooseAmount - case createCjit(UInt64) - case geoBlocked -} - -enum ReceiveLiquiditySource: Equatable { - case savings - case auto - case spending -} - enum ReceiveLiquidityDecision { static func canCreateLightningInvoice( hasReadyChannels: Bool, @@ -27,62 +14,4 @@ enum ReceiveLiquidityDecision { return invoiceAmountSats <= inboundCapacitySats } - - static func additionalLiquidityAction( - source: ReceiveLiquiditySource, - invoiceAmountSats: UInt64, - inboundCapacitySats: UInt64?, - minCjitSats: UInt64?, - maxCjitAmountSats: UInt64?, - isGeoBlocked: Bool - ) -> ReceiveAdditionalLiquidityAction { - guard source == .spending, needsInboundLiquidity(invoiceAmountSats: invoiceAmountSats, inboundCapacitySats: inboundCapacitySats) else { - return .none - } - - let inboundCapacitySats = inboundCapacitySats ?? 0 - if inboundCapacitySats == 0 { - return .none - } - - if isGeoBlocked { - return .geoBlocked - } - - let minCjitSats = minCjitSats ?? 0 - guard let maxCjitAmountSats, maxCjitAmountSats > 0 else { - return .chooseAmount - } - - if invoiceAmountSats == 0 || minCjitSats == 0 || invoiceAmountSats < minCjitSats || invoiceAmountSats > maxCjitAmountSats { - return .chooseAmount - } - - return .createCjit(invoiceAmountSats) - } - - static func needsCjitLimitsForAdditionalLiquidity( - source: ReceiveLiquiditySource, - invoiceAmountSats: UInt64, - inboundCapacitySats: UInt64?, - isGeoBlocked: Bool - ) -> Bool { - guard source == .spending, needsInboundLiquidity(invoiceAmountSats: invoiceAmountSats, inboundCapacitySats: inboundCapacitySats), - (inboundCapacitySats ?? 0) > 0 - else { - return false - } - - return !isGeoBlocked - } - - static func needsInboundLiquidity(invoiceAmountSats: UInt64, inboundCapacitySats: UInt64?) -> Bool { - let inboundCapacitySats = inboundCapacitySats ?? 0 - - if invoiceAmountSats == 0 { - return inboundCapacitySats == 0 - } - - return invoiceAmountSats > inboundCapacitySats - } } diff --git a/Bitkit/Resources/Localization/ar.lproj/Localizable.strings b/Bitkit/Resources/Localization/ar.lproj/Localizable.strings index b0269c7fd..fce3b58b4 100644 --- a/Bitkit/Resources/Localization/ar.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/ar.lproj/Localizable.strings @@ -6,8 +6,6 @@ "cards__notifications__title" = "استلم الأموال"; "cards__invite__title" = "دعوة"; "cards__invite__description" = "شارك Bitkit"; -"cards__lightning__title" = "إنفاق"; -"cards__lightning__description" = "مدفوعات فورية"; "cards__transferPending__description" = "جاهز خلال {duration}"; "cards__pin__title" = "تأمين"; "cards__pin__description" = "إعداد رمز PIN"; @@ -203,7 +201,6 @@ "lightning__transfer_hw__sign_title" = "وقّع باستخدام\nجهازك"; "lightning__transfer_hw__signed_title" = "تم توقيع\nالمعاملة"; "lightning__transfer_to_savings_button" = "تحويل إلى المدخرات"; -"lightning__transfer_to_spending_button" = "تحويل إلى الإنفاق"; "lightning__spending_intro__title" = "تحويل\nإلى الإنفاق"; "lightning__spending_intro__text" = "حوّل الأموال إلى رصيد إنفاقك للاستمتاع بمعاملات فورية ورخيصة مع الأصدقاء والعائلة والتجار."; "lightning__spending_intro__button" = "ابدأ"; @@ -319,6 +316,8 @@ "lightning__channel_node_id" = "معرّف النظير"; "lightning__channel_id" = "معرّف القناة"; "lightning__channel_point" = "نقطة القناة"; +"lightning__channels_closing_banner" = "تُغلق القنوات في {date}"; +"lightning__channels_closing_notice" = "ستُغلق قنوات Lightning الخاصة بك في {date}. انقل رصيد الإنفاق إلى المدخرات قبل ذلك."; "lightning__closure_reason" = "سبب الإغلاق"; "lightning__support" = "الدعم"; "lightning__connection_closed__title" = "تم إغلاق الاتصال"; @@ -462,13 +461,6 @@ "other__try_again" = "يرجى المحاولة مرة أخرى."; "other__scan__error__generic" = "لا يستطيع Bitkit قراءة البيانات المقدمة."; "other__scan__error__expired" = "انتهت صلاحية فاتورة Lightning هذه."; -"other__gift__claimed__text" = "عذرًا، لقد تأخرت! تمت المطالبة بجميع الهدايا لهذا الرمز."; -"other__gift__claiming__text" = "جارٍ المطالبة برمز هدية Bitkit..."; -"other__gift__claiming__title" = "جارٍ المطالبة بالهدية"; -"other__gift__error__title" = "خطأ في رمز الهدية"; -"other__gift__error__text" = "تعذر على Bitkit المطالبة بالأموال. يرجى المحاولة لاحقًا أو الاتصال بنا."; -"other__gift__used__title" = "رمز مستخدم"; -"other__gift__used__text" = "تم استخدام رمز هدية Bitkit هذا بالفعل، وتم دفع الأموال."; "other__shop__intro__title" = "تسوق"; "other__shop__intro__description" = "اجعل حياتك على معيار Bitcoin. أنفق Bitcoin على بطاقات الهدايا الرقمية وبطاقات eSIM وشحن الهاتف والمزيد."; "other__shop__intro__button" = "ابدأ"; @@ -773,9 +765,6 @@ "wallet__receive_show_qr" = "عرض رمز QR"; "wallet__receive_background_setup_text" = "قم بتمكين الإعداد في الخلفية للخروج من Bitkit بأمان أثناء تكوين رصيدك."; "wallet__receive_background_setup_toggle" = "تمكين الإعداد في الخلفية"; -"wallet__receive_spending" = "استلام أموال Lightning"; -"wallet__receive_spending_text" = "استمتع بمدفوعات Bitcoin فورية ورخيصة على شبكة Lightning."; -"wallet__receive_spending_title" = "استلم على رصيد الإنفاق"; "wallet__receive_connect_initial" = "لإعداد رصيد إنفاقك، سيتم خصم رسوم شبكة {networkFee} ورسوم مزود الخدمة {serviceFee}."; "wallet__receive_connect_additional" = "لاستلام المزيد من Bitcoin الفوري، يتعين على Bitkit زيادة سيولتك. سيتم خصم رسوم شبكة {networkFee} ورسوم مزود الخدمة {serviceFee} من المبلغ الذي حددته."; "wallet__receive_liquidity__nav_title" = "إعداد رصيد الإنفاق"; diff --git a/Bitkit/Resources/Localization/ca.lproj/Localizable.strings b/Bitkit/Resources/Localization/ca.lproj/Localizable.strings index 29eec5810..61820c0d0 100644 --- a/Bitkit/Resources/Localization/ca.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/ca.lproj/Localizable.strings @@ -8,8 +8,6 @@ "cards__notifications__title" = "Rebre pagaments"; "cards__invite__title" = "Convidar"; "cards__invite__description" = "Comparteix Bitkit"; -"cards__lightning__title" = "Gastar"; -"cards__lightning__description" = "Pagaments instantanis"; "cards__lightningSettingUp__title" = "Transferència"; "cards__lightningSettingUp__description" = "En progrés..."; "cards__lightningConnecting__title" = "Conectant"; @@ -229,7 +227,6 @@ "lightning__transfer_hw__sign_title" = "Signa amb\nel teu dispositiu"; "lightning__transfer_hw__signed_title" = "Transacció\nsignada"; "lightning__transfer_to_savings_button" = "Transferir a estalvis"; -"lightning__transfer_to_spending_button" = "Transferir a despesa"; "lightning__spending_intro__title" = "Transferir\na despesa"; "lightning__spending_intro__text" = "Transfereix fons al teu saldo de despesa per gaudir de transaccions instantànies i barates amb amics, família i comerciants."; "lightning__spending_intro__button" = "Començar"; @@ -356,6 +353,8 @@ "lightning__channel_node_id" = "ID de node parell"; "lightning__channel_id" = "ID de canal"; "lightning__channel_point" = "Punt del canal"; +"lightning__channels_closing_banner" = "ELS CANALS ES TANQUEN EL {date}"; +"lightning__channels_closing_notice" = "Els teus canals Lightning es tancaran el {date}. Transfereix el teu saldo de despesa a estalvis abans d'aquesta data."; "lightning__closure_reason" = "Motiu del tancament"; "lightning__support" = "Suport"; "lightning__connection_closed__title" = "Connexió tancada"; @@ -533,15 +532,6 @@ "other__try_again" = "Torna-ho a provar."; "other__scan__error__generic" = "Bitkit no pot llegir les dades proporcionades."; "other__scan__error__expired" = "Aquesta factura Lightning ha caducat."; -"other__gift__claimed__text" = "Ho sentim, arribes tard! Tots els regals per a aquest codi ja s\'han reclamat."; -"other__gift__claiming__text" = "Reclamant el teu codi de regal de Bitkit..."; -"other__gift__claiming__title" = "Reclamant regal"; -"other__gift__error__title" = "Error del codi de regal"; -"other__gift__error__text" = "Bitkit no ha pogut reclamar els fons. Si us plau, torna-ho a provar més tard o contacta amb nosaltres."; -"other__gift__error__button" = "OK"; -"other__gift__used__title" = "Codi usat"; -"other__gift__used__text" = "Aquest codi de regal de Bitkit ja s\'ha utilitzat i els fons s\'han pagat."; -"other__gift__used__button" = "OK"; "other__shop__intro__title" = "Botiga"; "other__shop__intro__description" = "Viu la teva vida amb l\'estàndard Bitcoin. Gasta els teus Bitcoin en targetes regal digitals, eSIM, recàrregues de telèfon i molt més."; "other__shop__intro__button" = "Començar"; @@ -1282,9 +1272,6 @@ "wallet__receive_show_qr" = "Mostra el codi QR"; "wallet__receive_background_setup_text" = "Activa la configuració en segon pla per sortir de Bitkit amb seguretat mentre el teu saldo s\'està configurant."; "wallet__receive_background_setup_toggle" = "Activa la configuració en segon pla"; -"wallet__receive_spending" = "Rebre fons Lightning"; -"wallet__receive_spending_text" = "Gaudeix de pagaments de bitcoin instantanis i barats a la xarxa Lightning."; -"wallet__receive_spending_title" = "Rebre al saldo de despesa"; "wallet__receive_connect_initial" = "Per configurar el teu saldo de despesa, es descomptarà una comissió de xarxa de {networkFee} i una comissió de proveïdor de servei de {serviceFee}."; "wallet__receive_connect_additional" = "Per rebre més Bitcoin instantani, Bitkit ha d\'augmentar la teva liquiditat. Una comissió de xarxa de {networkFee} i una comissió de proveïdor de servei de {serviceFee} es descomptaran de l\'import que has especificat."; "wallet__receive_liquidity__nav_title" = "Configuració del saldo de despesa"; diff --git a/Bitkit/Resources/Localization/cs.lproj/Localizable.strings b/Bitkit/Resources/Localization/cs.lproj/Localizable.strings index 8d813a89d..ad14061a9 100644 --- a/Bitkit/Resources/Localization/cs.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/cs.lproj/Localizable.strings @@ -8,8 +8,6 @@ "cards__notifications__title" = "Přijímat platby"; "cards__invite__title" = "Pozvat"; "cards__invite__description" = "Sdílet Bitkit"; -"cards__lightning__title" = "Utratit"; -"cards__lightning__description" = "Okamžité platby"; "cards__lightningSettingUp__title" = "Převod"; "cards__lightningSettingUp__description" = "Probíhá..."; "cards__lightningConnecting__title" = "Připojování"; @@ -230,7 +228,6 @@ "lightning__transfer_hw__sign_title" = "Podepište\nsvým zařízením"; "lightning__transfer_hw__signed_title" = "Transakce\npodepsána"; "lightning__transfer_to_savings_button" = "Převést do úspor"; -"lightning__transfer_to_spending_button" = "Převést do útraty"; "lightning__spending_intro__title" = "Převést\ndo výdajů"; "lightning__spending_intro__text" = "Převeďte peníze na svůj disponibilní zůstatek a využívejte okamžité a levné transakce s přáteli, rodinou a obchodníky."; "lightning__spending_intro__button" = "Začít"; @@ -372,6 +369,8 @@ "lightning__channel_node_id" = "ID uzlu"; "lightning__channel_id" = "ID kanálu"; "lightning__channel_point" = "Bod kanálu"; +"lightning__channels_closing_banner" = "KANÁLY SE ZAVŘOU {date}"; +"lightning__channels_closing_notice" = "Vaše Lightning kanály budou uzavřeny {date}. Do té doby převeďte zůstatek útrat do úspor."; "lightning__closure_reason" = "Důvod uzavření"; "lightning__support" = "Podpora"; "lightning__connection_closed__title" = "Připojení uzavřeno"; @@ -573,15 +572,6 @@ "other__transfer_notification__body" = "Otevřete Bitkit pro dokončení přenosu"; "other__scan__error__generic" = "Bitkit není schopen přečíst zadaná data."; "other__scan__error__expired" = "Platnost lightning faktury vypršela."; -"other__gift__claimed__text" = "Je nám líto, přišli jste pozdě! Všechny dary pro tento kód již byly vyzvednuty."; -"other__gift__claiming__text" = "Uplatnění dárkového kódu Bitkit..."; -"other__gift__claiming__title" = "Uplatnění daru"; -"other__gift__error__title" = "Chyba dárkového kódu"; -"other__gift__error__text" = "Bitkit nemohl nárokovat prostředky. Zkuste to prosím znovu později nebo nás kontaktujte."; -"other__gift__error__button" = "OK"; -"other__gift__used__title" = "Použitý kód"; -"other__gift__used__text" = "Tento dárkový kód BitKitu již byl použit a prostředky byly vyplaceny."; -"other__gift__used__button" = "OK"; "other__shop__intro__title" = "Nakoupit"; "other__shop__intro__description" = "Zařiď si život podle Bitcoin standardu. Utrácej své Bitcoiny za digitální dárkové karty, eSIMky, dobíjení kreditu a mnohem víc."; "other__shop__intro__button" = "Začít"; @@ -1396,9 +1386,6 @@ "wallet__receive_text_lnfunds" = "Chcete obdržet prostředky lightning?"; "wallet__receive_background_setup_text" = "Povolte nastavení na pozadí pro bezpečné ukončení Bitkitu, zatímco se váš zůstatek konfiguruje."; "wallet__receive_background_setup_toggle" = "Povolit nastavení na pozadí"; -"wallet__receive_spending" = "Přijmout Lightning prostředky"; -"wallet__receive_spending_text" = "Užívejte si okamžité a levné bitcoinové platby v síti Lightning."; -"wallet__receive_spending_title" = "Přijmout na disponibilní zůstatek"; "wallet__receive_connect_initial" = "Chcete-li nastavit disponibilní zůstatek, bude odečten síťový poplatek {networkFee} a {serviceFee} poplatek poskytovateli služeb."; "wallet__receive_connect_additional" = "Chcete-li získat více okamžitých bitcoinů, musí Bitkit navýšit vaši likviditu. Od vámi zadané částky bude odečten síťový poplatek {networkFee} a {serviceFee} poplatek poskytovatele služeb ."; "wallet__receive_liquidity__nav_title" = "Nastavení disponibilního zůstatku"; diff --git a/Bitkit/Resources/Localization/de.lproj/Localizable.strings b/Bitkit/Resources/Localization/de.lproj/Localizable.strings index cfcd3dccc..a2dc6cc8a 100644 --- a/Bitkit/Resources/Localization/de.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/de.lproj/Localizable.strings @@ -8,8 +8,6 @@ "cards__notifications__title" = "Zahlungen empfangen"; "cards__invite__title" = "Einladen"; "cards__invite__description" = "Teile Bitkit"; -"cards__lightning__title" = "Ausgeben"; -"cards__lightning__description" = "Sofortüberweisung"; "cards__lightningSettingUp__title" = "Übertragung"; "cards__lightningSettingUp__description" = "in Vorbereitung..."; "cards__lightningConnecting__title" = "Verbinden"; @@ -230,7 +228,6 @@ "lightning__transfer_hw__sign_title" = "Mit deinem\nGerät signieren"; "lightning__transfer_hw__signed_title" = "Transaktion\nsigniert"; "lightning__transfer_to_savings_button" = "Zu Ersparnissen transferieren"; -"lightning__transfer_to_spending_button" = "Zu Ausgaben transferieren"; "lightning__spending_intro__title" = "zu den Ausgaben"; "lightning__spending_intro__text" = "Überweisen Sie Geld auf Ihr Guthaben, um sofortige und günstige Transaktionen mit Freunden, Familie und Händlern zu tätigen."; "lightning__spending_intro__button" = "Los geht\'s"; @@ -370,6 +367,8 @@ "lightning__channel_node_id" = "Peer ID"; "lightning__channel_id" = "Channel ID"; "lightning__channel_point" = "Kanalpunkt"; +"lightning__channels_closing_banner" = "KANÄLE SCHLIESSEN AM {date}"; +"lightning__channels_closing_notice" = "Deine Lightning-Kanäle werden am {date} geschlossen. Übertrage dein Guthaben vorher auf das Sparkonto."; "lightning__closure_reason" = "Grund für die Schließung"; "lightning__support" = "Support"; "lightning__connection_closed__title" = "Verbindung geschlossen"; @@ -571,15 +570,6 @@ "other__transfer_notification__body" = "Öffnen Sie Bitkit, um Ihre Übertragung abzuschließen"; "other__scan__error__generic" = "Bitkit ist nicht in der Lage, die bereitgestellten Daten zu lesen."; "other__scan__error__expired" = "Diese Lightning-Rechnung ist abgelaufen."; -"other__gift__claimed__text" = "Leider kommst du zu spät! Alle Geschenke für diesen Code wurden bereits eingelöst."; -"other__gift__claiming__text" = "Dein Bitkit-Geschenkcode wird eingelöst..."; -"other__gift__claiming__title" = "Geschenk einlösen"; -"other__gift__error__title" = "Rabattcode-Fehler"; -"other__gift__error__text" = "Bitkit konnte die Gelder nicht einlösen. Bitte versuche es später erneut oder kontaktiere uns."; -"other__gift__error__button" = "OK"; -"other__gift__used__title" = "Verwendeter Code"; -"other__gift__used__text" = "Dieser Bitkit-Rabattcode wurde bereits verwendet und der Betrag wurde ausbezahlt."; -"other__gift__used__button" = "OK"; "other__shop__intro__title" = "Shop"; "other__shop__intro__description" = "Lebe nach dem Bitcoin-Standard. Gib dein Bitcoin für digitale Geschenkkarten, eSIMs, Handy-Aufladungen und mehr aus."; "other__shop__intro__button" = "Los geht\'s"; @@ -1394,9 +1384,6 @@ "wallet__receive_text_lnfunds" = "Möchtest du Lightning-Gelder empfangen?"; "wallet__receive_background_setup_text" = "Aktiviere die Hintergrundeinrichtung, um Bitkit sicher zu verlassen, während dein Guthaben konfiguriert wird."; "wallet__receive_background_setup_toggle" = "Hintergrundeinrichtung aktivieren"; -"wallet__receive_spending" = "Auf Ausgabenkonto empfangen"; -"wallet__receive_spending_text" = "Genieße sofortige und günstige Bitcoin-Zahlungen im Lightning-Netzwerk."; -"wallet__receive_spending_title" = "Auf Ausgabenguthaben empfangen"; "wallet__receive_connect_initial" = "Um dein Ausgabenguthaben einzurichten, werden eine {networkFee} Netzwerkgebühr und {serviceFee} Dienstanbietergebühr abgezogen."; "wallet__receive_connect_additional" = "Um mehr sofortiges Bitcoin zu empfangen, muss Bitkit deine Liquidität erhöhen. Eine {networkFee} Netzwerkgebühr und {serviceFee} Dienstanbietergebühr werden vom angegebenen Betrag abgezogen."; "wallet__receive_liquidity__nav_title" = "Einrichtung des Ausgabensaldos"; diff --git a/Bitkit/Resources/Localization/el.lproj/Localizable.strings b/Bitkit/Resources/Localization/el.lproj/Localizable.strings index f4bc31454..fd0361605 100644 --- a/Bitkit/Resources/Localization/el.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/el.lproj/Localizable.strings @@ -7,8 +7,6 @@ "cards__notifications__title" = "Λήψη Πληρωμής"; "cards__invite__title" = "Πρόσκληση"; "cards__invite__description" = "Μοιραστείτε το Bitkit"; -"cards__lightning__title" = "Δαπάνες"; -"cards__lightning__description" = "Άμεσες πληρωμές"; "cards__lightningSettingUp__title" = "Μεταφορά"; "cards__lightningConnecting__title" = "Σύνδεση"; "cards__lightningConnecting__description" = "Έτοιμο σε ±10 λεπτά"; @@ -225,7 +223,6 @@ "lightning__transfer_hw__sign_title" = "Υπόγραψε με\nτη συσκευή σου"; "lightning__transfer_hw__signed_title" = "Η συναλλαγή\nυπογράφηκε"; "lightning__transfer_to_savings_button" = "Μεταφορά στις Αποταμιεύσεις"; -"lightning__transfer_to_spending_button" = "Μεταφορά στις Δαπάνες"; "lightning__spending_intro__title" = "Μεταφορά\nστις δαπάνες"; "lightning__spending_intro__text" = "Μεταφέρετε κεφάλαια στο υπόλοιπο δαπανών σας για να απολαύσετε άμεσες και φθηνές συναλλαγές με φίλους, οικογένεια και εμπόρους."; "lightning__spending_intro__button" = "Ξεκινήστε"; @@ -343,6 +340,8 @@ "lightning__channel_node_id" = "Peer ID"; "lightning__channel_id" = "Channel ID"; "lightning__channel_point" = "Channel point"; +"lightning__channels_closing_banner" = "ΤΑ ΚΑΝΑΛΙΑ ΚΛΕΙΝΟΥΝ ΣΤΙΣ {date}"; +"lightning__channels_closing_notice" = "Τα κανάλια Lightning σας θα κλείσουν στις {date}. Μεταφέρετε το υπόλοιπο δαπανών σε αποταμιεύσεις πριν από τότε."; "lightning__closure_reason" = "Λόγος κλεισίματος"; "lightning__support" = "Υποστήριξη"; "lightning__connection_closed__title" = "Η Σύνδεση Έκλεισε"; @@ -489,15 +488,6 @@ "other__try_again" = "Δοκιμάστε ξανά."; "other__scan__error__generic" = "Το Bitkit δεν μπορεί να διαβάσει τα παρεχόμενα δεδομένα."; "other__scan__error__expired" = "Αυτό το τιμολόγιο Lightning έχει λήξει."; -"other__gift__claimed__text" = "Λυπούμαστε, αργήσατε! Όλα τα δώρα για αυτόν τον κωδικό έχουν ήδη διεκδικηθεί."; -"other__gift__claiming__text" = "Διεκδίκηση του κωδικού δώρου Bitkit σας..."; -"other__gift__claiming__title" = "Διεκδίκηση Δώρου"; -"other__gift__error__title" = "Σφάλμα Κωδικού Δώρου"; -"other__gift__error__text" = "Το Bitkit δεν μπόρεσε να διεκδικήσει τα κεφάλαια. Παρακαλώ δοκιμάστε ξανά αργότερα ή επικοινωνήστε μαζί μας."; -"other__gift__error__button" = "ΟΚ"; -"other__gift__used__title" = "Χρησιμοποιημένος Κωδικός"; -"other__gift__used__text" = "Αυτός ο κωδικός δώρου Bitkit έχει ήδη χρησιμοποιηθεί και τα κεφάλαια έχουν εκταμιευθεί."; -"other__gift__used__button" = "ΟΚ"; "other__shop__intro__title" = "Κατάστημα"; "other__shop__intro__description" = "Ζήστε τη ζωή σας με το πρότυπο Bitcoin. Ξοδέψτε τα Bitcoin σας σε ψηφιακές δωροκάρτες, eSIMs, ανανεώσεις τηλεφώνου και άλλα."; "other__shop__intro__button" = "Ξεκινήστε"; @@ -1161,9 +1151,6 @@ "wallet__receive_show_qr" = "Εμφάνιση Κωδικού QR"; "wallet__receive_background_setup_text" = "Ενεργοποιήστε τη ρύθμιση παρασκηνίου για να βγείτε με ασφάλεια από το Bitkit ενώ διαμορφώνεται το υπόλοιπό σας."; "wallet__receive_background_setup_toggle" = "Ενεργοποίηση ρύθμισης παρασκηνίου"; -"wallet__receive_spending" = "Λήψη κεφαλαίων Lightning"; -"wallet__receive_spending_text" = "Απολαύστε άμεσες και φθηνές πληρωμές bitcoin στο Lightning Network."; -"wallet__receive_spending_title" = "Λήψη στο υπόλοιπο δαπανών"; "wallet__receive_connect_initial" = "Για τη ρύθμιση του υπολοίπου δαπανών σας, θα αφαιρεθεί τέλος δικτύου {networkFee} και τέλος παρόχου υπηρεσιών {serviceFee}."; "wallet__receive_connect_additional" = "Για να λάβετε περισσότερα άμεσα Bitcoin, το Bitkit πρέπει να αυξήσει τη ρευστότητά σας. Τέλος δικτύου {networkFee} και τέλος παρόχου υπηρεσιών {serviceFee} θα αφαιρεθούν από το ποσό που καθορίσατε."; "wallet__receive_liquidity__nav_title" = "Ρύθμιση Υπολοίπου Δαπανών"; diff --git a/Bitkit/Resources/Localization/en.lproj/Localizable.strings b/Bitkit/Resources/Localization/en.lproj/Localizable.strings index 5a3c17e2f..56a9391e2 100644 --- a/Bitkit/Resources/Localization/en.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/en.lproj/Localizable.strings @@ -9,8 +9,6 @@ "cards__notifications__title" = "Get Paid"; "cards__invite__title" = "Invite"; "cards__invite__description" = "Share Bitkit"; -"cards__lightning__title" = "Spend"; -"cards__lightning__description" = "Instant payments"; "cards__lightningSettingUp__title" = "Transfer"; "cards__lightningSettingUp__description" = "In progress..."; "cards__lightningConnecting__title" = "Connecting"; @@ -223,7 +221,6 @@ "lightning__transfer_hw__sign_title" = "Sign with\nyour device"; "lightning__transfer_hw__signed_title" = "Transaction\nsigned"; "lightning__transfer_to_savings_button" = "Transfer To Savings"; -"lightning__transfer_to_spending_button" = "Transfer To Spending"; "lightning__spending_intro__title" = "Transfer\nto spending"; "lightning__spending_intro__text" = "Transfer funds to your spending balance to enjoy instant and cheap transactions with friends, family, and merchants."; "lightning__spending_intro__button" = "Get Started"; @@ -365,6 +362,8 @@ "lightning__channel_node_id" = "Peer ID"; "lightning__channel_id" = "Channel ID"; "lightning__channel_point" = "Channel point"; +"lightning__channels_closing_banner" = "CHANNELS CLOSE ON {date}"; +"lightning__channels_closing_notice" = "Your Lightning channels will be closed on {date}. Move your spending balance to savings before then."; "lightning__closure_reason" = "Closure reason"; "lightning__support" = "Support"; "lightning__connection_closed__title" = "Connection Closed"; @@ -563,15 +562,6 @@ "other__transfer_notification__body" = "Open Bitkit to complete your transfer"; "other__scan__error__generic" = "Bitkit is unable to read the provided data."; "other__scan__error__expired" = "This Lightning invoice has expired."; -"other__gift__claimed__text" = "Sorry, you're too late! All gifts for this code have already been claimed."; -"other__gift__claiming__text" = "Claiming your Bitkit gift code..."; -"other__gift__claiming__title" = "Claiming Gift"; -"other__gift__error__title" = "Gift Code Error"; -"other__gift__error__text" = "Bitkit couldn\'t claim the funds. Please try again later or contact us."; -"other__gift__error__button" = "OK"; -"other__gift__used__title" = "Used Code"; -"other__gift__used__text" = "This Bitkit gift code has already been used, and the funds have been paid out."; -"other__gift__used__button" = "OK"; "other__shop__intro__title" = "Shop"; "other__shop__intro__description" = "Get your life on the Bitcoin standard. Spend your Bitcoin on digital gift cards, eSIMs, phone refills, and more."; "other__shop__intro__button" = "Get Started"; @@ -1410,9 +1400,6 @@ "wallet__receive_text_lnfunds" = "Want to receive Lightning funds?"; "wallet__receive_background_setup_text" = "Enable background setup to safely exit Bitkit while your balance is being configured."; "wallet__receive_background_setup_toggle" = "Enable background setup"; -"wallet__receive_spending" = "Receive Lightning Funds"; -"wallet__receive_spending_text" = "Enjoy instant and cheap bitcoin payments on the Lightning Network."; -"wallet__receive_spending_title" = "Receive on spending balance"; "wallet__receive_connect_initial" = "To set up your spending balance, a {networkFee} network fee and {serviceFee} service provider fee will be deducted."; "wallet__receive_connect_additional" = "To receive more instant Bitcoin, Bitkit has to increase your liquidity. A {networkFee} network fee and {serviceFee} service provider fee will be deducted from the amount you specified."; "wallet__receive_liquidity__nav_title" = "Spending Balance Setup"; diff --git a/Bitkit/Resources/Localization/es-419.lproj/Localizable.strings b/Bitkit/Resources/Localization/es-419.lproj/Localizable.strings index 4a6cc334b..ef4c4e819 100644 --- a/Bitkit/Resources/Localization/es-419.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/es-419.lproj/Localizable.strings @@ -9,8 +9,6 @@ "cards__notifications__title" = "Cobrar"; "cards__invite__title" = "Invitar"; "cards__invite__description" = "Compatir BitKit"; -"cards__lightning__title" = "Gastar"; -"cards__lightning__description" = "Pagos instantáneos"; "cards__lightningSettingUp__title" = "Transferir"; "cards__lightningSettingUp__description" = "En curso"; "cards__lightningConnecting__title" = "Conectandose"; @@ -231,7 +229,6 @@ "lightning__transfer_hw__sign_title" = "Firme con\nsu dispositivo"; "lightning__transfer_hw__signed_title" = "Transacción\nfirmada"; "lightning__transfer_to_savings_button" = "Transferir a ahorros"; -"lightning__transfer_to_spending_button" = "Transferir a gastos"; "lightning__spending_intro__title" = "Transferir\na gastos"; "lightning__spending_intro__text" = "Transfiera fondos a su saldo de gastos para disfrutar de transacciones baratas e instantáneas con amigos y comercios."; "lightning__spending_intro__button" = "Empezar"; @@ -373,6 +370,8 @@ "lightning__channel_node_id" = "ID de par"; "lightning__channel_id" = "Canal ID"; "lightning__channel_point" = "Punto del canal"; +"lightning__channels_closing_banner" = "LOS CANALES CIERRAN EL {date}"; +"lightning__channels_closing_notice" = "Tus canales Lightning se cerrarán el {date}. Transfiere tu saldo de gasto a ahorros antes de esa fecha."; "lightning__closure_reason" = "Motivo del cierre"; "lightning__support" = "Soporte"; "lightning__connection_closed__title" = "Conexión cerrada"; @@ -574,15 +573,6 @@ "other__transfer_notification__body" = "Abra Bitkit para completar su transferencia"; "other__scan__error__generic" = "Bitkit no puede leer los datos proporcionados."; "other__scan__error__expired" = "Esta factura lightning ha caducado."; -"other__gift__claimed__text" = "¡Lo sentimos, llegaste tarde! Todos los regalos para este código ya han sido reclamados."; -"other__gift__claiming__text" = "Canjeando su código de regalo Bitkit..."; -"other__gift__claiming__title" = "Canjeando regalo"; -"other__gift__error__title" = "Error de código de regalo"; -"other__gift__error__text" = "Bitkit no pudo recibir los fondos. Por favor, intente de nuevo más tarde o póngase en contacto con nosotros."; -"other__gift__error__button" = "OK"; -"other__gift__used__title" = "Código usado"; -"other__gift__used__text" = "Este código de regalo de Bitkit ya ha sido utilizado y los fondos han sido pagados."; -"other__gift__used__button" = "OK"; "other__shop__intro__title" = "Tienda"; "other__shop__intro__description" = "Adopta el estándar Bitcoin. Gasta tus Bitcoin en tarjetas de regalo digitales, eSIMs, recargas telefónicas y más."; "other__shop__intro__button" = "Empezar"; @@ -1399,9 +1389,6 @@ "wallet__receive_text_lnfunds" = "Quiero recibir"; "wallet__receive_background_setup_text" = "Activa la configuración en segundo plano para salir de Bitkit de forma segura mientras se configura tu saldo."; "wallet__receive_background_setup_toggle" = "Activar configuración en segundo plano"; -"wallet__receive_spending" = "Recibir en Saldo de Gastos"; -"wallet__receive_spending_text" = "Disfrute de pagos de bitcoin instantáneos y baratos en la red Lightning."; -"wallet__receive_spending_title" = "Recibir en saldo de gastos"; "wallet__receive_connect_initial" = "Para configurar tu saldo para gastar, se descontará una comisión de red de {networkFee} y una comisión del proveedor de servicio de {serviceFee}."; "wallet__receive_connect_additional" = "Para recibir más Bitcoin al instante, Bitkit necesita aumentar tu liquidez. Se descontará una comisión de red de {networkFee} y una comisión del proveedor de servicio de {serviceFee} del monto que especificaste."; "wallet__receive_liquidity__nav_title" = "Configuración del saldo de gastos"; diff --git a/Bitkit/Resources/Localization/es.lproj/Localizable.strings b/Bitkit/Resources/Localization/es.lproj/Localizable.strings index 47cd807dc..5ddb7d2b7 100644 --- a/Bitkit/Resources/Localization/es.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/es.lproj/Localizable.strings @@ -7,8 +7,6 @@ "cards__notifications__title" = "Recibir Pagos"; "cards__invite__title" = "Invitar"; "cards__invite__description" = "Comparta Bitkit"; -"cards__lightning__title" = "Gastar"; -"cards__lightning__description" = "Pagos instantáneos"; "cards__lightningSettingUp__title" = "Transferir"; "cards__lightningSettingUp__description" = "En progreso..."; "cards__lightningConnecting__title" = "Conectando"; @@ -227,7 +225,6 @@ "lightning__transfer_hw__sign_title" = "Firme con\nsu dispositivo"; "lightning__transfer_hw__signed_title" = "Transacción\nfirmada"; "lightning__transfer_to_savings_button" = "Transferir a Ahorros"; -"lightning__transfer_to_spending_button" = "Transferir a Gasto"; "lightning__spending_intro__title" = "Transferir\na gasto"; "lightning__spending_intro__text" = "Transfiera fondos a su saldo de gasto para disfrutar de transacciones baratas e instantáneas con amigos, familia y comerciantes."; "lightning__spending_intro__button" = "Empezar"; @@ -357,6 +354,8 @@ "lightning__channel_node_id" = "ID del par"; "lightning__channel_id" = "ID del canal"; "lightning__channel_point" = "Punto del canal"; +"lightning__channels_closing_banner" = "LOS CANALES CIERRAN EL {date}"; +"lightning__channels_closing_notice" = "Tus canales Lightning se cerrarán el {date}. Transfiere tu saldo de gasto a ahorros antes de esa fecha."; "lightning__closure_reason" = "Motivo del cierre"; "lightning__support" = "Soporte"; "lightning__connection_closed__title" = "Conexión Cerrada"; @@ -532,15 +531,6 @@ "other__try_again" = "Por favor, intente de nuevo."; "other__scan__error__generic" = "Bitkit no puede leer los datos proporcionados."; "other__scan__error__expired" = "Esta factura Lightning ha expirado."; -"other__gift__claimed__text" = "Lo sentimos, ¡llegas tarde! Todos los regalos para este código ya han sido reclamados."; -"other__gift__claiming__text" = "Reclamando tu código de regalo de Bitkit..."; -"other__gift__claiming__title" = "Reclamando Regalo"; -"other__gift__error__title" = "Error en el Código de Regalo"; -"other__gift__error__text" = "Bitkit no pudo reclamar los fondos. Por favor, inténtelo más tarde o contáctenos."; -"other__gift__error__button" = "OK"; -"other__gift__used__title" = "Código Usado"; -"other__gift__used__text" = "Este código de regalo de Bitkit ya ha sido usado, y los fondos ya han sido pagados."; -"other__gift__used__button" = "OK"; "other__shop__intro__title" = "Tienda"; "other__shop__intro__description" = "Vive tu vida con el estándar Bitcoin. Gasta tu Bitcoin en tarjetas regalo digitales, eSIMs, recargas de teléfono y más."; "other__shop__intro__button" = "Empezar"; @@ -1296,9 +1286,6 @@ "wallet__receive_show_qr" = "Mostrar código QR"; "wallet__receive_background_setup_text" = "Activa la configuración en segundo plano para salir de Bitkit de forma segura mientras tu saldo se está configurando."; "wallet__receive_background_setup_toggle" = "Activar configuración en segundo plano"; -"wallet__receive_spending" = "Recibir Fondos Lightning"; -"wallet__receive_spending_text" = "Disfrute de pagos de bitcoin instantáneos y baratos en la red Lightning."; -"wallet__receive_spending_title" = "Recibir en saldo de gasto"; "wallet__receive_connect_initial" = "Para configurar tu saldo de gasto, se deducirá una comisión de red de {networkFee} y una comisión del proveedor de servicio de {serviceFee}."; "wallet__receive_connect_additional" = "Para recibir más Bitcoin instantáneo, Bitkit tiene que aumentar tu liquidez. Una comisión de red de {networkFee} y una comisión del proveedor de servicio de {serviceFee} se deducirán de la cantidad que especificaste."; "wallet__receive_liquidity__nav_title" = "Configuración del Saldo de Gasto"; diff --git a/Bitkit/Resources/Localization/fr.lproj/Localizable.strings b/Bitkit/Resources/Localization/fr.lproj/Localizable.strings index 2535a6350..caba4cfa6 100644 --- a/Bitkit/Resources/Localization/fr.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/fr.lproj/Localizable.strings @@ -9,8 +9,6 @@ "cards__notifications__title" = "Soyez rémunéré"; "cards__invite__title" = "Inviter"; "cards__invite__description" = "Partager Bitkit"; -"cards__lightning__title" = "Dépenser"; -"cards__lightning__description" = "Paiements instantanés"; "cards__lightningSettingUp__title" = "Transfert"; "cards__lightningSettingUp__description" = "En cours..."; "cards__lightningConnecting__title" = "Connexion"; @@ -231,7 +229,6 @@ "lightning__transfer_hw__sign_title" = "Signez avec\nvotre appareil"; "lightning__transfer_hw__signed_title" = "Transaction\nsignée"; "lightning__transfer_to_savings_button" = "Transférer vers l\'épargne"; -"lightning__transfer_to_spending_button" = "Transférer vers les dépenses"; "lightning__spending_intro__title" = "Transfert\naux dépenses"; "lightning__spending_intro__text" = "Transférez des fonds vers votre solde Dépenses pour profiter de transactions instantanées et moins chers avec vos amis, votre famille et les commerçants."; "lightning__spending_intro__button" = "Commencer"; @@ -373,6 +370,8 @@ "lightning__channel_node_id" = "ID du pair"; "lightning__channel_id" = "ID du canal"; "lightning__channel_point" = "Point du canal"; +"lightning__channels_closing_banner" = "LES CANAUX FERMENT LE {date}"; +"lightning__channels_closing_notice" = "Vos canaux Lightning seront fermés le {date}. Transférez votre solde de dépenses vers l'Épargne avant cette date."; "lightning__closure_reason" = "Motif de la fermeture"; "lightning__support" = "Support"; "lightning__connection_closed__title" = "Connexion fermée"; @@ -574,15 +573,6 @@ "other__transfer_notification__body" = "Ouvrez Bitkit pour terminer votre transfert"; "other__scan__error__generic" = "Bitkit ne peut pas lire les données fournies."; "other__scan__error__expired" = "Cette facture Lightning a expiré."; -"other__gift__claimed__text" = "Désolé, vous arrivez trop tard ! Tous les cadeaux pour ce code ont déjà été réclamés."; -"other__gift__claiming__text" = "Réclamer votre code cadeau Bitkit..."; -"other__gift__claiming__title" = "Réclamer un cadeau"; -"other__gift__error__title" = "Erreur de code cadeau"; -"other__gift__error__text" = "Bitkit n\'a pas pu réclamer les fonds. Veuillez réessayer plus tard ou nous contacter."; -"other__gift__error__button" = "OK"; -"other__gift__used__title" = "Code utilisé"; -"other__gift__used__text" = "Ce code cadeau Bitkit a déjà été utilisé et les fonds ont été versés."; -"other__gift__used__button" = "OK"; "other__shop__intro__title" = "Boutique"; "other__shop__intro__description" = "Passez à la norme Bitcoin. Dépensez vos bitcoins en cartes-cadeaux numériques, eSIM, recharges téléphoniques, et plus encore."; "other__shop__intro__button" = "Commencer"; @@ -1407,9 +1397,6 @@ "wallet__receive_text_lnfunds" = "Vous souhaitez recevoir des fonds Lightning?"; "wallet__receive_background_setup_text" = "Activez la configuration en arrière-plan pour quitter Bitkit en toute sécurité pendant que votre solde est configuré."; "wallet__receive_background_setup_toggle" = "Activer la configuration en arrière-plan"; -"wallet__receive_spending" = "Recevoir sur le compte de dépenses"; -"wallet__receive_spending_text" = "Profitez de paiements en bitcoins instantanés et bon marché sur le Lightning Network."; -"wallet__receive_spending_title" = "Recevoir sur le solde de dépenses"; "wallet__receive_connect_initial" = "Pour établir votre solde Dépenses, un {networkFee} frais de réseau et {serviceFee} frais de fournisseur de services seront déduits."; "wallet__receive_connect_additional" = "Pour recevoir plus de bitcoins instantanés, Bitkit doit augmenter vos liquidités. Des frais de réseau de {networkFee} et des frais de fournisseur de services de {serviceFee} seront déduits du montant que vous avez spécifié."; "wallet__receive_liquidity__nav_title" = "Configuration du solde compte Dépenses"; diff --git a/Bitkit/Resources/Localization/it.lproj/Localizable.strings b/Bitkit/Resources/Localization/it.lproj/Localizable.strings index 8b73e25a9..9d929f19e 100644 --- a/Bitkit/Resources/Localization/it.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/it.lproj/Localizable.strings @@ -8,8 +8,6 @@ "cards__notifications__title" = "Ricevi Pagamenti"; "cards__invite__title" = "Invita"; "cards__invite__description" = "Condividi Bitkit"; -"cards__lightning__title" = "Spendi"; -"cards__lightning__description" = "Pagamenti istantanei"; "cards__lightningSettingUp__title" = "Trasferisci"; "cards__lightningSettingUp__description" = "In corso..."; "cards__lightningConnecting__title" = "Collegamento"; @@ -228,7 +226,6 @@ "lightning__transfer_hw__sign_title" = "Firma con\nil tuo dispositivo"; "lightning__transfer_hw__signed_title" = "Transazione\nfirmata"; "lightning__transfer_to_savings_button" = "Trasferisci Ai Risparmi"; -"lightning__transfer_to_spending_button" = "Trasferisci A Spesa"; "lightning__spending_intro__title" = "Trasferisci\na spesa"; "lightning__spending_intro__text" = "Trasferisci fondi al tuo conto di spesa per usufruire di transazioni istantanee ed economiche con amici, familiari e commercianti."; "lightning__spending_intro__button" = "Inizia"; @@ -366,6 +363,8 @@ "lightning__channel_node_id" = "ID del nodo"; "lightning__channel_id" = "ID Canale"; "lightning__channel_point" = "Punto canale"; +"lightning__channels_closing_banner" = "I CANALI CHIUDONO IL {date}"; +"lightning__channels_closing_notice" = "I tuoi canali Lightning verranno chiusi il {date}. Trasferisci il saldo di spesa ai risparmi prima di allora."; "lightning__closure_reason" = "Motivo della chiusura"; "lightning__support" = "Supporto"; "lightning__connection_closed__title" = "Connessione chiusa"; @@ -565,15 +564,6 @@ "other__try_again" = "Per favore riprova."; "other__scan__error__generic" = "Bitkit non è in grado di leggere i dati forniti."; "other__scan__error__expired" = "Questa fattura Lightning è scaduta."; -"other__gift__claimed__text" = "Spiacente, sei arrivato troppo tardi! Tutti i regali per questo codice sono già stati riscattati."; -"other__gift__claiming__text" = "Riscatto del tuo codice regalo Bitkit..."; -"other__gift__claiming__title" = "Riscatto Regalo"; -"other__gift__error__title" = "Errore Codice Regalo"; -"other__gift__error__text" = "Bitkit non è riuscito a riscattare i fondi. Riprova più tardi o contattaci."; -"other__gift__error__button" = "OK"; -"other__gift__used__title" = "Codice Usato"; -"other__gift__used__text" = "Questo codice regalo Bitkit è già stato utilizzato e i fondi sono stati erogati."; -"other__gift__used__button" = "OK"; "other__shop__intro__title" = "Shop"; "other__shop__intro__description" = "Porta la tua vita sullo standard Bitcoin. Spendi i tuoi Bitcoin in gift card digitali, eSIM, ricariche telefoniche e altro."; "other__shop__intro__button" = "Inizia"; @@ -1371,9 +1361,6 @@ "wallet__receive_text_lnfunds" = "Vuoi ricevere fondi Lightning?"; "wallet__receive_background_setup_text" = "Abilita la configurazione in background per uscire da Bitkit in sicurezza mentre il tuo saldo viene configurato."; "wallet__receive_background_setup_toggle" = "Abilita configurazione in background"; -"wallet__receive_spending" = "Ricevi fondi Lightning"; -"wallet__receive_spending_text" = "Goditi pagamenti bitcoin istantanei ed economici sulla Lightning Network."; -"wallet__receive_spending_title" = "Ricevi su saldo di spesa"; "wallet__receive_connect_initial" = "Per configurare il tuo conto di spesa, verranno dedotte una commissione di rete di {networkFee} e una commissione del fornitore di servizi di {serviceFee}."; "wallet__receive_connect_additional" = "Per ricevere altri Bitcoin istantanei, Bitkit deve aumentare la tua liquidità. Una commissione di rete di {networkFee} e una commissione del fornitore di servizi di {serviceFee} verranno dedotte dall'importo specificato."; "wallet__receive_liquidity__nav_title" = "Configurazione Saldo di Spesa"; diff --git a/Bitkit/Resources/Localization/nl.lproj/Localizable.strings b/Bitkit/Resources/Localization/nl.lproj/Localizable.strings index 1bf09b447..205b57d0e 100644 --- a/Bitkit/Resources/Localization/nl.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/nl.lproj/Localizable.strings @@ -8,8 +8,6 @@ "cards__notifications__title" = "Ontvang"; "cards__invite__title" = "Uitnodiging"; "cards__invite__description" = "Deel Bitkit"; -"cards__lightning__title" = "Besteed"; -"cards__lightning__description" = "Onmiddellijke betalingen"; "cards__lightningSettingUp__title" = "Overboeken"; "cards__lightningSettingUp__description" = "Bezig..."; "cards__lightningConnecting__title" = "Verbinding aan het maken"; @@ -230,7 +228,6 @@ "lightning__transfer_hw__sign_title" = "Onderteken met\nuw apparaat"; "lightning__transfer_hw__signed_title" = "Transactie\nondertekend"; "lightning__transfer_to_savings_button" = "Overboeken Naar Spaargeld"; -"lightning__transfer_to_spending_button" = "Overboeken Naar Bestedingssaldo"; "lightning__spending_intro__title" = "Overboeking\nnaar bestedingssaldo"; "lightning__spending_intro__text" = "Boek geld over naar uw bestedingssaldo om te genieten van directe en goedkope transacties met vrienden, familie en winkeliers."; "lightning__spending_intro__button" = "Starten"; @@ -370,6 +367,8 @@ "lightning__channel_node_id" = "Peer-ID"; "lightning__channel_id" = "Kanaal-ID"; "lightning__channel_point" = "Kanaalpunt"; +"lightning__channels_closing_banner" = "KANALEN SLUITEN OP {date}"; +"lightning__channels_closing_notice" = "Je Lightning-kanalen worden op {date} gesloten. Boek je bestedingssaldo vóór die datum over naar spaargeld."; "lightning__closure_reason" = "Reden van sluiting"; "lightning__support" = "Ondersteuning"; "lightning__connection_closed__title" = "Verbinding Gesloten"; @@ -571,15 +570,6 @@ "other__transfer_notification__body" = "Öffnen Sie Bitkit, um Ihre Übertragung abzuschließen"; "other__scan__error__generic" = "Bitkit kan de verstrekte data niet lezen."; "other__scan__error__expired" = "Deze Lightning factuur is verlopen."; -"other__gift__claimed__text" = "Sorry, u bent te laat! Alle cadeaus voor deze code zijn al geclaimd."; -"other__gift__claiming__text" = "Uw Bitkit cadeau code wordt geclaimd..."; -"other__gift__claiming__title" = "Cadeau Claimen"; -"other__gift__error__title" = "Cadeau Code Fout"; -"other__gift__error__text" = "Bitkit kon het geld niet claimen. Probeer het later opnieuw of neem contact met ons op."; -"other__gift__error__button" = "OK"; -"other__gift__used__title" = "Gebruikte Code"; -"other__gift__used__text" = "Deze Bitkit cadeau code is al gebruikt en het geld is uitbetaald."; -"other__gift__used__button" = "OK"; "other__shop__intro__title" = "Winkel"; "other__shop__intro__description" = "Zet uw leven op de Bitcoin standaard. Besteed uw Bitcoin aan digitale cadeaukaarten, eSIMs, telefoon opwaarderingen en meer."; "other__shop__intro__button" = "Starten"; @@ -1402,9 +1392,6 @@ "wallet__receive_text_lnfunds" = "Wilt u Lightning ₿ ontvangen?"; "wallet__receive_background_setup_text" = "Schakel achtergrondinstallatie in om Bitkit veilig te verlaten terwijl uw saldo wordt geconfigureerd."; "wallet__receive_background_setup_toggle" = "Achtergrondinstallatie inschakelen"; -"wallet__receive_spending" = "Ontvang Lightning-geld"; -"wallet__receive_spending_text" = "Geniet van directe en goedkope bitcoin-betalingen op het Lightning Network."; -"wallet__receive_spending_title" = "Ontvang op bestedingssaldo"; "wallet__receive_connect_initial" = "Om uw bestedingssaldo in te stellen, worden een {networkFee} netwerkvergoeding en {serviceFee} serviceprovidervergoeding afgetrokken."; "wallet__receive_connect_additional" = "Om meer direct Bitcoin te ontvangen, moet Bitkit uw liquiditeit verhogen. Een {networkFee} netwerkvergoeding en {serviceFee} serviceprovidervergoeding worden afgetrokken van het bedrag dat u heeft opgegeven."; "wallet__receive_liquidity__nav_title" = "Bestedingssaldo Instellen"; diff --git a/Bitkit/Resources/Localization/pl.lproj/Localizable.strings b/Bitkit/Resources/Localization/pl.lproj/Localizable.strings index 1df98c9fd..6e90fa1d5 100644 --- a/Bitkit/Resources/Localization/pl.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/pl.lproj/Localizable.strings @@ -8,8 +8,6 @@ "cards__notifications__title" = "Otrzymuj płatności"; "cards__invite__title" = "Zaproś"; "cards__invite__description" = "Udostępnij Bitkit"; -"cards__lightning__title" = "Wydawaj"; -"cards__lightning__description" = "Płatności natychmiastowe"; "cards__lightningSettingUp__title" = "Transfer"; "cards__lightningSettingUp__description" = "W toku..."; "cards__lightningConnecting__title" = "Łączenie"; @@ -230,7 +228,6 @@ "lightning__transfer_hw__sign_title" = "Podpisz\nurządzeniem"; "lightning__transfer_hw__signed_title" = "Transakcja\npodpisana"; "lightning__transfer_to_savings_button" = "Prześlij na oszczędności"; -"lightning__transfer_to_spending_button" = "Prześlij na wydatki"; "lightning__spending_intro__title" = "Prześlij \nna saldo wydatków"; "lightning__spending_intro__text" = "Przelewaj środki na swoje saldo wydatków, aby cieszyć się natychmiastowymi i tanimi transakcjami z przyjaciółmi, rodziną i sprzedawcami."; "lightning__spending_intro__button" = "Rozpocznij"; @@ -372,6 +369,8 @@ "lightning__channel_node_id" = "ID węzła"; "lightning__channel_id" = "ID kanału"; "lightning__channel_point" = "Punkt kanału"; +"lightning__channels_closing_banner" = "KANAŁY ZOSTANĄ ZAMKNIĘTE {date}"; +"lightning__channels_closing_notice" = "Twoje kanały Lightning zostaną zamknięte {date}. Przed tym terminem prześlij saldo wydatków na oszczędności."; "lightning__closure_reason" = "Powód zamknięcia"; "lightning__support" = "Wsparcie"; "lightning__connection_closed__title" = "Połączenie zamknięte"; @@ -573,15 +572,6 @@ "other__transfer_notification__body" = "Otwórz Bitkit, aby dokończyć transfer"; "other__scan__error__generic" = "Bitkit nie może odczytać dostarczonych danych."; "other__scan__error__expired" = "Ta faktura Lightning wygasła."; -"other__gift__claimed__text" = "Przepraszamy, spóźniłeś się! Wszystkie prezenty dla tego kodu zostały już odebrane."; -"other__gift__claiming__text" = "Odbiór kodu podarunkowego Bitkit..."; -"other__gift__claiming__title" = "Odbiór prezentu"; -"other__gift__error__title" = "Błąd kodu podarunkowego"; -"other__gift__error__text" = "Bitkit nie mógł odebrać środków. Proszę spróbować ponownie później lub skontaktować się z nami."; -"other__gift__error__button" = "OK"; -"other__gift__used__title" = "Użyty kod"; -"other__gift__used__text" = "Ten kod podarunkowy Bitkit został już wykorzystany, a środki zostały wypłacone."; -"other__gift__used__button" = "OK"; "other__shop__intro__title" = "Sklep"; "other__shop__intro__description" = "Przestaw się na standard Bitcoina. Wydawaj swoje Bitcoiny na karty podarunkowe, eSIM-y, doładowania telefonu i inne usługi."; "other__shop__intro__button" = "Rozpocznij"; @@ -1406,9 +1396,6 @@ "wallet__receive_text_lnfunds" = "Chcesz otrzymać fundusze Lightning?"; "wallet__receive_background_setup_text" = "Włącz konfigurację w tle, aby bezpiecznie wyjść z Bitkit podczas konfigurowania salda."; "wallet__receive_background_setup_toggle" = "Włącz konfigurację w tle"; -"wallet__receive_spending" = "Odbierz środki Lightning"; -"wallet__receive_spending_text" = "Korzystaj z natychmiastowych i tanich płatności bitcoin w sieci Lightning."; -"wallet__receive_spending_title" = "Odbierz na saldo wydatków"; "wallet__receive_connect_initial" = "Aby skonfigurować saldo wydatków, zostanie pobrana opłata sieciowa {networkFee} oraz opłata dostawcy usługi {serviceFee}"; "wallet__receive_connect_additional" = "Bitkit musi zwiększyć Twoją płynność, abyś mógł otrzymywać więcej natychmiastowych płatności. Od wskazanej kwoty zostanie pobrana opłata sieciowa {networkFee} i serwisowa {serviceFee}."; "wallet__receive_liquidity__nav_title" = "Konfiguracja salda wydatków"; diff --git a/Bitkit/Resources/Localization/pt-BR.lproj/Localizable.strings b/Bitkit/Resources/Localization/pt-BR.lproj/Localizable.strings index 6b4158f28..2f6b4329d 100644 --- a/Bitkit/Resources/Localization/pt-BR.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/pt-BR.lproj/Localizable.strings @@ -8,8 +8,6 @@ "cards__notifications__title" = "Receba Pagamentos"; "cards__invite__title" = "Convide"; "cards__invite__description" = "Compartilhe a Bitkit"; -"cards__lightning__title" = "Pagar"; -"cards__lightning__description" = "Pagamentos instantâneos"; "cards__lightningSettingUp__title" = "Transferência"; "cards__lightningSettingUp__description" = "Em andamento..."; "cards__lightningConnecting__title" = "Conectando"; @@ -230,7 +228,6 @@ "lightning__transfer_hw__sign_title" = "Assine com\nseu dispositivo"; "lightning__transfer_hw__signed_title" = "Transação\nassinada"; "lightning__transfer_to_savings_button" = "Transferir para poupança"; -"lightning__transfer_to_spending_button" = "Transferir para gastos"; "lightning__spending_intro__title" = "Transferir\npara gastos"; "lightning__spending_intro__text" = "Transfira fundos para seu saldo de gastos para desfrutar de transações instantâneas e baratas com amigos, familiares e comerciantes."; "lightning__spending_intro__button" = "Começar"; @@ -372,6 +369,8 @@ "lightning__channel_node_id" = "ID do Peer"; "lightning__channel_id" = "ID do canal"; "lightning__channel_point" = "Ponto do canal"; +"lightning__channels_closing_banner" = "CANAIS FECHAM EM {date}"; +"lightning__channels_closing_notice" = "Seus canais Lightning serão fechados em {date}. Transfira seu saldo de gastos para a poupança antes dessa data."; "lightning__closure_reason" = "Motivo do fechamento"; "lightning__support" = "Suporte"; "lightning__connection_closed__title" = "Conexão Desativada"; @@ -573,15 +572,6 @@ "other__transfer_notification__body" = "Abra a Bitkit para concluir sua transferência"; "other__scan__error__generic" = "Bitkit não consegue ler os dados fornecidos."; "other__scan__error__expired" = "Este invoice lightning expirou."; -"other__gift__claimed__text" = "Desculpe, você chegou tarde! Todos os presentes para este código já foram resgatados."; -"other__gift__claiming__text" = "Reivindicando seu gitf code Bitkit..."; -"other__gift__claiming__title" = "Reivindicando presente"; -"other__gift__error__title" = "Erro no código de presente"; -"other__gift__error__text" = "A Bitkit não pôde reivindicar os fundos. Tente novamente mais tarde ou entre em contato conosco."; -"other__gift__error__button" = "OK"; -"other__gift__used__title" = "Código usado"; -"other__gift__used__text" = "Este gift code da Bitkit já foi usado e os fundos já foram pagos."; -"other__gift__used__button" = "OK"; "other__shop__intro__title" = "Comprar"; "other__shop__intro__description" = "Coloque sua vida no padrão Bitcoin. Gaste seu Bitcoin em cartões-presente digitais, eSIMs, recargas de telefone e muito mais."; "other__shop__intro__button" = "Começar"; @@ -1407,9 +1397,6 @@ "wallet__receive_text_lnfunds" = "Deseja receber via Lightning?"; "wallet__receive_background_setup_text" = "Ative a configuração em segundo plano para sair do Bitkit com segurança enquanto seu saldo está sendo configurado."; "wallet__receive_background_setup_toggle" = "Ativar configuração em segundo plano"; -"wallet__receive_spending" = "Receber no Saldo de Gastos"; -"wallet__receive_spending_text" = "Aproveite pagamentos em bitcoin instantâneos e baratos na Lightning Network."; -"wallet__receive_spending_title" = "Receber no saldo de gastos"; "wallet__receive_connect_initial" = "Para configurar seu saldo de gastos, uma taxa de rede de {networkFee} e {serviceFee} de taxa do provedor de serviços serão deduzidas."; "wallet__receive_connect_additional" = "Para receber mais Bitcoin instantâneo, a Bitkit precisa aumentar sua liquidez. A taxa de rede de {networkFee} e {serviceFee} de taxa do provedor de serviços serão deduzidas do valor que você especificou."; "wallet__receive_liquidity__nav_title" = "Setup do saldo de gastos"; diff --git a/Bitkit/Resources/Localization/pt.lproj/Localizable.strings b/Bitkit/Resources/Localization/pt.lproj/Localizable.strings index 322ddc8fb..eb935e796 100644 --- a/Bitkit/Resources/Localization/pt.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/pt.lproj/Localizable.strings @@ -59,8 +59,6 @@ "cards__notifications__title" = "Receba pagamentos"; "cards__invite__title" = "Convide"; "cards__invite__description" = "Compartilhe a Bitkit"; -"cards__lightning__title" = "Pagar"; -"cards__lightning__description" = "Pagamentos instantâneos"; "cards__transferPending__description" = "Pronto em {duration}"; "cards__pin__title" = "Segurança"; "cards__pin__description" = "Configure um PIN"; @@ -256,7 +254,6 @@ "lightning__transfer_hw__sign_title" = "Assine com\no seu dispositivo"; "lightning__transfer_hw__signed_title" = "Transação\nassinada"; "lightning__transfer_to_savings_button" = "Transferir Para Poupança"; -"lightning__transfer_to_spending_button" = "Transferir Para Gastos"; "lightning__spending_intro__title" = "Transferir\npara gastos"; "lightning__spending_intro__text" = "Transfira fundos para seu saldo de gastos para desfrutar de transações instantâneas e baratas com amigos, familiares e comerciantes."; "lightning__spending_intro__button" = "Começar"; @@ -372,6 +369,8 @@ "lightning__channel_node_id" = "ID do Peer"; "lightning__channel_id" = "ID do canal"; "lightning__channel_point" = "Ponto do canal"; +"lightning__channels_closing_banner" = "CANAIS FECHAM A {date}"; +"lightning__channels_closing_notice" = "Os seus canais Lightning serão fechados a {date}. Transfira o seu saldo de gastos para a poupança antes dessa data."; "lightning__closure_reason" = "Motivo do fechamento"; "lightning__support" = "Suporte"; "lightning__connection_closed__title" = "Ligação fechada"; @@ -515,13 +514,6 @@ "other__try_again" = "Tente novamente."; "other__scan__error__generic" = "Bitkit não consegue ler os dados fornecidos."; "other__scan__error__expired" = "Este invoice lightning expirou."; -"other__gift__claimed__text" = "Desculpe, você chegou tarde demais! Todos os presentes para este código já foram resgatados."; -"other__gift__claiming__text" = "Reivindicando seu gitf code Bitkit..."; -"other__gift__claiming__title" = "Reivindicando presente"; -"other__gift__error__title" = "Erro no código de presente"; -"other__gift__error__text" = "A Bitkit não pôde reivindicar os fundos. Tente novamente mais tarde ou entre em contato conosco."; -"other__gift__used__title" = "Código usado"; -"other__gift__used__text" = "Este gift code da Bitkit já foi usado e os fundos já foram pagos."; "other__shop__intro__title" = "Comprar"; "other__shop__intro__description" = "Coloque sua vida no padrão Bitcoin. Gaste seu Bitcoin em cartões-presente digitais, eSIMs, recargas de telefone e muito mais."; "other__shop__intro__button" = "Começar"; @@ -1127,9 +1119,6 @@ "wallet__receive_show_qr" = "Mostrar código QR"; "wallet__receive_background_setup_text" = "Ative a configuração em segundo plano para sair da Bitkit com segurança enquanto seu saldo está sendo configurado."; "wallet__receive_background_setup_toggle" = "Ativar configuração em segundo plano"; -"wallet__receive_spending" = "Receber fundos Lightning"; -"wallet__receive_spending_text" = "Desfrute de pagamentos em bitcoin instantâneos e baratos na rede Lightning."; -"wallet__receive_spending_title" = "Receba no saldo de gastos"; "wallet__receive_connect_initial" = "Para configurar seu saldo de gastos, uma taxa de rede de {networkFee} e {serviceFee} de taxa do provedor de serviços serão deduzidas."; "wallet__receive_connect_additional" = "Para receber mais Bitcoin instantâneo, a Bitkit precisa aumentar sua liquidez. A taxa de rede de {networkFee} e {serviceFee} de taxa do provedor de serviços serão deduzidas do valor que você especificou."; "wallet__receive_liquidity__nav_title" = "Setup do saldo de gastos"; diff --git a/Bitkit/Resources/Localization/ru.lproj/Localizable.strings b/Bitkit/Resources/Localization/ru.lproj/Localizable.strings index c7d64f169..0f5d3e7ec 100644 --- a/Bitkit/Resources/Localization/ru.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/ru.lproj/Localizable.strings @@ -8,8 +8,6 @@ "cards__notifications__title" = "Получайте Платежи"; "cards__invite__title" = "Пригласить"; "cards__invite__description" = "Поделитесь Bitkit"; -"cards__lightning__title" = "Тратить"; -"cards__lightning__description" = "Мгновенные платежи"; "cards__lightningSettingUp__title" = "Перенос"; "cards__lightningSettingUp__description" = "Идёт процесс..."; "cards__lightningConnecting__title" = "Подключение"; @@ -229,7 +227,6 @@ "lightning__transfer_hw__sign_title" = "Подпишите на\nустройстве"; "lightning__transfer_hw__signed_title" = "Транзакция\nподписана"; "lightning__transfer_to_savings_button" = "Перевести в Сбережения"; -"lightning__transfer_to_spending_button" = "Перевести на Расходы"; "lightning__spending_intro__title" = "Перевод\nна Расходы"; "lightning__spending_intro__text" = "Переведите средства на свой баланс расходов, чтобы наслаждаться мгновенными и недорогими транзакциями с друзьями, семьёй и продавцами."; "lightning__spending_intro__button" = "Начать"; @@ -367,6 +364,8 @@ "lightning__channel_node_id" = "ID Узла"; "lightning__channel_id" = "ID Канала"; "lightning__channel_point" = "Точка Канала"; +"lightning__channels_closing_banner" = "КАНАЛЫ ЗАКРОЮТСЯ {date}"; +"lightning__channels_closing_notice" = "Ваши каналы Lightning будут закрыты {date}. До этой даты переведите баланс расходов в сбережения."; "lightning__closure_reason" = "Причина Закрытия"; "lightning__support" = "Поддержка"; "lightning__connection_closed__title" = "Соединение Закрыто"; @@ -568,15 +567,6 @@ "other__transfer_notification__body" = "Откройте Bitkit, чтобы завершить перевод"; "other__scan__error__generic" = "Bitkit не может прочитать предоставленные данные."; "other__scan__error__expired" = "Срок действия этого Lightning инвойса истёк."; -"other__gift__claimed__text" = "Извините, вы опоздали! Все подарки по этому коду уже получены."; -"other__gift__claiming__text" = "Получение подарочного кода Bitkit..."; -"other__gift__claiming__title" = "Получение Подарка"; -"other__gift__error__title" = "Ошибка Подарочного Кода"; -"other__gift__error__text" = "Bitkit не удалось получить средства. Пожалуйста, попробуйте позже или свяжитесь с нами."; -"other__gift__error__button" = "Ок"; -"other__gift__used__title" = "Использованный Код"; -"other__gift__used__text" = "Этот подарочный код Bitkit уже был использован, и средства были выплачены."; -"other__gift__used__button" = "Ок"; "other__shop__intro__title" = "Магазин"; "other__shop__intro__description" = "Переходите на биткоин-стандарт. Тратьте свои Bitcoin на цифровые подарочные карты, eSIM, пополнение телефона и многое другое."; "other__shop__intro__button" = "Начать"; @@ -1390,9 +1380,6 @@ "wallet__receive_text_lnfunds" = "Хотите получить Лайтнинг средства?"; "wallet__receive_background_setup_text" = "Включите фоновую настройку, чтобы безопасно выйти из Bitkit пока настраивается ваш баланс."; "wallet__receive_background_setup_toggle" = "Включить фоновую настройку"; -"wallet__receive_spending" = "Получить Lightning средства"; -"wallet__receive_spending_text" = "Наслаждайтесь мгновенными и дешёвыми биткоин-платежами в сети Lightning."; -"wallet__receive_spending_title" = "Получить на баланс расходов"; "wallet__receive_connect_initial" = "Для настройки вашего баланса расходов будет удержана сетевая комиссия {networkFee} и комиссия провайдера услуг {serviceFee}."; "wallet__receive_connect_additional" = "Чтобы получить больше мгновенных Bitcoin, Bitkit должен увеличить вашу ликвидность. Из указанной суммы будет удержана сетевая комиссия {networkFee} и комиссия провайдера услуг {serviceFee}."; "wallet__receive_liquidity__nav_title" = "Настройка Баланса Расходов"; diff --git a/Bitkit/Utilities/LocalizeHelpers.swift b/Bitkit/Utilities/LocalizeHelpers.swift index 6f86493c0..7ce92d884 100644 --- a/Bitkit/Utilities/LocalizeHelpers.swift +++ b/Bitkit/Utilities/LocalizeHelpers.swift @@ -19,7 +19,7 @@ enum LocalizationHelper { $languageOverride.withValue("en", operation: body) } - private static var currentLanguageCode: String { + static var currentLanguageCode: String { if let languageOverride { return languageOverride } diff --git a/Bitkit/ViewModels/AppViewModel.swift b/Bitkit/ViewModels/AppViewModel.swift index b273656da..10049c9a3 100644 --- a/Bitkit/ViewModels/AppViewModel.swift +++ b/Bitkit/ViewModels/AppViewModel.swift @@ -343,11 +343,6 @@ class AppViewModel: ObservableObject { if isBolt11Invoice(url) { return false } - if url.scheme?.lowercased() == "bitkit", - url.host?.lowercased().hasPrefix("gift-") == true - { - return false - } return !PubkyAuthRequest.isProtocolURL(url.absoluteString.removingLightningSchemes()) } @@ -1099,8 +1094,6 @@ extension AppViewModel { return } await handlePubkyAuthApproval(sourceURI) - case let .gift(code, amount): - sheetViewModel.showSheet(.gift, data: GiftConfig(code: code, amount: Int(amount))) default: Logger.warn("Unhandled invoice type: \(data)") toast(type: .error, title: "Unsupported", description: "This type of invoice is not supported yet") @@ -1513,7 +1506,7 @@ extension AppViewModel { } } - case .lnurlPay, .lnurlWithdraw, .lnurlChannel, .lnurlAuth, .nodeId, .gift: + case .lnurlPay, .lnurlWithdraw, .lnurlChannel, .lnurlAuth, .nodeId: // These types are valid if decoded successfully result = .valid diff --git a/Bitkit/ViewModels/SheetViewModel.swift b/Bitkit/ViewModels/SheetViewModel.swift index 6cc694616..97e963013 100644 --- a/Bitkit/ViewModels/SheetViewModel.swift +++ b/Bitkit/ViewModels/SheetViewModel.swift @@ -8,7 +8,6 @@ enum SheetID: String, CaseIterable { case connectionClosed case forceTransfer case forgotPin - case gift case highBalance case lnurlAuth case lnurlWithdraw @@ -281,20 +280,6 @@ class SheetViewModel: ObservableObject { } } - var giftSheetItem: GiftSheetItem? { - get { - guard let config = activeSheetConfiguration, config.id == .gift else { return nil } - let giftConfig = config.data as? GiftConfig - guard let code = giftConfig?.code, let amount = giftConfig?.amount else { return nil } - return GiftSheetItem(code: code, amount: amount) - } - set { - if newValue == nil { - activeSheetConfiguration = nil - } - } - } - var highBalanceSheetItem: HighBalanceSheetItem? { get { guard let config = activeSheetConfiguration, config.id == .highBalance else { return nil } diff --git a/Bitkit/ViewModels/WalletViewModel.swift b/Bitkit/ViewModels/WalletViewModel.swift index 5e9b859c8..5fa2b32cb 100644 --- a/Bitkit/ViewModels/WalletViewModel.swift +++ b/Bitkit/ViewModels/WalletViewModel.swift @@ -111,6 +111,12 @@ class WalletViewModel: ObservableObject { @Published var currentBlockHeight: UInt32 = 0 @Published var isRetryingLightningPayment = false + /// Whether any channel, open or pending, is with a Blocktank node, which closes them on `ChannelsClosing.CHANNELS_CLOSE_DATE`. + var hasTrustedChannels: Bool { + guard let channels else { return false } + return !lightningService.separateTrustedChannels(channels).trusted.isEmpty + } + init( lightningService: LightningService = .shared, coreService: CoreService = .shared, diff --git a/Bitkit/Views/Gift/GiftError.swift b/Bitkit/Views/Gift/GiftError.swift deleted file mode 100644 index 111dbedfa..000000000 --- a/Bitkit/Views/Gift/GiftError.swift +++ /dev/null @@ -1,38 +0,0 @@ -import SwiftUI - -struct GiftFailed: View { - @Binding var navigationPath: [GiftRoute] - @EnvironmentObject private var sheets: SheetViewModel - - var body: some View { - VStack(alignment: .leading, spacing: 0) { - SheetHeader(title: t("other__gift__error__title")) - - VStack(spacing: 0) { - BodyMText(t("other__gift__error__text")) - .frame(maxWidth: .infinity, alignment: .leading) - - Spacer() - - Image("exclamation-mark") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: UIScreen.main.bounds.width * 0.8) - .frame(maxHeight: 256) - - Spacer() - } - - CustomButton( - title: t("common__ok") - ) { - sheets.hideSheet() - } - } - .navigationBarHidden(true) - .allowSwipeBack(false) - .padding(.horizontal, 16) - .sheetBackground() - .accessibilityIdentifier("GiftError") - } -} diff --git a/Bitkit/Views/Gift/GiftLoading.swift b/Bitkit/Views/Gift/GiftLoading.swift deleted file mode 100644 index bc7c04a83..000000000 --- a/Bitkit/Views/Gift/GiftLoading.swift +++ /dev/null @@ -1,149 +0,0 @@ -import BitkitCore -import SwiftUI - -struct GiftLoading: View { - @Binding var navigationPath: [GiftRoute] - @EnvironmentObject private var sheets: SheetViewModel - @EnvironmentObject private var wallet: WalletViewModel - - let code: String - let amount: Int - - var body: some View { - VStack(alignment: .leading, spacing: 0) { - SheetHeader(title: t("other__gift__claiming__title")) - - VStack(spacing: 0) { - MoneyStack(sats: amount, showSymbol: true) - - BodyMText(t("other__gift__claiming__text")) - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.top, 32) - - Spacer() - - Image("gift-figure") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: UIScreen.main.bounds.width * 0.8) - .frame(maxHeight: 256) - - Spacer() - - ActivityIndicator() - .padding(.bottom, 32) - } - } - .padding(.horizontal, 16) - .accessibilityIdentifier("GiftLoading") - .task { - await claimGift() - } - } - - private func claimGift() async { - // Wait for peers to be connected (equivalent to waitForLdkPeers) - await waitForPeers() - - let maxInboundCapacity = wallet.totalInboundLightningSats ?? 0 - - Logger.debug("Max inbound capacity: \(maxInboundCapacity)") - - if maxInboundCapacity >= UInt64(amount) { - // User has sufficient inbound capacity, use existing channels - await claimWithLiquidity() - } else { - // User needs new channel, create order - await claimWithoutLiquidity() - } - } - - private func waitForPeers() async { - // Wait a bit for peers to connect if node is starting - guard wallet.nodeLifecycleState == .running else { - // Wait for node to be running - await wallet.waitForNodeToRun(timeoutSeconds: 30.0) - return - } - - // Give some time for peer connections - try? await Task.sleep(nanoseconds: 2_000_000_000) // 2 seconds - } - - private func claimWithLiquidity() async { - do { - // Create a zero amount lightning invoice - let invoice = try await wallet.createInvoice(note: "blocktank-gift-code:\(code)") - _ = try await giftPay(invoice: invoice) - } catch { - handleGiftClaimError(error) - } - } - - private func claimWithoutLiquidity() async { - do { - guard let nodeId = LightningService.shared.nodeId else { - throw AppError(serviceError: .nodeNotStarted) - } - - // Create an order for the gift using BitkitCore - let order = try await giftOrder(clientNodeId: nodeId, code: "blocktank-gift-code:\(code)") - - guard let orderId = order.orderId else { - throw AppError(message: "Order ID is nil", debugMessage: "Order ID is nil") - } - - // Open the channel - let openedOrder = try await CoreService.shared.blocktank.open(orderId: orderId) - - let nowTimestamp = UInt64(Date().timeIntervalSince1970) - - // Create activity item for the received gift - let lightningActivity = LightningActivity( - walletId: WalletScope.default, - id: openedOrder.channel?.fundingTx.id ?? orderId, - txType: .received, - status: .succeeded, - value: UInt64(amount), - fee: 0, - // invoice can't be empty, so use the invoice from the order - invoice: openedOrder.payment?.bolt11Invoice?.request ?? "", - message: code, - timestamp: nowTimestamp, - preimage: nil, - contact: nil, - createdAt: nowTimestamp, - updatedAt: nil, - seenAt: nil - ) - - // Add to activity list - try await CoreService.shared.activity.insert(.lightning(lightningActivity)) - - // Mark the activity as seen before showing the sheet - await CoreService.shared.activity.markActivityAsSeen(id: lightningActivity.id) - - // Trigger haptic feedback - Haptics.notify(.success) - - // Close gift sheet and show received transaction sheet - sheets.showSheet(.receivedTx, data: ReceivedTxSheetDetails(type: .lightning, sats: UInt64(amount))) - } catch { - handleGiftClaimError(error) - } - } - - private func handleGiftClaimError(_ error: Error) { - Logger.error("Gift claim failed: \(error)") - - // Check the full error description for gift code error types - let errorDescription = String(describing: error) - if errorDescription.contains("GIFT_CODE_ALREADY_USED") { - navigationPath.append(.used) - } else if errorDescription.contains("GIFT_CODE_USED_UP") { - navigationPath.append(.usedUp) - } else { - navigationPath.append(.failed) - } - } -} diff --git a/Bitkit/Views/Gift/GiftUsed.swift b/Bitkit/Views/Gift/GiftUsed.swift deleted file mode 100644 index e287c051e..000000000 --- a/Bitkit/Views/Gift/GiftUsed.swift +++ /dev/null @@ -1,38 +0,0 @@ -import SwiftUI - -struct GiftUsed: View { - @Binding var navigationPath: [GiftRoute] - @EnvironmentObject private var sheets: SheetViewModel - - var body: some View { - VStack(alignment: .leading, spacing: 0) { - SheetHeader(title: t("other__gift__used__title")) - - VStack(spacing: 0) { - BodyMText(t("other__gift__used__text")) - .frame(maxWidth: .infinity, alignment: .leading) - - Spacer() - - Image("exclamation-mark") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: UIScreen.main.bounds.width * 0.8) - .frame(maxHeight: 256) - - Spacer() - } - - CustomButton( - title: t("common__ok") - ) { - sheets.hideSheet() - } - } - .navigationBarHidden(true) - .allowSwipeBack(false) - .padding(.horizontal, 16) - .sheetBackground() - .accessibilityIdentifier("GiftUsed") - } -} diff --git a/Bitkit/Views/Gift/GiftUsedUp.swift b/Bitkit/Views/Gift/GiftUsedUp.swift deleted file mode 100644 index a0b521df2..000000000 --- a/Bitkit/Views/Gift/GiftUsedUp.swift +++ /dev/null @@ -1,38 +0,0 @@ -import SwiftUI - -struct GiftUsedUp: View { - @Binding var navigationPath: [GiftRoute] - @EnvironmentObject private var sheets: SheetViewModel - - var body: some View { - VStack(alignment: .leading, spacing: 0) { - SheetHeader(title: t("Out of Gifts")) - - VStack(spacing: 0) { - BodyMText(t("other__gift__claimed__text")) - .frame(maxWidth: .infinity, alignment: .leading) - - Spacer() - - Image("exclamation-mark") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: UIScreen.main.bounds.width * 0.8) - .frame(maxHeight: 256) - - Spacer() - } - - CustomButton( - title: t("common__ok") - ) { - sheets.hideSheet() - } - } - .navigationBarHidden(true) - .allowSwipeBack(false) - .padding(.horizontal, 16) - .sheetBackground() - .accessibilityIdentifier("GiftUsedUp") - } -} diff --git a/Bitkit/Views/Home/HomeWalletView.swift b/Bitkit/Views/Home/HomeWalletView.swift index 80aa46790..3cac295a4 100644 --- a/Bitkit/Views/Home/HomeWalletView.swift +++ b/Bitkit/Views/Home/HomeWalletView.swift @@ -93,6 +93,16 @@ struct HomeWalletView: View { .padding(.bottom, 32) } + if wallet.hasTrustedChannels { + Button { navigation.navigate(.spendingWallet) } label: { + ActivityBanner(type: .channelsClosing) + } + .buttonStyle(.plain) + .accessibilityIdentifier("ChannelsClosingBanner") + .padding(.bottom, 16) + .transition(.opacity) + } + if hasActivity { ActivityLatest(showsUsdtWallet: showsUsdt) @@ -112,5 +122,6 @@ struct HomeWalletView: View { .padding(.bottom, ScreenLayout.bottomPaddingWithSafeArea) .padding(.horizontal) .animation(.spring(response: 0.3), value: hasActivity) + .animation(.spring(response: 0.4, dampingFraction: 0.8), value: wallet.hasTrustedChannels) } } diff --git a/Bitkit/Views/Settings/Advanced/LightningConnectionsView.swift b/Bitkit/Views/Settings/Advanced/LightningConnectionsView.swift index 1f80a4b63..31205045b 100644 --- a/Bitkit/Views/Settings/Advanced/LightningConnectionsView.swift +++ b/Bitkit/Views/Settings/Advanced/LightningConnectionsView.swift @@ -19,7 +19,7 @@ struct LightningConnectionsView: View { NavigationBar( title: t("lightning__connections"), action: AnyView( - NavigationLink(value: Route.fundingOptions) { + NavigationLink(value: Route.fundingAdvanced) { Image("plus") .resizable() .frame(width: 24, height: 24) @@ -207,7 +207,7 @@ struct LightningConnectionsView: View { } CustomButton(title: buttonText) { - navigation.navigate(.fundingOptions) + navigation.navigate(.fundingAdvanced) } } } diff --git a/Bitkit/Views/Sheets/GiftSheet.swift b/Bitkit/Views/Sheets/GiftSheet.swift deleted file mode 100644 index 5e79083d8..000000000 --- a/Bitkit/Views/Sheets/GiftSheet.swift +++ /dev/null @@ -1,46 +0,0 @@ -import SwiftUI - -enum GiftRoute: Hashable { - case loading(code: String, amount: Int) - case used - case usedUp - case failed -} - -struct GiftConfig { - let code: String - let amount: Int -} - -struct GiftSheetItem: SheetItem { - let id: SheetID = .gift - let size: SheetSize = .large - let code: String - let amount: Int -} - -struct GiftSheet: View { - @EnvironmentObject private var sheets: SheetViewModel - @State private var navigationPath: [GiftRoute] = [] - let config: GiftSheetItem - - var body: some View { - Sheet(id: .gift, data: config) { - NavigationStack(path: $navigationPath) { - GiftLoading(navigationPath: $navigationPath, code: config.code, amount: config.amount) - .navigationDestination(for: GiftRoute.self) { route in - switch route { - case .loading: - GiftLoading(navigationPath: $navigationPath, code: config.code, amount: config.amount) - case .used: - GiftUsed(navigationPath: $navigationPath) - case .usedUp: - GiftUsedUp(navigationPath: $navigationPath) - case .failed: - GiftFailed(navigationPath: $navigationPath) - } - } - } - } - } -} diff --git a/Bitkit/Views/Wallets/Activity/ActivityLatest.swift b/Bitkit/Views/Wallets/Activity/ActivityLatest.swift index 9f6b91e16..464ebf1a0 100644 --- a/Bitkit/Views/Wallets/Activity/ActivityLatest.swift +++ b/Bitkit/Views/Wallets/Activity/ActivityLatest.swift @@ -37,7 +37,7 @@ struct ActivityLatest: View { } /// Three or four vertical slots (by screen size) shared by: hardware wallet rows (two wallets per row), - /// transfer banner, widgets onboarding and activity items; only the item count shrinks so the total stays within the cap. + /// transfer and channels-closing banners, widgets onboarding and activity items; only the item count shrinks so the total stays within the cap. private var maxActivityItemsOnHome: Int { let slotCapacity = UIScreen.main.isSmall ? ActivityDisplayConstants.maxHomeActivityItems - 1 : ActivityDisplayConstants.maxHomeActivityItems var nonItemSlots = 0 @@ -45,6 +45,9 @@ struct ActivityLatest: View { if shouldShowBanner { nonItemSlots += 1 } + if wallet.hasTrustedChannels { + nonItemSlots += 1 + } if settings.showWidgets, !app.hasDismissedWidgetsOnboardingHint { nonItemSlots += 1 } diff --git a/Bitkit/Views/Wallets/HardwareWalletScreen.swift b/Bitkit/Views/Wallets/HardwareWalletScreen.swift index 2af1c9f82..ca7620123 100644 --- a/Bitkit/Views/Wallets/HardwareWalletScreen.swift +++ b/Bitkit/Views/Wallets/HardwareWalletScreen.swift @@ -3,8 +3,7 @@ import SwiftUI /// Detail overview of a paired hardware wallet, tracked as a watch-only balance. Mirrors the /// Savings/Spending screens: device name + blue Bitcoin icon in the top bar, balance header, the -/// device's on-chain activity grouped by date (blue hardware icons), a Transfer-To-Spending action -/// on funded devices, and a Remove action. Ports bitkit-android's `HardwareWalletScreen`. +/// device's on-chain activity grouped by date (blue hardware icons), and a Remove action. Ports bitkit-android's `HardwareWalletScreen`. struct HardwareWalletScreen: View { let walletId: String @@ -41,9 +40,6 @@ struct HardwareWalletScreen: View { .task(id: wallet?.walletId) { await loadActivities() } - .task { - await app.checkGeoStatus() - } .onReceive(activity.activitiesChangedPublisher) { _ in Task { await loadActivities() } } @@ -63,8 +59,6 @@ struct HardwareWalletScreen: View { private func content(for wallet: HwWallet) -> some View { let hasActivity = !activities.isEmpty - // Only the native-segwit account can currently fund a transfer to spending. - let canTransferToSpending = wallet.fundingBalanceSats > 0 return VStack(spacing: 0) { ScrollView(showsIndicators: false) { @@ -76,11 +70,6 @@ struct HardwareWalletScreen: View { testIdPrefix: "TotalBalance" ) - if canTransferToSpending { - transferButton - .padding(.top, 28) - } - if hasActivity { HardwareWalletActivityList(activities: activities) .padding(.top, 32) @@ -119,26 +108,6 @@ struct HardwareWalletScreen: View { .offset(x: placement.trailingOverhang, y: ScreenLayout.topPaddingWithoutSafeArea) } - private var transferButton: some View { - CustomButton( - title: t("lightning__transfer_to_spending_button"), - variant: .secondary, - icon: Image("arrow-up-down") - .resizable() - .scaledToFit() - .frame(width: 16, height: 16) - .foregroundColor(.white80), - isDisabled: GeoService.shared.isGeoBlocked - ) { - if app.hasSeenTransferToSpendingIntro { - navigation.navigate(.spendingAmountHw(walletId: walletId)) - } else { - navigation.navigate(.spendingIntroHw(walletId: walletId)) - } - } - .accessibilityIdentifier("HardwareTransferToSpending") - } - private func removeButton(for wallet: HwWallet) -> some View { CustomButton( title: t("hardware__remove_button", variables: ["name": wallet.name]), diff --git a/Bitkit/Views/Wallets/Receive/ReceiveEdit.swift b/Bitkit/Views/Wallets/Receive/ReceiveEdit.swift index de38d29e8..f9302e148 100644 --- a/Bitkit/Views/Wallets/Receive/ReceiveEdit.swift +++ b/Bitkit/Views/Wallets/Receive/ReceiveEdit.swift @@ -3,7 +3,6 @@ import SwiftUI struct ReceiveEdit: View { @EnvironmentObject private var app: AppViewModel - @EnvironmentObject private var blocktank: BlocktankViewModel @EnvironmentObject private var currency: CurrencyViewModel @EnvironmentObject private var wallet: WalletViewModel @EnvironmentObject private var tagManager: TagManager @@ -28,19 +27,6 @@ struct ReceiveEdit: View { amountViewModel.amountSats } - private var liquiditySource: ReceiveLiquiditySource { - switch sourceTab { - case .savings: - return .savings - case .unified: - return .auto - case .spending: - return .spending - case .hardware, .usdt: - return .savings - } - } - var body: some View { VStack(spacing: 0) { SheetHeader(title: t("wallet__receive_specify"), showBackButton: true) @@ -228,36 +214,9 @@ struct ReceiveEdit: View { // Wait until node is running if it's in starting state if await wallet.waitForNodeToRun() { do { - var maxCjitAmountSats: UInt64? - if needsCjitLimitsForAdditionalLiquidity() { - try? await blocktank.refreshMinCjitSats() - maxCjitAmountSats = try? await blocktank.maxCjitAmountSats() - } - - switch additionalLiquidityAction(maxCjitAmountSats: maxCjitAmountSats) { - case .none: - try await wallet.refreshBip21(forceRefreshBolt11: true) - finishWithQr() - case .chooseAmount: - try await wallet.refreshBip21(forceRefreshBolt11: true) - finishWithRoute(.cjitAmount) - case let .createCjit(amountSats): - let entry = try await blocktank.createCjit(amountSats: amountSats, description: note) - finishWithRoute(.cjitConfirm(entry: entry, receiveAmountSats: amountSats, isAdditional: true)) - case .geoBlocked: - finishWithRoute(.cjitGeoBlocked) - } + try await wallet.refreshBip21(forceRefreshBolt11: true) + finishWithQr() } catch { - if error.isCjitNodeCapacityExceeded { - showNodeCapacityExceededToast() - return - } - - if error.isChannelSizeExceedsMaximum { - finishWithRoute(.cjitAmount) - return - } - app.toast(error) } } else { @@ -278,22 +237,6 @@ struct ReceiveEdit: View { } } - private func finishWithRoute(_ route: ReceiveRoute) { - if replacesCurrentQr { - Self.replaceEditedQrRoute(in: &navigationPath, with: route) - } else { - navigationPath.append(route) - } - } - - private func showNodeCapacityExceededToast() { - app.toast( - type: .warning, - title: t("wallet__receive_cjit_error_node_capacity__title"), - description: t("wallet__receive_cjit_error_node_capacity__description") - ) - } - private func deleteTag(_ tag: String) async { do { try await wallet.mutateReceiveTags(.remove([tag])) @@ -306,26 +249,6 @@ struct ReceiveEdit: View { } } - private func additionalLiquidityAction(maxCjitAmountSats: UInt64?) -> ReceiveAdditionalLiquidityAction { - ReceiveLiquidityDecision.additionalLiquidityAction( - source: liquiditySource, - invoiceAmountSats: amountViewModel.amountSats, - inboundCapacitySats: wallet.totalReadyInboundLightningSats, - minCjitSats: blocktank.minCjitSats, - maxCjitAmountSats: maxCjitAmountSats, - isGeoBlocked: GeoService.shared.isGeoBlocked - ) - } - - private func needsCjitLimitsForAdditionalLiquidity() -> Bool { - ReceiveLiquidityDecision.needsCjitLimitsForAdditionalLiquidity( - source: liquiditySource, - invoiceAmountSats: amountViewModel.amountSats, - inboundCapacitySats: wallet.totalReadyInboundLightningSats, - isGeoBlocked: GeoService.shared.isGeoBlocked - ) - } - @ViewBuilder private var numberPadButtons: some View { HStack(alignment: .bottom) { diff --git a/Bitkit/Views/Wallets/Receive/ReceiveQr.swift b/Bitkit/Views/Wallets/Receive/ReceiveQr.swift index 85cd92dd6..16bc0b2cf 100644 --- a/Bitkit/Views/Wallets/Receive/ReceiveQr.swift +++ b/Bitkit/Views/Wallets/Receive/ReceiveQr.swift @@ -77,13 +77,14 @@ struct ReceiveQr: View { } private var availableTabItems: [TabItem] { - var items: [TabItem] = [ - TabItem(.savings), - TabItem(.spending), - ] + var items: [TabItem] = [TabItem(.savings)] if canShowUnifiedReceive { - items.insert(TabItem(.unified), at: 1) + items.append(TabItem(.unified)) + } + + if canShowSpendingReceive { + items.append(TabItem(.spending)) } if selectedHardwareWalletId != nil { @@ -113,6 +114,10 @@ struct ReceiveQr: View { return hwWalletManager.watcherReceiveAddress(walletId: walletId) ?? hardwareAddress } + private var canShowSpendingReceive: Bool { + wallet.canCreateReceiveLightningInvoice || cjitInvoice != nil + } + private var canShowUnifiedReceive: Bool { (wallet.canCreateReceiveLightningInvoice || wallet.hasCoherentReceiveInvoice) && cjitInvoice == nil } @@ -159,10 +164,6 @@ struct ReceiveQr: View { ) } - var showingCjitOnboarding: Bool { - return !wallet.canCreateReceiveLightningInvoice && cjitInvoice == nil && selectedTab == .spending - } - var body: some View { if selectedTab == .usdt, !settings.isUsdtEnabled { UsdtIntroContent(isEnablePrompt: true, onDecline: { selectedTabBinding.wrappedValue = .savings }, @@ -200,7 +201,10 @@ struct ReceiveQr: View { tabContent(for: .unified) } - tabContent(for: .spending) + if canShowSpendingReceive { + tabContent(for: .spending) + } + if settings.isUsdtEnabled, cjitInvoice == nil { Color.clear.tag(ReceivePage.tab(.usdt)) } @@ -215,22 +219,7 @@ struct ReceiveQr: View { Spacer() Group { - if showingCjitOnboarding { - CustomButton( - title: t("wallet__receive_spending"), - icon: Image("bolt") - .resizable() - .frame(width: 16, height: 16) - .foregroundColor(.purpleAccent), - isDisabled: wallet.nodeLifecycleState != .running - ) { - if GeoService.shared.isGeoBlocked { - navigationPath.append(.cjitGeoBlocked) - } else if !wallet.canCreateReceiveLightningInvoice { - navigationPath.append(.cjitAmount) - } - } - } else if showDetails { + if showDetails { VStack(spacing: 16) { if selectedTab == .hardware { CustomButton( @@ -280,6 +269,7 @@ struct ReceiveQr: View { } } .onAppear { + selectSavingsIfSpendingUnavailable() applyDefaultTabIfNeeded() } } @@ -331,6 +321,7 @@ struct ReceiveQr: View { selectedTab = .savings } + selectSavingsIfSpendingUnavailable() applyDefaultTabIfNeeded() } .onDisappear { @@ -342,6 +333,12 @@ struct ReceiveQr: View { } } + private func selectSavingsIfSpendingUnavailable() { + if selectedTab == .spending, !canShowSpendingReceive { + selectedTab = .savings + } + } + private func applyDefaultTabIfNeeded() { guard tab == nil, !hasAppliedDefaultTab, !hasUserSelectedTab, canShowUnifiedReceive else { return @@ -398,8 +395,6 @@ struct ReceiveQr: View { } else { ProgressView() } - } else if tab == .spending && !wallet.canCreateReceiveLightningInvoice && cjitInvoice == nil { - cjitOnboarding } else if receiveUnavailable(for: tab) { receiveRefreshStatus(for: tab) } else if showDetails { @@ -515,39 +510,6 @@ struct ReceiveQr: View { } } - var cjitOnboarding: some View { - VStack(alignment: .leading, spacing: 0) { - DisplayText(t("wallet__receive_spending_title"), accentColor: .purpleAccent) - .padding(.bottom, 12) - - BodyMText(t("wallet__receive_spending_text")) - - Spacer() - - HStack { - Spacer() - Image("bolt") - .resizable() - .frame(width: 64, height: 64) - .foregroundColor(.purpleAccent) - Spacer() - } - } - .padding(32) - .background(Color.black) - .cornerRadius(8) - .aspectRatio(1, contentMode: .fit) - .overlay(alignment: .bottomLeading) { - if !UIScreen.main.isSmall { - Image("arrow-cjit") - .resizable() - .scaledToFit() - .frame(height: 210) - .offset(x: 70, y: 110) - } - } - } - func detailsContent(for tab: ReceiveTab) -> some View { VStack { let addressPairs: [CopyAddressPair] = { diff --git a/Bitkit/Views/Wallets/SavingsWalletScreen.swift b/Bitkit/Views/Wallets/SavingsWalletScreen.swift index e3cdce22b..5365caf35 100644 --- a/Bitkit/Views/Wallets/SavingsWalletScreen.swift +++ b/Bitkit/Views/Wallets/SavingsWalletScreen.swift @@ -49,12 +49,6 @@ struct SavingsWalletScreen: View { } if !shouldShowOnboarding { - if !GeoService.shared.isGeoBlocked { - transferButton - .transition(.move(edge: .leading).combined(with: .opacity)) - .padding(.top, 28) - } - ActivityList(viewType: .onchain) CustomButton(title: t("common__show_all"), variant: .tertiary) { @@ -112,25 +106,6 @@ struct SavingsWalletScreen: View { .navigationBarHidden(true) .animation(.spring(response: 0.3), value: wallet.totalOnchainSats) } - - var transferButton: some View { - CustomButton( - title: t("lightning__transfer_to_spending_button"), - variant: .secondary, - icon: Image("arrow-up-down") - .resizable() - .scaledToFit() - .frame(width: 16, height: 16) - .foregroundColor(.white80) - ) { - if app.hasSeenTransferToSpendingIntro { - navigation.navigate(.spendingAmount) - } else { - navigation.navigate(.spendingIntro) - } - } - .accessibilityIdentifier("TransferToSpending") - } } #Preview { diff --git a/Bitkit/Views/Wallets/SpendingWalletScreen.swift b/Bitkit/Views/Wallets/SpendingWalletScreen.swift index e907bee69..0a62c5053 100644 --- a/Bitkit/Views/Wallets/SpendingWalletScreen.swift +++ b/Bitkit/Views/Wallets/SpendingWalletScreen.swift @@ -33,10 +33,14 @@ struct SpendingWalletScreen: View { .padding(.top, 16) } - if shouldShowOnboarding && wallet.totalOnchainSats > 0 && !GeoService.shared.isGeoBlocked { - transferFromSavingsButton - .transition(.move(edge: .leading).combined(with: .opacity)) - .padding(.top, 28) + if wallet.hasTrustedChannels { + BodyMText( + t("lightning__channels_closing_notice", variables: ["date": ChannelsClosing.formattedCloseDate()]), + textColor: .white64 + ) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.top, 16) + .accessibilityIdentifier("ChannelsClosingNotice") } if !shouldShowOnboarding { @@ -104,25 +108,6 @@ struct SpendingWalletScreen: View { .animation(.spring(response: 0.3), value: wallet.totalLightningSats) } - var transferFromSavingsButton: some View { - CustomButton( - title: t("lightning__transfer_to_spending_button"), - variant: .secondary, - icon: Image("arrow-up-down") - .resizable() - .scaledToFit() - .frame(width: 16, height: 16) - .foregroundColor(.white80) - ) { - if app.hasSeenTransferToSpendingIntro { - navigation.navigate(.spendingAmount) - } else { - navigation.navigate(.spendingIntro) - } - } - .accessibilityIdentifier("TransferFromSavings") - } - var transferButton: some View { CustomButton( title: t("lightning__transfer_to_savings_button"), diff --git a/BitkitTests/ChannelsClosingTests.swift b/BitkitTests/ChannelsClosingTests.swift new file mode 100644 index 000000000..02e4f7bc7 --- /dev/null +++ b/BitkitTests/ChannelsClosingTests.swift @@ -0,0 +1,9 @@ +@testable import Bitkit +import XCTest + +final class ChannelsClosingTests: XCTestCase { + func testCloseDateUsesMediumDateStyle() { + XCTAssertEqual(ChannelsClosing.formattedCloseDate(locale: Locale(identifier: "en")), "Dec 15, 2026") + XCTAssertEqual(ChannelsClosing.formattedCloseDate(locale: Locale(identifier: "de")), "15.12.2026") + } +} diff --git a/BitkitTests/ReceiveEditTests.swift b/BitkitTests/ReceiveEditTests.swift index 92664516d..aeb88daec 100644 --- a/BitkitTests/ReceiveEditTests.swift +++ b/BitkitTests/ReceiveEditTests.swift @@ -36,15 +36,4 @@ final class ReceiveEditTests: XCTestCase { ] ) } - - func testReplacingEditedQrRouteCanNavigateToFreshCjitFlow() { - var navigationPath: [ReceiveRoute] = [ - .qr(cjitInvoice: "stale-cjit-invoice", tab: .spending), - .edit(tab: .spending, onchainOnly: false, replacesCurrentQr: true), - ] - - ReceiveEdit.replaceEditedQrRoute(in: &navigationPath, with: .cjitAmount) - - XCTAssertEqual(navigationPath, [.cjitAmount]) - } } diff --git a/BitkitTests/ReceiveLiquidityDecisionTests.swift b/BitkitTests/ReceiveLiquidityDecisionTests.swift index 06ddf67fa..2d35eb111 100644 --- a/BitkitTests/ReceiveLiquidityDecisionTests.swift +++ b/BitkitTests/ReceiveLiquidityDecisionTests.swift @@ -47,156 +47,4 @@ final class ReceiveLiquidityDecisionTests: XCTestCase { ) ) } - - func testZeroInboundDoesNotRouteToCjit() { - XCTAssertEqual( - ReceiveLiquidityDecision.additionalLiquidityAction( - source: .spending, - invoiceAmountSats: 10000, - inboundCapacitySats: 0, - minCjitSats: 5000, - maxCjitAmountSats: 100_000, - isGeoBlocked: false - ), - .none - ) - } - - func testSavingsAndAutoEditsDoNotRouteToCjit() { - for source in [ReceiveLiquiditySource.savings, .auto] { - XCTAssertEqual( - ReceiveLiquidityDecision.additionalLiquidityAction( - source: source, - invoiceAmountSats: 10000, - inboundCapacitySats: 1000, - minCjitSats: 5000, - maxCjitAmountSats: 100_000, - isGeoBlocked: false - ), - .none - ) - } - } - - func testBelowCjitMinimumRoutesToAmountPicker() { - XCTAssertEqual( - ReceiveLiquidityDecision.additionalLiquidityAction( - source: .spending, - invoiceAmountSats: 4000, - inboundCapacitySats: 1000, - minCjitSats: 5000, - maxCjitAmountSats: 100_000, - isGeoBlocked: false - ), - .chooseAmount - ) - } - - func testAtCjitMinimumCreatesCjit() { - XCTAssertEqual( - ReceiveLiquidityDecision.additionalLiquidityAction( - source: .spending, - invoiceAmountSats: 5000, - inboundCapacitySats: 1000, - minCjitSats: 5000, - maxCjitAmountSats: 100_000, - isGeoBlocked: false - ), - .createCjit(5000) - ) - } - - func testOverMaxCjitAmountRoutesToAmountPicker() { - XCTAssertEqual( - ReceiveLiquidityDecision.additionalLiquidityAction( - source: .spending, - invoiceAmountSats: 100_001, - inboundCapacitySats: 1000, - minCjitSats: 5000, - maxCjitAmountSats: 100_000, - isGeoBlocked: false - ), - .chooseAmount - ) - } - - func testUnknownMaxCjitAmountRoutesToAmountPicker() { - XCTAssertEqual( - ReceiveLiquidityDecision.additionalLiquidityAction( - source: .spending, - invoiceAmountSats: 10000, - inboundCapacitySats: 1000, - minCjitSats: 5000, - maxCjitAmountSats: nil, - isGeoBlocked: false - ), - .chooseAmount - ) - } - - func testGeoBlockedRoutesToGeoBlock() { - XCTAssertEqual( - ReceiveLiquidityDecision.additionalLiquidityAction( - source: .spending, - invoiceAmountSats: 10000, - inboundCapacitySats: 1000, - minCjitSats: 5000, - maxCjitAmountSats: 100_000, - isGeoBlocked: true - ), - .geoBlocked - ) - } - - func testNoAdditionalLiquidityNeededReturnsNone() { - XCTAssertEqual( - ReceiveLiquidityDecision.additionalLiquidityAction( - source: .spending, - invoiceAmountSats: 1000, - inboundCapacitySats: 1000, - minCjitSats: 5000, - maxCjitAmountSats: 100_000, - isGeoBlocked: false - ), - .none - ) - } - - func testCjitLimitsAreFetchedOnlyWhenAdditionalLiquidityCanUseThem() { - XCTAssertFalse( - ReceiveLiquidityDecision.needsCjitLimitsForAdditionalLiquidity( - source: .auto, - invoiceAmountSats: 10000, - inboundCapacitySats: 1000, - isGeoBlocked: false - ) - ) - - XCTAssertFalse( - ReceiveLiquidityDecision.needsCjitLimitsForAdditionalLiquidity( - source: .spending, - invoiceAmountSats: 10000, - inboundCapacitySats: 0, - isGeoBlocked: false - ) - ) - - XCTAssertFalse( - ReceiveLiquidityDecision.needsCjitLimitsForAdditionalLiquidity( - source: .spending, - invoiceAmountSats: 10000, - inboundCapacitySats: 1000, - isGeoBlocked: true - ) - ) - - XCTAssertTrue( - ReceiveLiquidityDecision.needsCjitLimitsForAdditionalLiquidity( - source: .spending, - invoiceAmountSats: 10000, - inboundCapacitySats: 1000, - isGeoBlocked: false - ) - ) - } } diff --git a/BitkitTests/TabBarReceiveTests.swift b/BitkitTests/TabBarReceiveTests.swift deleted file mode 100644 index 470cff36b..000000000 --- a/BitkitTests/TabBarReceiveTests.swift +++ /dev/null @@ -1,67 +0,0 @@ -@testable import Bitkit -import XCTest - -@MainActor -final class TabBarReceiveTests: XCTestCase { - func testSpendingReceiveUsesFreshVariableAmountCheck() { - let wallet = WalletViewModel() - wallet.channels = [ - .mock(isChannelReady: true, isUsable: true, inboundCapacityMsat: 50_000_000), - ] - wallet.invoiceAmountSats = 100_000 - - XCTAssertFalse(wallet.canCreateReceiveLightningInvoice) - XCTAssertTrue(wallet.canCreateReceiveLightningInvoice(amountSats: nil)) - XCTAssertFalse( - TabBar.shouldOpenSpendingCjitEntry( - isSpendingWallet: true, - wallet: wallet, - hasPendingTransfersToSpending: false - ) - ) - } - - func testSpendingReceiveOpensCjitOnlyWhenVariableLightningReceiveIsUnavailable() { - let wallet = WalletViewModel() - - XCTAssertTrue( - TabBar.shouldOpenSpendingCjitEntry( - isSpendingWallet: true, - wallet: wallet, - hasPendingTransfersToSpending: false - ) - ) - - wallet.channels = [ - .mock(isChannelReady: true, isUsable: true, inboundCapacityMsat: 1000), - ] - - XCTAssertFalse( - TabBar.shouldOpenSpendingCjitEntry( - isSpendingWallet: true, - wallet: wallet, - hasPendingTransfersToSpending: false - ) - ) - } - - func testSpendingReceiveDoesNotOpenCjitOutsideSpendingWalletOrWithPendingTransfers() { - let wallet = WalletViewModel() - - XCTAssertFalse( - TabBar.shouldOpenSpendingCjitEntry( - isSpendingWallet: false, - wallet: wallet, - hasPendingTransfersToSpending: false - ) - ) - - XCTAssertFalse( - TabBar.shouldOpenSpendingCjitEntry( - isSpendingWallet: true, - wallet: wallet, - hasPendingTransfersToSpending: true - ) - ) - } -} diff --git a/changelog.d/next/1097.removed.md b/changelog.d/next/1097.removed.md new file mode 100644 index 000000000..24fc843c9 --- /dev/null +++ b/changelog.d/next/1097.removed.md @@ -0,0 +1 @@ +Bitkit no longer opens new Lightning channels or instant receive channels, and wallets with Lightning channels show the date the channels close so funds can be moved to savings first. From 8c57df27d80a6e3021281b16df018615c3a7ca01 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Thu, 22 Oct 2026 12:36:34 +0200 Subject: [PATCH 02/12] test: retire channel purchase journeys and add channels closing journeys --- journeys/README.md | 52 +++++++-- journeys/amount-limits/README.md | 110 +++++------------- .../external-amount-over-max.xml | 5 +- .../transfer-spending-advanced-over-max.xml | 51 -------- .../transfer-spending-over-max.xml | 29 ----- journeys/channels-closing/README.md | 43 +++++++ .../channels-closing-notice.xml | 33 ++++++ journeys/cjit-notifications/README.md | 40 ++++--- .../cjit-background-notification.xml | 26 ----- .../cjit-push-single-notification.xml | 23 ---- ...n-cjit-channel-no-payment-notification.xml | 28 +++-- journeys/hardware-wallet/README.md | 40 +++---- .../hardware-wallet/activity-blue-icons.xml | 5 +- journeys/hardware-wallet/detail-overview.xml | 6 +- .../passphrase-transfer-to-spending.xml | 36 ------ .../transfer-to-spending-max-lsp-cap.xml | 22 ---- .../transfer-to-spending-node-warmup.xml | 19 --- .../transfer-to-spending-over-max.xml | 39 ------- .../hardware-wallet/transfer-to-spending.xml | 27 ----- journeys/notification-permission/README.md | 66 +++-------- ...ceive-cjit-confirm-notification-toggle.xml | 23 ---- ...ive-cjit-liquidity-notification-toggle.xml | 24 ---- .../toggle-off-and-system-settings-route.xml | 29 ----- ...r-spending-confirm-notification-toggle.xml | 26 ----- .../receive-onchain-only-without-channel.xml | 31 +++++ journeys/receive/receive-tabs-wraparound.xml | 18 ++- journeys/receive/usdt-receive-swipe-tabs.xml | 21 ++-- .../spending-confirm-amount-change.xml | 38 ------ journeys/usdt-wallet-navigation.xml | 2 +- 29 files changed, 291 insertions(+), 621 deletions(-) delete mode 100644 journeys/amount-limits/transfer-spending-advanced-over-max.xml delete mode 100644 journeys/amount-limits/transfer-spending-over-max.xml create mode 100644 journeys/channels-closing/README.md create mode 100644 journeys/channels-closing/channels-closing-notice.xml delete mode 100644 journeys/cjit-notifications/cjit-background-notification.xml delete mode 100644 journeys/cjit-notifications/cjit-push-single-notification.xml delete mode 100644 journeys/hardware-wallet/passphrase-transfer-to-spending.xml delete mode 100644 journeys/hardware-wallet/transfer-to-spending-max-lsp-cap.xml delete mode 100644 journeys/hardware-wallet/transfer-to-spending-node-warmup.xml delete mode 100644 journeys/hardware-wallet/transfer-to-spending-over-max.xml delete mode 100644 journeys/hardware-wallet/transfer-to-spending.xml delete mode 100644 journeys/notification-permission/receive-cjit-confirm-notification-toggle.xml delete mode 100644 journeys/notification-permission/receive-cjit-liquidity-notification-toggle.xml delete mode 100644 journeys/notification-permission/toggle-off-and-system-settings-route.xml delete mode 100644 journeys/notification-permission/transfer-spending-confirm-notification-toggle.xml create mode 100644 journeys/receive/receive-onchain-only-without-channel.xml delete mode 100644 journeys/transfer/spending-confirm-amount-change.xml diff --git a/journeys/README.md b/journeys/README.md index 311ff8e29..7863662c5 100644 --- a/journeys/README.md +++ b/journeys/README.md @@ -25,7 +25,7 @@ separately confirmed the app is wrong. What this proves, and the preconditions needed to prove it. Tap the Spending balance card on the home screen - Verify the spending amount screen (id "SpendingAmount") is visible + Verify the spending wallet screen (id "TotalBalance-primary") is visible ``` @@ -49,7 +49,7 @@ xcodebuildmcp simulator snapshot-ui # semantic snapshot with e xcodebuildmcp ui-automation tap --element-ref e12 # tap one ref from the latest snapshot xcodebuildmcp ui-automation type-text --element-ref e8 --text "..." xcodebuildmcp ui-automation wait-for-ui --predicate textContains --text "Spending Balance Maximum" -xcodebuildmcp ui-automation wait-for-ui --identifier SpendingAmount --predicate exists +xcodebuildmcp ui-automation wait-for-ui --identifier TotalBalance-primary --predicate exists xcodebuildmcp simulator screenshot # only when a snapshot cannot settle a question ``` @@ -111,6 +111,22 @@ script, and there is no iOS copy yet — #694 tracks porting it. The relative pa journeys already reach for `../bitkit-docker` the same way. If you do not have that clone, fund the wallet however you normally do against regtest and skip the helper. +### Getting a Lightning channel + +Blocktank is winding down, and this build no longer buys channels from it (synonymdev/bitkit-ios#1097): +Transfer To Spending, the Receive sheet's CJIT button and gift codes are gone. A journey that needs a +Lightning channel as its precondition says "a wallet with an open Lightning channel"; get one in either way: + +- Install a build from before that change (master before synonymdev/bitkit-ios#1097), open a channel with + Transfer To Spending, then install this build over it. The wallet, its channel and its balances carry over. +- Or open a manual channel to the LSP node on this build: Settings › Advanced › Lightning + Connections › add connection ("Get Started", "Add Connection" or the navigation bar `+`, id + `NavigationAction`; it opens Funding Advanced) › Manual Setup (id `FundManual`), then enter the node id, + host and port from `../bitkit-android/lsp GET /info`, pick an amount, confirm, and mine blocks with + `../bitkit-android/lsp POST /regtest/chain/mine '{"count":3}'`. The Spending balance is available once + the channel is ready. A channel this wallet opens holds outbound liquidity only, so a journey that + receives over Lightning needs the first route, which buys inbound capacity. + ### Paykit amount-entry rates `paykit-quoted-usdt.xml` preserves the originally entered amount across denomination switches until @@ -166,6 +182,8 @@ Known naming differences: | Send available balance | `AvailableAmount` and `available_balance` (Android emits both) | `AvailableAmount` | | Send max | `SendAmountMax` | *(no button — tap `AvailableAmount`)* | | External amount available | — | `ExternalAmountAvailable` | +| Wallet screen balance | `TotalBalance` | `TotalBalance-primary` | +| Lightning Connections add connection | `add_connection_button` | — *(no identifier on the bottom "Get Started"/"Add Connection" button; the navigation bar `+` is `NavigationAction`)* | | Payment Request details screen | `PaymentRequestDetailsScreen` | `PaymentRequestDetailScreen` | | Hardware wallet receive tab | `Tab-hardware` (after bitkit-android#1231) | `Tab-trezor` for a Trezor wallet, `Tab-jade` for a Jade | | Hardware sign amount | `HardwareSendAmount` | `HardwareSendSignAmount` | @@ -180,7 +198,8 @@ subscription carries the same `paymentRequestId` and they can list together — and lets those rows collide. Match on the prefix when a journey needs to work on both platforms. Everything else — `N0`–`N9`, `N000`, `NDecimal`, `NRemove`, `SpendingAmount*`, `SpendingAdvanced*`, -`External*`, `Hardware*`, `Widget*` — matches Android exactly. +`External*`, `Hardware*`, `Widget*`, `ChannelsClosingBanner`, `ChannelsClosingNotice` — matches Android +exactly. ## Capabilities @@ -197,10 +216,10 @@ journey PR, which is what made this file conflict on every merge. | USDT wallet navigation without funding | an onboarded build with USDT configured; wallet syncing remains silent while offline; payment-action errors remain available — [USDT QA](../Docs/USDT-QA.md) | | Controlled USDT quotes, provider history, funded submission and recovery | an explicitly configured local USDT gateway/RPC/bundler fixture with owned test accounts and funds; absent from the standard journey environment. Report these steps as blocked unless that setup and transfer authorization are supplied — [USDT QA](../Docs/USDT-QA.md) | | On-chain funds and blocks on regtest | `../bitkit-android/lsp` deposit and mine, borrowed from the sibling Android checkout until #694 lands an iOS copy — [Backend preconditions](#backend-preconditions) | -| Lightning channels, CJIT orders and quoted maxima | the LSP the `E2E_BUILD` app targets, plus its node as an external LN peer — [Backend preconditions](#backend-preconditions), [amount-limits](amount-limits/README.md) | +| Lightning channels and quoted maxima | the LSP the `E2E_BUILD` app targets, plus its node as an external LN peer; this build opens channels only through Lightning Connections › add connection — [Getting a Lightning channel](#getting-a-lightning-channel), [amount-limits](amount-limits/README.md) | | A hardware wallet to pair, watch and sign with | the deterministic Trezor emulator from `bitkit-docker`, reached through Trezor Bridge on the host; the simulator has no Bluetooth LE and iOS cannot do WebUSB, so BLE pairing and USB are not covered — [hardware-wallet](hardware-wallet/README.md) | -| Push notifications to a backgrounded or killed app | a real APNs round trip on an attached physical device, never the simulator, read back from Notification Center — [cjit-notifications](cjit-notifications/README.md) | -| The OS notification-permission dialog | the one-shot `UNUserNotificationCenter` alert, reset with `xcrun simctl uninstall to.bitkit` and a rebuild — [notification-permission](notification-permission/README.md) | +| Notifications read back from Notification Center | an attached physical device, never the simulator — [cjit-notifications](cjit-notifications/README.md). Push to a backgrounded or killed app via a CJIT order is retired with CJIT | +| The OS notification-permission dialog | the one-shot `UNUserNotificationCenter` alert, reset with `xcrun simctl uninstall to.bitkit` and a rebuild; no journey uses it since the suite was retired — [notification-permission](notification-permission/README.md) | | An incoming Payment Request from a linked issuer | the fixture issuer, saved as a contact and linked as identities — [payment-requests](payment-requests/README.md) | | A Pubky identity and a two-wallet marketplace purchase | a Bitkit-generated Pubky profile, plus the integration fixture runtime: Pubky testnet, Paykit Server, regtest bitcoind and Fulcrum — [pubky-auth](pubky-auth/README.md), [pubky-marketplace](pubky-marketplace/README.md) | | LNURL pay, withdraw, channel and auth, and Lightning Addresses | the `bitkit-docker` `lnurl-server` on local regtest, reached by a simulator app built with `E2E_BUILD`, whose default `E2E_BACKEND=local` targets the host at `127.0.0.1`; it issues memo invoices, so a check that needs a description-hash invoice needs another endpoint | @@ -208,6 +227,23 @@ journey PR, which is what made this file conflict on every merge. Device-clock fault injection requires a separate manual run: [Paykit clock changes](paykit-clock-changes.md). +## Retired journeys + +Blocktank channel purchases were removed in synonymdev/bitkit-ios#1097, so every journey whose route +buys a channel went with them. They are listed here rather than left as stale files; the Android twin +(synonymdev/bitkit-android#1669) retires its counterparts, named in the last column where they differ. + +| Retired journey | Route it covered | Android counterpart | +| --- | --- | --- | +| `amount-limits/transfer-spending-over-max.xml`, `transfer-spending-advanced-over-max.xml` | Transfer To Spending amount and Receiving Capacity number pads | same names | +| `cjit-notifications/cjit-background-notification.xml` | CJIT notification with the app backgrounded | `cjit-foreground-service-notification.xml` | +| `cjit-notifications/cjit-push-single-notification.xml` | CJIT notification from a push to a killed app | same name | +| `hardware-wallet/transfer-to-spending.xml`, `transfer-to-spending-max-lsp-cap.xml`, `transfer-to-spending-node-warmup.xml`, `passphrase-transfer-to-spending.xml` | Hardware wallet Transfer To Spending (the `HardwareTransferToSpending` button is gone) | same names | +| `hardware-wallet/transfer-to-spending-over-max.xml` | Hardware wallet Transfer To Spending amount cap | none — iOS-only | +| `notification-permission/receive-cjit-confirm-notification-toggle.xml`, `receive-cjit-liquidity-notification-toggle.xml`, `transfer-spending-confirm-notification-toggle.xml` | The background-setup toggle on the CJIT and Spending Confirm screens, which nothing navigates to now | same names | +| `notification-permission/toggle-off-and-system-settings-route.xml` | Toggle off on the CJIT confirm screen, then Settings › Notifications into the iOS Settings app | `toggle-off-opens-system-settings.xml` | +| `transfer/spending-confirm-amount-change.xml` | Spending Confirm amount rebuilt at swipe | same name | + ## Not ported **`onchain-receive/confirmed-only-background-notification.xml`.** It covers the notification Android's @@ -228,8 +264,8 @@ required to run the suite. When you port an Android feature, port its journeys too — see the Journeys section in `AGENTS.md`. -Some suites are adapted rather than ported verbatim: `amount-limits` because iOS snaps to the -spending maximum differently, `cjit-notifications` because the notification copy differs, and +Some suites are adapted rather than ported verbatim: `amount-limits` because +`custom-fee-wallet-switch.xml` is iOS-only, `cjit-notifications` because the notification copy differs, and `pubky-marketplace` because the two-wallet payment runs on regtest here. In `pubky-auth`, the watch-only OS handoff journey is iOS-only; `grant-signup.xml` and `grant-signup-existing-identity.xml` are shared with Android. diff --git a/journeys/amount-limits/README.md b/journeys/amount-limits/README.md index d353136ff..8188f4dc7 100644 --- a/journeys/amount-limits/README.md +++ b/journeys/amount-limits/README.md @@ -1,11 +1,11 @@ # Amount-limit journeys These journeys exercise the "block number pad input exceeding the max/available amount" behaviour. -The same `AmountInputViewModel` cap + `maxExceededCount` effect path backs all four amount-entry -screens (Send, Transfer→Spending, Receiving capacity, External node). - -A fifth screen shares that path — the hardware Transfer To Spending amount. It needs a paired device, -so its journey lives in [`../hardware-wallet/transfer-to-spending-over-max.xml`](../hardware-wallet/transfer-to-spending-over-max.xml). +The same `AmountInputViewModel` cap + `maxExceededCount` effect path backs the amount-entry +screens. Two over-max journeys remain: Send and External node. The Transfer→Spending, Receiving-capacity and +hardware Transfer To Spending journeys are retired because Blocktank channel purchases were removed +(synonymdev/bitkit-ios#1097); their screens still back the same view model but no button navigates +to them. `custom-fee-wallet-switch.xml` covers adjacent send preparation: a custom fee switches Spending to Savings, with manual or automatic coin selection after returning through the fee picker. It requires @@ -38,9 +38,8 @@ preparation. Do not submit a payment. then `../bitkit-android/lsp POST /regtest/chain/mine '{"count":3}'`. `mine` prints nothing on success — that is not a failure. There is no iOS copy of that helper yet — see #694 and the top-level README. -2. **Transfer/Spending and Receiving-capacity flows need the node connected to the LSP** so a real - max can be quoted. On the Spending amount screen the max starts at `0` behind a spinner — wait - for it to populate before typing. +2. **The External-node flow needs the node running** so the on-chain maximum can be computed; + wait for the available amount to populate before typing. 3. **The external-node flow needs a reachable Lightning peer.** The staging LSP node works (`../bitkit-android/lsp GET /info` for its id/host/port). @@ -52,96 +51,43 @@ preparation. Do not submit a payment. an over-max state, so the capped value is valid and Continue stays **enabled**. - Toasts are short-lived (`Toast.visibilityTimeShort`) and will have faded before a snapshot round-trip completes. Assert one with `xcodebuildmcp ui-automation wait-for-ui` on its identifier - (`SendAmountExceededToast`, `SpendingAmountExceededToast`, `SpendingAdvancedExceededToast`, - `ExternalAmountExceededToast`) immediately after the over-max keypress. + (`SendAmountExceededToast`, `ExternalAmountExceededToast`) immediately after the over-max keypress. - Prefer `snapshot-ui` and tap elements by identifier (`N9`, `NRemove`) over screen coordinates. ## Verified behaviour -All three walked on an iPhone 17 simulator against the stag0 regtest backend. **Send caps by -rejecting the keypress while the two transfer screens snap to the maximum, so assert "does not exceed -the stated maximum" — never a specific value:** +Send was walked on an iPhone 17 simulator against the stag0 regtest backend. **Send caps by rejecting +the keypress, so assert "does not exceed the stated maximum" — never a specific value:** | Screen | Cap | Nine taps on `N9` | After one `NRemove` | | --- | --- | --- | --- | | Send | 297 393 | **99 999** — the largest all-9s value still under the cap | 9 999 | -| Transfer → Spending | 296 522 | **296 522** — clamped to the max exactly | 29 652 | -| Transfer → Spending | 22 016 | **22 016** — same shape, a later run on a wallet with less LSP headroom | 2 201 | -| Receiving capacity | 564 | **564** — clamped to the max exactly | 56 | - -The cap column is whatever that screen enforces, not the wallet balance: for the two transfer rows it -is the settled maximum, and it moves with LSP headroom between runs. Assert the shape, never a value. -The receiving-capacity row is the behaviour PR #686 adds — Android leaves `99` there, because it -rejects the third keypress instead of snapping. - -On Send and Transfer → Spending, Continue stays enabled throughout: the cap is applied to the input -rather than leaving it in an invalid state. On the receiving-capacity screen Continue can still be -disabled at the cap, because it also enforces a minimum — see the `Min` > `Max` note below. The -exceeded toasts fire on the first rejected keypress and are gone well before a snapshot round-trip -returns — race a `wait-for-ui` on the toast identifier against the taps. - -The Transfer → Spending max populates from the on-chain balance and cached Blocktank info, so the -screen is reachable and cappable even with the regtest backend down. That is *not* a substitute for -a real LSP quote: without one the numbers are not the ones a real transfer would use. - -### Two things the walk turned up - -**The settling window is shorter than a snapshot round-trip.** `isSettlingAdvancedCapacity` disables -the number pad and Continue for exactly as long as the budget read and the LSP quotes take, and -against a warm stag0 LSP that is under a second — eight back-to-back snapshots raced against the -Advanced tap all came back with the pad already enabled. The gate is real (`NumberPad(isDisabled:)` -is wired to it, and disabled buttons drop out of the snapshot's Targets list, which is how the -Spending amount screen's own spinner is visible), but do not expect to catch it here. Treat -"already settled" as a pass. -**`Min` can exceed `Max` once LSP headroom runs out.** On a wallet with two existing Blocktank -channels the walk found `SpendingAdvancedMin` at 77 984 and `SpendingAdvancedMax` at 564 — the -minimum is derived from the client balance, the maximum from what is left of the LSP's channel-size -cap, and nothing reconciles them. Continue then stays disabled at every capacity, so the journey's -tail (Continue → order created) cannot be walked. That is a property of the regtest LSP allocation, -not of this PR; close a channel to free headroom, or accept that the run covers the cap behaviour -only. +On Send, Continue stays enabled throughout: the cap is applied to the input rather than leaving it in +an invalid state. The exceeded toasts fire on the first rejected keypress and are gone well before a +snapshot round-trip returns — race a `wait-for-ui` on the toast identifier against the taps. -## Transfer maximums are settled before they are offered - -[#686](https://github.com/synonymdev/bitkit-ios/pull/686) (porting -[bitkit-android#1179](https://github.com/synonymdev/bitkit-android/pull/1179) and -[#1180](https://github.com/synonymdev/bitkit-android/pull/1180)) settles both transfer maximums on an -amount the wallet can actually pay the LSP order fee for, rather than deriving one and hoping it -fits. Three behaviours it adds are covered by `transfer-spending-advanced-over-max.xml`: - -- **The number pad and Continue are disabled while the maximum settles.** Settling costs one or two - live fee quotes, so the advanced screen holds `NumberPad(isDisabled:)` until they return. Min, - Default and Max stay tappable — that is the iOS shape; Android disables the amount buttons too. -- **An entered capacity above the settled maximum snaps down to it**, with - `SpendingAdvancedExceededToast` naming the settled value. This is the snap - `SpendingAmount.onMaxExceeded()` already performed, now on the advanced screen as well. -- **Tapping Max before the maximum settles is corrected once it does.** `updateInputCap()` brings the - entered amount down when the settled maximum lands below it, so an early Max does not leave a - capacity selected that no longer exists. - -The journey sizes the transfer at **MAX** rather than 25% for this reason: settling only bites when -the client balance and the receiving capacity together crowd the funding budget, and at 25% the -settled maximum is simply the LSP's advertised maximum. - -**Regtest is regression coverage here, not proof.** The staging/regtest service fee *falls* as the -client balance rises, where production's rises, which leaves a vulnerable window roughly 2 satoshis -wide against about 37 in production. Expect the settled maximum to equal the advertised one and the -two "comes down" steps to hold trivially. The unit tests in `BitkitTests/TransferViewModelTests.swift` -are the gate for the fix itself. +The transfer-maximum settling from [#686](https://github.com/synonymdev/bitkit-ios/pull/686) is no +longer reachable from the UI; the unit tests in `BitkitTests/TransferViewModelTests.swift` remain its +gate. ## Identifiers used - Number pad keys: digits `N0`–`N9`, triple-zero `N000`, decimal `NDecimal`, delete `NRemove`. - Send: screen `SendAmount`, field `SendNumberField`, available `AvailableAmount`, continue `ContinueAmount`; recipient `RecipientManual` / `RecipientInput` / `AddressContinue`; home Send button `Send`. -- Transfer→Spending: screen `SpendingAmount`, field `SpendingAmountNumberField`, - available `SpendingAmountAvailable`, 25% `SpendingAmountQuarter`, max `SpendingAmountMax`, - continue `SpendingAmountContinue`. -- Receiving capacity: screen `SpendingAdvanced`, field `SpendingAdvancedNumberField`, - min/default/max `SpendingAdvancedMin`/`SpendingAdvancedDefault`/`SpendingAdvancedMax`, - continue `SpendingAdvancedContinue`. -- External: funding `FundManual`; connection `NodeIdInput`/`HostInput`/`PortInput`/`ExternalContinue`; +- External: Lightning Connections add button (navigation bar `+`, `NavigationAction`; the bottom + "Get Started"/"Add Connection" button has no identifier, Android `add_connection_button`) opens + Funding Advanced, then `FundManual`; connection `NodeIdInput`/`HostInput`/`PortInput`/`ExternalContinue`; amount screen `ExternalAmount`, field `ExternalAmountNumberField`, available `ExternalAmountAvailable`, 25% `ExternalAmountQuarter`, max `ExternalAmountMax`, continue `ExternalAmountContinue`. + +## Retired journeys + +`transfer-spending-over-max.xml` and `transfer-spending-advanced-over-max.xml` were retired because +Blocktank channel purchases were removed (synonymdev/bitkit-ios#1097), and so was +`../hardware-wallet/transfer-to-spending-over-max.xml`. Their routes started at the Savings wallet's +"Transfer To Spending" button or the hardware wallet's, which are gone. Their identifiers +(`TransferToSpending`, `SpendingAmount*`, `SpendingAdvanced*`, `HardwareTransferAmount*`) are no +longer reachable from the UI. diff --git a/journeys/amount-limits/external-amount-over-max.xml b/journeys/amount-limits/external-amount-over-max.xml index aa3b62239..c7f8bdfcd 100644 --- a/journeys/amount-limits/external-amount-over-max.xml +++ b/journeys/amount-limits/external-amount-over-max.xml @@ -8,12 +8,15 @@ port from `../bitkit-android/lsp GET /info`, borrowed from the sibling Android checkout because there is no iOS copy of that helper yet (see #694). Start on the wallet home screen. + iOS: the bottom "Get Started"/"Add Connection" button has no identifier (Android + "add_connection_button"); the navigation bar "+" (id "NavigationAction") opens the same screen. + Do NOT assert that Continue is disabled at the over-max input: the input is capped (never left in an over-max state), so the capped value is valid and Continue stays enabled. Open Settings, then the "Advanced" tab, then "Lightning Connections" - Tap "Get Started" to open the funding options screen + Tap "Get Started" ("Add Connection" when a connection already exists, or the navigation bar "+" with id "NavigationAction") to open the Funding Advanced screen, which offers LNURL Channel QR and Manual Setup Tap "Manual Setup" (id "FundManual") Type the node id into the node id field (id "NodeIdInput") Type the host into the host field (id "HostInput") diff --git a/journeys/amount-limits/transfer-spending-advanced-over-max.xml b/journeys/amount-limits/transfer-spending-advanced-over-max.xml deleted file mode 100644 index 6fde01bec..000000000 --- a/journeys/amount-limits/transfer-spending-advanced-over-max.xml +++ /dev/null @@ -1,51 +0,0 @@ - - - Verifies the receiving capacity (advanced) screen settles its maximum on a capacity the wallet - can pay the order fee for before offering it, holds the number pad disabled while that settles, - and snaps an entered capacity above the settled maximum down to it with the "Receiving Capacity - Maximum" warning toast — while still allowing deleting digits at the cap. - - Precondition: onboarded dev wallet with a POSITIVE on-chain Savings balance and a running node - connected to the LSP. Start on the wallet home screen. The advanced screen is reached by first - setting a spending amount and continuing to the confirm screen. - - Adapted from Android on purpose, for the behaviour PR #686 ports from bitkit-android#1179 and - bitkit-android#1180. Three deviations, all deliberate: - * The transfer is sized at MAX rather than 25%. Settling only bites when the receiving capacity - and the client balance together crowd the funding budget; at 25% there is enough headroom that - the settled maximum is the LSP's advertised maximum and every "comes down" step is a no-op. - * iOS snaps the entered capacity down to the settled maximum, the way `SpendingAmount` already - does; Android rejects the keypress and leaves the display at the largest all-9s value still - under the cap. Both satisfy "does not exceed the maximum". - * iOS disables the number pad and Continue while the maximum settles, but leaves Min, Default - and Max tappable — tapping Max early is corrected once the maximum settles. Android disables - the amount buttons too. - - Walked on an iPhone 17 simulator against the stag0 regtest LSP, where two findings shaped the - steps below. The settle finished faster than a single snapshot round-trip, so the disabled number - pad was never caught — the step is written to accept "already settled". And with the LSP headroom - already consumed by two channels, the offered minimum (77 984) sat above the offered maximum - (564), which leaves Continue disabled at every capacity: the cap steps still hold, the - order-creation tail cannot be walked. Both are properties of the regtest allocation rather than - of the app; `README.md` has the detail. Regtest is regression coverage for this journey, not - proof of the fix — the unit tests are that. - - - Tap the Savings balance card on the home screen (id "ActivitySavings") - Tap "Transfer To Spending" (id "TransferToSpending") - If a transfer intro screen appears, tap "Get Started" - Wait until the spending amount screen (id "SpendingAmount") has loaded a positive maximum (id "SpendingAmountAvailable") - Tap "MAX" (id "SpendingAmountMax") to size the transfer at the ceiling, where the least headroom is left for the receiving capacity - Tap Continue (id "SpendingAmountContinue") - On the confirm screen, tap "Advanced" (id "SpendingConfirmAdvanced") - Verify the receiving capacity screen (id "SpendingAdvanced") is visible - Verify the number pad and Continue are disabled while the maximum settles — a disabled button drops out of the snapshot's Targets list, so "N9" and "SpendingAdvancedContinue" missing from Targets is the signal, not an enabled flag. Against a warm LSP the settle finishes faster than one snapshot round-trip; report "already settled" rather than failing the step - Tap "MAX" (id "SpendingAdvancedMax") — unlike the number pad it stays tappable while the maximum settles - Wait for the number pad to be enabled (id "N9", predicate enabled), then verify the amount in the input field (id "SpendingAdvancedNumberField") does not exceed the maximum receiving capacity now offered - Tap the "9" key (id "N9") nine times to enter a capacity far larger than the maximum allowed - Verify a "Receiving Capacity Maximum" warning toast appears (id "SpendingAdvancedExceededToast") and names the settled maximum - Verify the amount in the input field (id "SpendingAdvancedNumberField") snapped to the maximum receiving capacity exactly, and does not exceed it - Tap the delete key (id "NRemove") once - Verify the amount in the input field (id "SpendingAdvancedNumberField") decreased after the delete - - diff --git a/journeys/amount-limits/transfer-spending-over-max.xml b/journeys/amount-limits/transfer-spending-over-max.xml deleted file mode 100644 index 8f9f00c23..000000000 --- a/journeys/amount-limits/transfer-spending-over-max.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - Verifies the "Transfer to Spending" amount number pad blocks input exceeding the maximum allowed - transfer amount, shows the "Spending Balance Maximum" warning toast, and still allows deleting - digits while at the cap. - - Precondition: onboarded dev wallet with a POSITIVE on-chain Savings balance and a running node - connected to the LSP (so a real max can be quoted). The max starts at 0 behind a spinner — wait - for it to populate. Note the cap may be lower than the visible "Available" due to fees. Start on - the wallet home screen. - - Enter via the Savings card, not the Spending card: the Spending screen only offers - "Transfer From Savings" while the spending balance is zero, and shows "Transfer To Savings" - once a balance exists. The Savings card's "Transfer To Spending" reaches the same screen in - both states. - - - Tap the Savings balance card on the home screen (id "ActivitySavings") - Tap "Transfer To Spending" (id "TransferToSpending") - If a transfer intro screen appears, tap "Get Started" - Verify the spending amount screen (id "SpendingAmount") is visible - Wait until the available/maximum amount (id "SpendingAmountAvailable") finishes loading and shows a positive value - Tap the "9" key (id "N9") nine times to enter an amount far larger than the maximum allowed - Verify a "Spending Balance Maximum" warning toast appears (id "SpendingAmountExceededToast") - Verify the amount in the input field (id "SpendingAmountNumberField") does not exceed the stated maximum - Tap the delete key (id "NRemove") once - Verify the amount in the input field (id "SpendingAmountNumberField") decreased after the delete - - diff --git a/journeys/channels-closing/README.md b/journeys/channels-closing/README.md new file mode 100644 index 000000000..f0d1803b0 --- /dev/null +++ b/journeys/channels-closing/README.md @@ -0,0 +1,43 @@ +# Channels closing journeys + +Synonym is winding down Blocktank, its Lightning service provider. synonymdev/bitkit-ios#1097 removed +every path that buys a new Lightning channel from it and added a notice for users who still have one. +The Android twin is synonymdev/bitkit-android#1669. + +## What the notice does + +- **Home banner** `ChannelsClosingBanner`, "CHANNELS CLOSE ON DEC 15, 2026", built from the same + component as the "TRANSFER IN PROGRESS" banner. It shows for a wallet with a Lightning channel, open + or pending, to a Blocktank (trusted LSP) node, whether or not the wallet has activity; channels to other + nodes (LNURL channel, manual setup to a third-party node) do not show it. Tapping it opens the Spending + wallet. +- **Spending wallet notice** `ChannelsClosingNotice` under the balance header, with the same date and the + same Blocktank-channel condition. +- The date is a placeholder constant (Dec 15, 2026) until the shutdown date is final. Journeys assert the + banner and the notice agree and read the date from the screen instead of hardcoding it. +- **Removed:** Savings "Transfer To Spending" (`TransferToSpending`), Spending "Transfer from Savings" + (`TransferFromSavings`), hardware wallet "Transfer To Spending" (`HardwareTransferToSpending`), the + Home "Spend" suggestion card (`Suggestion-lightning`), the Receive sheet's CJIT button and Spending + onboarding, the Edit Invoice CJIT offer, the TabBar Receive shortcut into the CJIT amount screen and + gift codes. Transfer To Savings, send, receive over an existing channel, LNURL channel and manual + external node stay. + +## Mandatory setup + +1. **A wallet with an open Lightning channel.** See [Getting a Lightning channel](../README.md#getting-a-lightning-channel): + install a build from before synonymdev/bitkit-ios#1097, open a channel with Transfer To Spending, + then install this build over it; or open a manual channel to the LSP node from Settings › + Advanced › Lightning Connections › add connection › Manual Setup. + +## Journeys + +| Journey | Covers | +| --- | --- | +| `channels-closing-notice.xml` | Banner on Home → tap → Spending wallet notice and Transfer To Savings; Savings wallet has no Transfer To Spending | +| [`../receive/receive-onchain-only-without-channel.xml`](../receive/receive-onchain-only-without-channel.xml) | The opposite precondition: a funded wallet with no channels shows no banner, no Spend card and a Savings-only Receive sheet | + +## Identifiers used + +`ChannelsClosingBanner`, `ChannelsClosingNotice`, `ActivitySavings`, `ActivitySpending`, +`TotalBalance-primary` (Android `TotalBalance`), `TransferToSavings`, `NavigationBack`, +`TransferToSpending` (asserted absent). diff --git a/journeys/channels-closing/channels-closing-notice.xml b/journeys/channels-closing/channels-closing-notice.xml new file mode 100644 index 000000000..c5e68d6b5 --- /dev/null +++ b/journeys/channels-closing/channels-closing-notice.xml @@ -0,0 +1,33 @@ + + + Verifies the notice that Synonym is winding down Blocktank (its Lightning service provider) for a + user with a Lightning channel (synonymdev/bitkit-ios#1097). Home shows a banner, + "CHANNELS CLOSE ON DEC 15, 2026" (id "ChannelsClosingBanner"), tapping it opens the Spending + wallet, which shows "Your Lightning channels will be closed on Dec 15, 2026. Move your spending + balance to savings before then." (id "ChannelsClosingNotice") under the balance header, and + Transfer To Savings (id "TransferToSavings") is still offered. The Savings wallet has no + Transfer To Spending button (id "TransferToSpending"), since new channels can no longer be + bought. The date is a placeholder constant until the shutdown date is final; assert whatever + date the build shows, and keep the banner and the notice in agreement. + + iOS: "Press the system back button" is the navigation bar back button (id "NavigationBack"). + + Precondition: onboarded regtest wallet with a Lightning channel to the LSP (Blocktank) node, open + or pending, and a POSITIVE Spending balance; channels to other nodes do not show the notice. This build cannot buy a channel from Blocktank, so get + one by installing a build from before the change (master before synonymdev/bitkit-ios#1097), + opening a channel with Transfer To Spending and installing this build over it, or by opening a + manual channel to the LSP node (see ../README.md, Getting a Lightning channel). Start on + the wallet home screen. + + + Verify the Spending balance card (id "ActivitySpending") shows a positive amount + Verify the banner (id "ChannelsClosingBanner") is visible on the home screen with the text "CHANNELS CLOSE ON DEC 15, 2026" and the same style as the "TRANSFER IN PROGRESS" banner; note the date it shows + Tap the banner (id "ChannelsClosingBanner") + Verify the Spending wallet screen is open, showing the balance (id "TotalBalance-primary"; Android "TotalBalance") + Verify the notice (id "ChannelsClosingNotice") reads "Your Lightning channels will be closed on Dec 15, 2026. Move your spending balance to savings before then." with the same date the banner showed + Verify the "Transfer To Savings" button (id "TransferToSavings") is visible + Press the system back button, then tap the Savings balance card (id "ActivitySavings") + Verify the Savings wallet screen is open and no "Transfer To Spending" button (id "TransferToSpending") is shown + Press the system back button, tap the Spending balance card (id "ActivitySpending") and verify the notice (id "ChannelsClosingNotice") is still shown + + diff --git a/journeys/cjit-notifications/README.md b/journeys/cjit-notifications/README.md index 2a0e40b06..1f6067042 100644 --- a/journeys/cjit-notifications/README.md +++ b/journeys/cjit-notifications/README.md @@ -1,7 +1,19 @@ -# CJIT notification journeys +# Channel-ready notification journeys -Verify that a CJIT payment surfaces exactly one notification, and that a regular (non-CJIT) channel -opening is never reported as a received payment. +One journey remains: `non-cjit-channel-no-payment-notification.xml`. Opening a regular (non-CJIT) +Lightning channel must not be reported as a received payment, whether it is an "Incoming Payment" +notification or a stack of duplicates. When the app is in the foreground the only feedback is the +in-app "Spending balance ready" toast. + +The folder keeps its name so the file path stays stable for the Android twin. + +## Retired journeys + +`cjit-background-notification.xml` and `cjit-push-single-notification.xml` were retired because +Blocktank channel purchases, CJIT included, were removed (synonymdev/bitkit-ios#1097). They paid a +CJIT order through the LSP and asserted a single Blocktank-pushed notification with the app +backgrounded and killed. Nothing in the app creates a CJIT order now, so there is no way to drive the +push. ## Adapted from Android — read this first @@ -21,24 +33,24 @@ Consequences for the port: - **Android issue #1 (missing thousands separators) has no iOS counterpart.** iOS never puts an amount in the notification, so there is no formatting to assert. Those actions are dropped rather than rewritten into something the app was never meant to do. -- **Android issue #2 (double notification) does port**, as "exactly one notification". iOS has no - foreground service and no `WakeNodeWorker`; the Notification Service Extension is the only poster, - so the duplicate would have to come from a duplicate push. -- **Android issue #3 (channel open reported as a payment) ports directly** and is the strongest of - the three: `orderPaymentConfirmed` must render as "Spending Balance Ready", never "Incoming Payment". -- `cjit-foreground-service-notification.xml` becomes `cjit-background-notification.xml` — iOS has no - foreground service, so the equivalent state is simply "app backgrounded". +- **Android issue #2 (double notification)** ported as "exactly one notification" in the retired CJIT + journeys. iOS has no foreground service and no `WakeNodeWorker`; the Notification Service Extension + is the only poster, so a duplicate would have to come from a duplicate push. +- **Android issue #3 (channel open reported as a payment) ports directly** and is the journey that + remains. A manual channel is not a Blocktank order, so no `orderPaymentConfirmed` push arrives and + no Bitkit notification is expected; none may read "Incoming Payment". ## Preconditions -- **A physical device, not the simulator.** These journeys need a real APNs round trip: the app must - hold a device token Blocktank can push to, and the notification extension must decrypt a real - Blocktank payload. Nothing runs this suite automatically, and that is not a gap to +- **A physical device, not the simulator.** The journey reads Notification Center to prove no + payment notification was posted, which needs an app registered for real APNs pushes. Nothing runs this suite automatically, and that is not a gap to fill here — `ai-device-tests.yml` runs `TrezorBridgeDashboardUITests` on a simulator and is not a journey runner. Run these by hand against an attached device. `test-push-server/` can drive a hand-built push at a known device token when you need to exercise the extension without the LSP. - Onboarded regtest wallet connected to the LSP, with notifications authorized and "Get paid when Bitkit is closed" enabled in Settings → Notifications. -- A funded CJIT entry ready to pay, and no Lightning channel open yet for the CJIT journeys. +- A positive on-chain balance and no Lightning channel yet. This build has no Transfer To Spending, + so the journey opens a manual channel to the LSP node: Settings › Advanced › Lightning Connections › + add connection › Manual Setup. See [Getting a Lightning channel](../README.md#getting-a-lightning-channel). ## Inspecting notifications on iOS diff --git a/journeys/cjit-notifications/cjit-background-notification.xml b/journeys/cjit-notifications/cjit-background-notification.xml deleted file mode 100644 index 81a644398..000000000 --- a/journeys/cjit-notifications/cjit-background-notification.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - iOS counterpart of the Android foreground-service journey (iOS has no foreground service, so the - equivalent state is the app simply being in the background). - - With the app backgrounded, a CJIT payment that opens a JIT channel must produce EXACTLY ONE - notification, posted by the Notification Service Extension with the cjitPaymentArrived copy: - title "Incoming Payment", body "Open Bitkit now to receive your payment". It must NOT be posted - twice, and must NOT use the orderPaymentConfirmed copy. - - Precondition: physical device with an onboarded regtest wallet connected to the LSP, - notifications authorized, "Get paid when Bitkit is closed" enabled. A funded CJIT entry is ready - to pay (see README.md). No Lightning channel open yet — this is the first, channel-opening payment. - - - Confirm "Get paid when Bitkit is closed" is enabled in Settings → Notifications, then go back to the wallet home screen - Send the app to the background by pressing Home, leaving no Bitkit screen in the foreground - Open Console.app with the device selected and filter on "🔔", including info-level messages — /usr/bin/log has no --device flag, so there is no verified one-liner here; if the log is not reachable, skip it and rely on Notification Center below - Pay the CJIT invoice using the sibling Android checkout's helper (no iOS copy yet, see #694): ../bitkit-android/lsp POST /regtest/lightning/pay '{"invoice":"<bolt11>"}' - Wait up to ~30s for the push to arrive and the notification to be delivered - Verify EXACTLY ONE Bitkit notification is posted for this channel opening, not two - Verify its title is "Incoming Payment" and its body is "Open Bitkit now to receive your payment" - Verify the streamed log shows a single "Configured notification" line with type=cjitPaymentArrived, and no orderPaymentConfirmed notification alongside it - Tap the notification, bring the app to the foreground, and verify the received-transaction sheet shows the correct amount - - diff --git a/journeys/cjit-notifications/cjit-push-single-notification.xml b/journeys/cjit-notifications/cjit-push-single-notification.xml deleted file mode 100644 index eff6be393..000000000 --- a/journeys/cjit-notifications/cjit-push-single-notification.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - The pure push path. With the app fully killed, a CJIT payment must still surface a single - notification, delivered by the Notification Service Extension — this is the wake-the-node-and- - open-the-channel path, so it must keep working. The notification must use the cjitPaymentArrived - copy: title "Incoming Payment", body "Open Bitkit now to receive your payment". - - Precondition: physical device with an onboarded regtest wallet connected to the LSP, - notifications authorized, "Get paid when Bitkit is closed" enabled. A funded CJIT entry is ready - to pay (see README.md), and the app force-quit from the app switcher so only the push and the - extension run. - - - Force-quit Bitkit from the app switcher so no Bitkit process is running - Open Console.app with the device selected and filter on "🔔", including info-level messages — /usr/bin/log has no --device flag, so there is no verified one-liner here; if the log is not reachable, skip it and rely on Notification Center below - Pay the CJIT invoice using the sibling Android checkout's helper (no iOS copy yet, see #694): ../bitkit-android/lsp POST /regtest/lightning/pay '{"invoice":"<bolt11>"}' - Wait up to ~30s for the push to wake the extension and deliver the notification - Verify EXACTLY ONE Bitkit notification is posted for this CJIT payment - Verify its title is "Incoming Payment" and its body is "Open Bitkit now to receive your payment" - Verify the streamed log shows exactly one "Configured notification" line, with type=cjitPaymentArrived - Tap the notification to open the app and verify the received amount in the activity list matches the CJIT payment - - diff --git a/journeys/cjit-notifications/non-cjit-channel-no-payment-notification.xml b/journeys/cjit-notifications/non-cjit-channel-no-payment-notification.xml index 619313922..b7653b12f 100644 --- a/journeys/cjit-notifications/non-cjit-channel-no-payment-notification.xml +++ b/journeys/cjit-notifications/non-cjit-channel-no-payment-notification.xml @@ -1,27 +1,35 @@ Opening a regular (non-CJIT) Lightning channel must NOT be reported as a received payment: no - "Incoming Payment" notification, and never several duplicates. The correct notification for this - path is the orderPaymentConfirmed copy, "Spending Balance Ready". When the app is in the - foreground the only feedback is the in-app "Spending balance ready" toast. + "Incoming Payment" notification, and never several duplicates. When the app is in the + foreground the only feedback is the in-app "Spending balance ready" toast. A manual channel is + not a Blocktank order, so Blocktank sends no orderPaymentConfirmed push for it and no Bitkit + notification is expected at all. No extension log assertion here: with the app in the foreground the node holds the .lightning process lock, and NotificationService returns on that lock before it decrypts or logs anything. The observable evidence is the toast plus the absence of an "Incoming Payment" notification. - Precondition: physical device with an onboarded regtest wallet connected to the LSP and a - positive on-chain balance to fund the transfer. No CJIT entry is involved — this is a manual - Transfer channel opening (orderPaymentConfirmed), not a CJIT receive. + Precondition: physical device with an onboarded regtest wallet connected to the LSP, with a + positive on-chain balance to fund the channel and no other Lightning channel (the toast shows + only for the wallet's first channel). No CJIT entry is involved here — this is a manual channel + opening, not a CJIT receive. This build cannot buy a channel from Blocktank + (synonymdev/bitkit-ios#1097), so the channel is opened to the LSP node by hand (see ../README.md, + Getting a Lightning channel). + + iOS: the bottom "Get Started"/"Add Connection" button has no identifier (Android + "add_connection_button"); the navigation bar "+" (id "NavigationAction") opens the same screen. - Keep the app in the foreground on the wallet home screen - Open a regular spending channel via Transfer → Spending, funding some sats and confirming - Mine confirmations if required, using the sibling Android checkout's helper (no iOS copy yet, see #694): ../bitkit-android/lsp POST /regtest/chain/mine '{"count":3}' + Start on the wallet home screen + Open Settings, then the "Advanced" tab (id "Tab-advanced"), then "Lightning Connections" (id "Channels"), tap "Get Started" or "Add Connection" (or the navigation bar "+", id "NavigationAction") and tap "Manual Setup" (id "FundManual") + Enter the LSP node id (id "NodeIdInput"), host (id "HostInput") and port (id "PortInput") from ../bitkit-android/lsp GET /info, tap Continue (id "ExternalContinue"), enter an amount within the available maximum, then continue and confirm to open the channel + Mine confirmations, using the sibling Android checkout's helper (no iOS copy yet, see #694): ../bitkit-android/lsp POST /regtest/chain/mine '{"count":3}' + Return to the wallet home screen and keep the app in the foreground Wait for the ChannelReady event (~5-10s) Verify an in-app "Spending balance ready" toast appears (id "SpendingBalanceReadyToast") Open Notification Center and inspect the Bitkit entries Verify NO "Incoming Payment" notification was posted for this channel opening - Verify any notification posted for it uses the "Spending Balance Ready" copy and not the "Incoming Payment" copy Verify there are not multiple duplicate channel-ready notifications stacked in Notification Center diff --git a/journeys/hardware-wallet/README.md b/journeys/hardware-wallet/README.md index 205c858a2..89e12bd4c 100644 --- a/journeys/hardware-wallet/README.md +++ b/journeys/hardware-wallet/README.md @@ -1,7 +1,7 @@ # Hardware wallet journeys Cover pairing a Trezor, the home tile and detail screen, the Hardware Wallets settings surface, -passphrase (hidden) wallets, wallet-scoped activity, and the watch-only Transfer To Spending flow. +passphrase (hidden) wallets, wallet-scoped activity, and on-chain send. ## Transport: Bridge, not USB @@ -51,10 +51,8 @@ Several journeys mutate pairing state. Run them in this order, or re-pair betwee 1. `connect-home-tile.xml` — pairs the emulator; every other journey assumes it ran. 2. `settings-hardware-wallets.xml`, `connect-flow.xml`, `suggestion-intro-sheet.xml` — each forgets and re-pairs the device, ending paired. -3. `activity-blue-icons.xml`, `activity-detail-hw-tags.xml`, `transfer-to-spending*.xml`, `reconnect.xml`, - `send-onchain.xml`. -4. `passphrase-pairing.xml` → `passphrase-duplicate.xml` → `passphrase-transfer-to-spending.xml` → - `passphrase-settings-remove.xml`. +3. `activity-blue-icons.xml`, `activity-detail-hw-tags.xml`, `reconnect.xml`, `send-onchain.xml`. +4. `passphrase-pairing.xml` → `passphrase-duplicate.xml` → `passphrase-settings-remove.xml`. 5. `detail-overview.xml` **last** — its final step forgets the device. ## Approving prompts on the emulator @@ -70,9 +68,12 @@ container (`docker compose --profile trezor-emulator exec -T trezor-emulator /trezor-user-env/.venv/bin/python3 /opt/bitkit-trezor/trezor-controller.py send-json '{"type":"emulator-press-yes"}'`), which is how a stack that cannot use the host helper presses Yes. -`send-onchain.xml` spends from the emulator's account, so it needs funds beyond what the transfer -journeys leave; fund the account's receive address again from the regtest node before it, and send -to an address of that node. +`send-onchain.xml` spends from the emulator's account, so fund the account's receive address from the +regtest node before it, and send to an address of that node. + +The activity journeys use a fixture with two confirmed hardware receives carrying distinct amounts and +one hardware Transfer To Spending transaction. The Transfer row cannot be created on this build; take +it from a build from before synonymdev/bitkit-ios#1097. ## Checking for passphrase leaks @@ -108,15 +109,8 @@ Walked as far as a simulator allows, against a wallet with two Trezor identities - Row identifiers are inconsistent in the app and this is not a typo in the journeys: `HardwareWalletRowName` has **no** separator, while `HardwareWalletRowDelete_` and `HardwareWalletRow_` use an underscore. The `` is the full `trezor:` device id. -- **The amount number pad was ungated.** `SpendingAmountHw` observed `maxAllowed` without - `initial: true`, and that value lives on the shared `TransferViewModel`, so a screen that opens - with the limits already computed has no change to observe and the fresh `AmountInputViewModel` - keeps `maxAmountOverride` nil. Nine taps on `N9` entered **999 999 999** against an AVAILABLE of - 610 015, with no toast and no snap; Continue stayed disabled, so no unaffordable order was - reachable. Fixed in #686 and covered by `transfer-to-spending-over-max.xml`. - Confirmed resolving without the emulator: `HardwareWalletsSettings`, `HardwareWalletsScreen`, `AddHardwareWallet`, `HardwareWalletIntroScreen`/`Continue`/`Cancel`, `HardwareWalletScreen`, - `HardwareTransferToSpending`, `HardwareTransferAmount` and its Available/25%/MAX/Continue controls, and the number pad keys. ## Identifiers used @@ -135,12 +129,7 @@ Walked as far as a simulator allows, against a wallet with two Trezor identities `RenameHardwareWalletInput`, `RenameHardwareWalletSave`. - Home and detail: tile `ActivityHardware`, screen `HardwareWalletScreen`, `RemoveHardwareWallet`, `RemoveHwWalletDialog`, `HwRemoveKeepBackupToggle`. -- Transfer: `HardwareTransferToSpending`, `HardwareTransferAmount`, - `HardwareTransferAmountAvailable`/`Quarter`/`Max`/`Continue`, - over-max toast `HardwareTransferAmountExceededToast`, `HardwareTransferSign`, - `HardwareTransferSignLearnMore`/`SignAdvanced`/`SignDefault`, - `HardwareTransferOpenTrezorConnect`, `HardwareTransferSigned`, - `HwTransferPassphraseSheet`/`Input`/`Continue`/`Cancel`. +- Transfer: `HardwareTransferToSpending`, asserted absent by `detail-overview.xml`. - Activity: `ActivityShowAll` (home only). Rows use **two different schemes** — `ActivityShort-` in the home recent list, `Activity-` in All Activity and on the hardware wallet screen. Tabs `Tab-all`/`Tab-sent`/`Tab-received`/`Tab-other`, @@ -148,6 +137,15 @@ Walked as far as a simulator allows, against a wallet with two Trezor identities detail chip list `ActivityTags` (only rendered once a tag exists), All Activity tag filter `TagsPrompt` (not `ActivityTags` — that one is detail-screen only), explorer `ActivityTxDetails`. +## Retired journeys + +`transfer-to-spending.xml`, `transfer-to-spending-max-lsp-cap.xml`, +`transfer-to-spending-node-warmup.xml`, `passphrase-transfer-to-spending.xml` and the iOS-only +`transfer-to-spending-over-max.xml` were retired because Blocktank channel purchases were removed +(synonymdev/bitkit-ios#1097): the hardware wallet detail screen no longer has a Transfer To Spending +button (`HardwareTransferToSpending`). The `HardwareTransfer*` and `HwTransferPassphrase*` screens and +identifiers are still in code but nothing in the UI reaches them. + ## Local Payment Completion The iOS `shop-onchain-proof.xml` recovery contract permits local Success after exact transaction, diff --git a/journeys/hardware-wallet/activity-blue-icons.xml b/journeys/hardware-wallet/activity-blue-icons.xml index 554f1dfa2..2f5159f31 100644 --- a/journeys/hardware-wallet/activity-blue-icons.xml +++ b/journeys/hardware-wallet/activity-blue-icons.xml @@ -2,7 +2,10 @@ Verifies hardware wallet on-chain activity in the unified home and All Activity lists, with wallet-scoped blue icons and tab filters. Requires a paired Bridge emulator with at - least two confirmed receives with distinct amounts and one Transfer To Spending activity. + least two confirmed receives with distinct amounts and one Transfer To Spending activity. This build + cannot create that activity (the button was removed in synonymdev/bitkit-ios#1097), so the + fixture comes from a build from before that change (master before it) that signed a hardware + Transfer To Spending; the activity stays in the wallet after installing this build over it. Launch the Bitkit app and go to the wallet home screen diff --git a/journeys/hardware-wallet/detail-overview.xml b/journeys/hardware-wallet/detail-overview.xml index a6ce28433..fd057c59e 100644 --- a/journeys/hardware-wallet/detail-overview.xml +++ b/journeys/hardware-wallet/detail-overview.xml @@ -1,8 +1,8 @@ Opens the hardware wallet detail screen from the home tile and verifies its overview: - the top bar, balance header, the Transfer To Spending entry when funds are present, - the grouped activity list, and the Remove device confirm dialog. The final + the top bar, balance header, the absence of a Transfer To Spending entry (removed with + Blocktank channel purchases, synonymdev/bitkit-ios#1097), the grouped activity list, and the Remove device confirm dialog. The final Remove step forgets the device, so run this last (re-run connect-home-tile.xml to pair again). Requires a paired Bridge emulator (run connect-home-tile.xml first). @@ -10,7 +10,7 @@ Launch the Bitkit app and go to the wallet home screen Tap the hardware wallet tile (id "ActivityHardware") beneath the SAVINGS and SPENDING tiles Verify the hardware wallet detail screen opens (id "HardwareWalletScreen"), showing the device name with a blue bitcoin icon in the top bar and a balance header - If a "Transfer To Spending" button is shown (id "HardwareTransferToSpending"), tap it, verify the transfer amount screen opens (id "HardwareTransferAmount") titled "TRANSFER TO SPENDING", then navigate back to the hardware wallet detail screen; otherwise skip this step + Verify no "Transfer To Spending" button (id "HardwareTransferToSpending") is shown, whether or not the wallet holds funds If the activity list shows transactions, verify their circular icons are blue, then tap the first one, verify an activity detail screen opens, and navigate back Tap the "Remove" button labelled with the device name near the bottom of the screen (id "RemoveHardwareWallet") Verify a confirm dialog appears (id "RemoveHwWalletDialog") explaining that your funds are safe and your coins won't be deleted diff --git a/journeys/hardware-wallet/passphrase-transfer-to-spending.xml b/journeys/hardware-wallet/passphrase-transfer-to-spending.xml deleted file mode 100644 index 17cf31923..000000000 --- a/journeys/hardware-wallet/passphrase-transfer-to-spending.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - Drives Transfer To Spending from a passphrase (hidden) wallet. While the Trezor session that - holds the passphrase is still open the transfer signs straight away; after the session is - dropped Bitkit asks for the passphrase again, and a wrong one is refused instead of signing - from whichever wallet the device happens to have open. Requires the emulator started with - passphrase protection enabled, the hidden wallet from passphrase-pairing.xml paired, and its - native-segwit account holding spendable regtest funds. - - - Launch the Bitkit app and go to the wallet home screen - Tap the hidden wallet tile ("Hidden Trezor") and verify its detail screen opens (id "HardwareWalletScreen") - Tap "Transfer To Spending" (id "HardwareTransferToSpending"), and if the first-run intro is shown tap "Get Started" - Tap the "25%" quick button (id "HardwareTransferAmountQuarter"), then "Continue" (id "HardwareTransferAmountContinue") and wait for the Blocktank order - Verify the sign screen opens (id "HardwareTransferSign") titled "SIGN WITH YOUR DEVICE" - Tap "Open Trezor Connect" (id "HardwareTransferOpenTrezorConnect") and verify no passphrase sheet appears, because the session opened at pairing still holds the hidden wallet - Approve the Recipient, Amount, Locktime, and Summary prompts on the Bridge emulator in order, and verify the transaction signed screen appears (id "HardwareTransferSigned") - Wait for the Processing Payment screen, tap "Continue Using Bitkit", and verify the app returns to the wallet home screen - Force-quit Bitkit: xcodebuildmcp simulator stop --bundle-id to.bitkit - Relaunch it: xcodebuildmcp simulator launch-app --bundle-id to.bitkit - Once the app is back on the home screen, open the hidden wallet tile and start Transfer To Spending again, setting an amount with the "25%" quick button and continuing to the sign screen - Tap "Open Trezor Connect" (id "HardwareTransferOpenTrezorConnect") and verify the passphrase sheet opens (id "HwTransferPassphraseSheet"), because the session that held the passphrase is gone - Type a wrong passphrase "not-the-one" into the input (id "HwTransferPassphraseInput") and tap "Continue" (id "HwTransferPassphraseContinue") - - Confirm on the emulated device for every account the passphrase session reads: the device - prompts once per address type and the call blocks until it is acknowledged. Either tap the - emulator UI or run: ../bitkit-docker/scripts/trezor-emulator send-json '{"type":"emulator-press-yes","id":1}' - - Verify an error toast says the passphrase opens a different wallet, that no signing prompt was shown on the emulator, and that no new transfer appears in the activity list - Navigate to the wallet home screen and verify the hardware tile count is unchanged: the wallet the wrong passphrase opened must not be added - Reopen the sign screen, tap "Open Trezor Connect", type the correct passphrase "bitkit-hidden" and tap "Continue" - Approve the Recipient, Amount, Locktime, and Summary prompts on the Bridge emulator, and verify the transaction signed screen appears (id "HardwareTransferSigned") - Scan the app group for both passphrases, treating only "no match" as clean: UDID=<the simulator this journey is running on>; GROUP=$(xcrun simctl get_app_container "$UDID" to.bitkit groups | awk '{print $2}'); [ -d "$GROUP" ] || { echo "LEAK_CHECK_INVALID: container not found"; exit 1; }; grep -rl -e "bitkit-hidden" -e "not-the-one" "$GROUP"; s=$?; case $s in 1) echo NO_PASSPHRASE_LEAK;; 0) echo "LEAK FOUND";; *) echo "LEAK_CHECK_INVALID: grep exit $s";; esac - Verify the previous command printed NO_PASSPHRASE_LEAK. LEAK_CHECK_INVALID means the check did not scan anything and the journey cannot conclude either way — treat it as a failed step, not a pass - - diff --git a/journeys/hardware-wallet/transfer-to-spending-max-lsp-cap.xml b/journeys/hardware-wallet/transfer-to-spending-max-lsp-cap.xml deleted file mode 100644 index e8f7fdc1f..000000000 --- a/journeys/hardware-wallet/transfer-to-spending-max-lsp-cap.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - Verifies that a funded hardware wallet whose balance is larger than the current - Blocktank/LSP headroom is capped by the transferable spending limit, not by the - device balance. Requires a paired Bridge emulator or physical Trezor with a hardware - balance larger than the available transfer limit, and at least one existing channel or - pending order consuming most of the regtest LSP cap. - - - Launch the Bitkit app and go to the wallet home screen - Verify the hardware wallet tile shows a balance larger than the AVAILABLE amount expected in the transfer flow - Tap the hardware wallet tile (id "ActivityHardware") beneath the SAVINGS and SPENDING tiles, and verify the hardware wallet detail screen opens (id "HardwareWalletScreen") - Tap the "Transfer To Spending" button (id "HardwareTransferToSpending") - Verify the transfer amount screen opens (id "HardwareTransferAmount"), titled "TRANSFER TO SPENDING", and the AVAILABLE amount (id "HardwareTransferAmountAvailable") is lower than the hardware wallet balance because it is capped by LSP headroom - Tap the "MAX" quick button (id "HardwareTransferAmountMax") - Verify the amount field matches the AVAILABLE amount exactly and does not use the full hardware wallet balance - Tap "Continue" (id "HardwareTransferAmountContinue") and wait for the Blocktank order to be created - Verify the sign screen opens (id "HardwareTransferSign"), showing NETWORK FEES, SERVICE FEES, TO SPENDING and TOTAL, without an insufficient-funds toast - Tap "Open Trezor Connect" (id "HardwareTransferOpenTrezorConnect") and approve every hardware-wallet prompt - Verify the transaction signed screen appears (id "HardwareTransferSigned") and then advances to Processing Payment / setting-up progress - - diff --git a/journeys/hardware-wallet/transfer-to-spending-node-warmup.xml b/journeys/hardware-wallet/transfer-to-spending-node-warmup.xml deleted file mode 100644 index 0d633ad6b..000000000 --- a/journeys/hardware-wallet/transfer-to-spending-node-warmup.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - Verifies that starting a hardware-wallet transfer while the Lightning node is still - warming up does not fail or strand the CTA in loading. The flow should show the normal - loading/progress UI, continue once the node reaches the needed state, and land on the - sign screen. Requires a paired and funded hardware wallet. - - - Force-quit Bitkit: xcodebuildmcp simulator stop --bundle-id to.bitkit - Relaunch it: xcodebuildmcp simulator launch-app --bundle-id to.bitkit - As soon as the wallet home screen is visible, tap the hardware wallet tile (id "ActivityHardware") beneath the SAVINGS and SPENDING tiles - Tap the "Transfer To Spending" button (id "HardwareTransferToSpending") - Verify the transfer amount screen or loading/progress UI appears, and no reconnect, node-not-ready, or generic failure toast is shown while the node warms up - Tap the "25%" quick button (id "HardwareTransferAmountQuarter") if the amount screen is shown - Tap "Continue" (id "HardwareTransferAmountContinue") if the amount screen is shown, then wait for order creation and node warm-up to finish - Verify the sign screen opens (id "HardwareTransferSign"), titled "SIGN WITH YOUR DEVICE", and the Continue/Open Trezor Connect CTA is no longer loading - Navigate back without signing so this journey only covers node warm-up behavior - - diff --git a/journeys/hardware-wallet/transfer-to-spending-over-max.xml b/journeys/hardware-wallet/transfer-to-spending-over-max.xml deleted file mode 100644 index 4e078cf8e..000000000 --- a/journeys/hardware-wallet/transfer-to-spending-over-max.xml +++ /dev/null @@ -1,39 +0,0 @@ - - - Verifies the hardware Transfer To Spending amount number pad enforces the transferable maximum: - an entry above it snaps down to the maximum with the "Spending Balance Maximum" warning toast, - and deleting digits still works while at the cap. - - iOS-only — there is no counterpart under `bitkit-android/journeys/hardware-wallet`. It was added - alongside the fix for the bug it covers. `SpendingAmountHw` observed `maxAllowed` without - `initial: true`, and since that value lives on the shared `TransferViewModel` and outlives the - screen, arriving with the limits already computed produced no change to observe. The freshly - created `AmountInputViewModel` then kept `maxAmountOverride` nil, leaving the pad bounded only by - the global 999 999 999 sat ceiling. Measured on an iPhone 17 simulator: nine taps on `N9` entered - 999 999 999 against an AVAILABLE of 610 015, with no toast and no snap. Continue stayed disabled, - so nothing unaffordable could be ordered — the gap was in the input, not in the order. - - Precondition: a paired Bridge emulator whose native-segwit account holds spendable regtest funds, - and a reachable Blocktank LSP so a real maximum can be quoted. Start on the wallet home screen. - - The screen is entered twice on purpose. A first entry that computes the limits from zero fires - the change either way, so it passes even with the bug; the second entry, where the limits are - already settled, is the one that regressed. - - - Tap the hardware wallet tile (id "ActivityHardware") beneath the SAVINGS and SPENDING tiles, and verify the hardware wallet detail screen opens (id "HardwareWalletScreen") - Tap the "Transfer To Spending" button (id "HardwareTransferToSpending") - If the first-run Transfer To Spending intro is shown, tap "Get Started" - Verify the transfer amount screen opens (id "HardwareTransferAmount") - Wait for the limits to load (id "N9", predicate enabled) — the number pad is held disabled behind the AVAILABLE spinner until the device balance and the LSP quote return - Verify the AVAILABLE amount (id "HardwareTransferAmountAvailable") shows a positive value - Tap the "9" key (id "N9") nine times to enter an amount far larger than the maximum allowed - Verify a "Spending Balance Maximum" warning toast appears (id "HardwareTransferAmountExceededToast") and names the maximum - Verify the amount in the input field snapped to the maximum and does not exceed the AVAILABLE amount — 999 999 999 here is the regression this journey exists for - Tap the delete key (id "NRemove") once, and verify the amount decreased - Tap back (id "NavigationBack") to return to the hardware wallet screen - Tap "Transfer To Spending" (id "HardwareTransferToSpending") again — this second entry arrives with the limits already computed, which is the case that regressed - Wait for the number pad (id "N9", predicate enabled), then tap the "9" key nine times again - Verify the toast appears again (id "HardwareTransferAmountExceededToast") and the amount snapped to the same maximum, not to 999 999 999 - - diff --git a/journeys/hardware-wallet/transfer-to-spending.xml b/journeys/hardware-wallet/transfer-to-spending.xml deleted file mode 100644 index c616e0676..000000000 --- a/journeys/hardware-wallet/transfer-to-spending.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - Drives the watch-only Transfer To Spending flow for a paired Trezor: Amount -> Sign With - Your Device -> Transaction Signed -> Processing Payment, then verifies the new transfer is - represented once in the hardware wallet's Core activity scope. Requires a paired Bridge - emulator whose native-segwit account holds spendable regtest funds and at least one older - hardware receive row with a distinct amount. - - - Launch the Bitkit app and go to the wallet home screen - Verify the recent activity list contains an older blue hardware received row with a distinct amount - Tap the hardware wallet tile (id "ActivityHardware") beneath the SAVINGS and SPENDING tiles, and verify the hardware wallet detail screen opens (id "HardwareWalletScreen") - Tap the "Transfer To Spending" button (id "HardwareTransferToSpending") - If the first-run Transfer To Spending intro is shown, tap "Get Started" - Verify the transfer amount screen opens (id "HardwareTransferAmount"), titled "TRANSFER TO SPENDING", showing an AVAILABLE row (id "HardwareTransferAmountAvailable"), the 25% and MAX quick buttons, and a number pad - Tap the "25%" quick button (id "HardwareTransferAmountQuarter") to set a valid amount below the available limit - Tap "Continue" (id "HardwareTransferAmountContinue") and wait for the fee estimate - Verify the sign screen opens (id "HardwareTransferSign"), titled "SIGN WITH YOUR DEVICE", showing the NETWORK FEES, SERVICE FEES, TO SPENDING and TOTAL cells, the Learn More and Advanced buttons, and the Trezor illustration - Tap "Open Trezor Connect" (id "HardwareTransferOpenTrezorConnect") to create the Blocktank order. If its fee is higher than the estimate, verify the fee cells refresh and tap "Open Trezor Connect" again; otherwise signing starts immediately - Approve the Recipient, Amount, Locktime, and Summary prompts on the Bridge emulator in order - Verify the transaction signed screen appears (id "HardwareTransferSigned"), titled "TRANSACTION SIGNED", showing the same fee cells and the checkmark illustration - Wait for the screen to auto-forward and verify the Processing Payment / setting-up progress screen appears - Tap "Continue Using Bitkit" and verify the app returns to the wallet home screen - Verify the recent activity list contains exactly one new blue hardware Transfer row with subtitle "From Savings", above the older blue received row - Tap the new blue Transfer row and verify its detail screen shows "TO SPENDING" - - diff --git a/journeys/notification-permission/README.md b/journeys/notification-permission/README.md index 6b7d18db1..72ecab3a7 100644 --- a/journeys/notification-permission/README.md +++ b/journeys/notification-permission/README.md @@ -1,49 +1,21 @@ # Notification-permission journeys -Verify that the background-setup toggle — shown on the Receive → CJIT confirm screen, the Receive → -CJIT liquidity screen and the Transfer → Spending confirm screen — drives the iOS notification -authorization prompt, and that the app's own notification settings own the route into the system -Settings app. - -**The label differs by screen.** The two Receive CJIT screens bind -`wallet__receive_background_setup_toggle` ("Enable background setup"); Transfer → Spending confirm -binds `lightning__spending_confirm__background_setup` ("Set up in background"), matching Android. -The switch identifiers are stable, so prefer those over the copy. - -## iOS vs Android - -Android requests `POST_NOTIFICATIONS` at the toggle and, once granted, re-tapping the toggle deep -links into system settings. iOS differs in two ways that these journeys are written around: - -- **The prompt is one-shot and system-owned.** `PushNotificationManager.requestPermission()` calls - `UNUserNotificationCenter.requestAuthorization`, which only shows the alert while the status is - `notDetermined`. Once the user has answered once, toggling on is silent. -- **Toggling off does not open system settings.** `MainNavView`'s `onChange(of:)` simply calls - `notificationManager.unregister()`. The route into the iOS Settings app is a dedicated button on - Settings → Notifications (id `NotificationsOpenSystemSettings`), which is what - `toggle-off-and-system-settings-route.xml` covers instead of Android's - `toggle-off-opens-system-settings.xml`. - -## Preconditions -- Onboarded dev wallet with the node connected to the LSP so a CJIT order can be quoted, and (for - the transfer journey) a positive on-chain Savings balance. -- **Notification authorization must be `notDetermined`** for the three "requests permission" - journeys. The alert is one-shot per install, so reset it by reinstalling the app: - `xcrun simctl uninstall to.bitkit` then `xcodebuildmcp simulator build-and-run`. - Re-onboarding the wallet is part of that reset. -- `settings.enableNotifications` must start **off** so the toggles read unchecked. - -## Gotcha: one setting, three toggles - -All three switches bind to the same `settings.enableNotifications`. Flipping one flips the other two, -so run these journeys one per app state — do not chain them expecting an unchecked toggle on the -second screen. - -## Identifiers used -- Receive CJIT: amount screen `ReceiveCjitAmount`, field `ReceiveCjitAmountNumberField`, - continue `ReceiveCjitAmountContinue`; confirm screen `ReceiveCjitConfirm` with switch - `ReceiveConfirmNotificationSwitch`; liquidity screen `ReceiveCjitLiquidity` with switch - `ReceiveLiquidityNotificationSwitch`. -- Transfer: `SpendingAmount`, `SpendingAmountAvailable`, `SpendingAmountContinue`, and the confirm - screen switch `SpendingConfirmNotificationSwitch`. -- Settings → Notifications: `NotificationsOpenSystemSettings`. +This suite is retired. Every journey in it reached the background-setup toggle through a Blocktank +channel purchase, and those were removed in synonymdev/bitkit-ios#1097: + +- `receive-cjit-confirm-notification-toggle.xml` and `toggle-off-and-system-settings-route.xml` used the + Receive → CJIT confirm screen (`ReceiveCjitConfirm`), reached through "Receive Lightning funds". +- `receive-cjit-liquidity-notification-toggle.xml` used the Receive → CJIT liquidity screen + (`ReceiveCjitLiquidity`). +- `transfer-spending-confirm-notification-toggle.xml` used the Transfer → Spending confirm screen + (`SpendingConfirmNotificationSwitch`), reached through "Transfer To Spending". + +No button in the UI navigates to those screens any more, so the toggle's permission behaviour (the +one-shot `UNUserNotificationCenter.requestAuthorization` alert on toggling on; toggling off only +unregisters push) is no longer reachable on a device through these routes. Settings › Notifications, +with its "Customize in iOS Bitkit Settings" button (id `NotificationsOpenSystemSettings`), is a +separate entry point that no journey covers yet. + +The reset for a fresh authorization state, should a journey for another entry point be written: +`xcrun simctl uninstall to.bitkit`, then `xcodebuildmcp simulator build-and-run` and +re-onboard the wallet. diff --git a/journeys/notification-permission/receive-cjit-confirm-notification-toggle.xml b/journeys/notification-permission/receive-cjit-confirm-notification-toggle.xml deleted file mode 100644 index 9c7919a8c..000000000 --- a/journeys/notification-permission/receive-cjit-confirm-notification-toggle.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - Verifies that the "Enable background setup" toggle on the Receive → CJIT confirm screen - (id "ReceiveCjitConfirm") launches the iOS notification authorization alert, and that - allowing it checks the toggle. - - Precondition: onboarded dev wallet with the node connected to the LSP so a CJIT order can be - quoted, notification authorization still notDetermined (reinstall first — the alert is one-shot), - and "Enable background setup" starting off. Start on the wallet home screen. - - - Tap the "Receive" button (id "Receive") on the home screen - Tap the "Spending" tab in the Receive sheet - Tap "Receive Lightning funds" - On the amount screen (id "ReceiveCjitAmount"), enter an amount above the CJIT minimum (e.g. 100 000 sats) using the number pad - Tap "Continue" (id "ReceiveCjitAmountContinue") and wait for the CJIT order to be created and the confirm screen (id "ReceiveCjitConfirm") to appear - Verify the "Enable background setup" toggle (id "ReceiveConfirmNotificationSwitch") is visible and off - Tap the "Enable background setup" toggle (id "ReceiveConfirmNotificationSwitch") - Verify the iOS notification authorization alert appears (text like "Bitkit" Would Like to Send You Notifications") - Tap "Allow" - Verify the "Enable background setup" toggle is now on - - diff --git a/journeys/notification-permission/receive-cjit-liquidity-notification-toggle.xml b/journeys/notification-permission/receive-cjit-liquidity-notification-toggle.xml deleted file mode 100644 index 448813937..000000000 --- a/journeys/notification-permission/receive-cjit-liquidity-notification-toggle.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - Verifies that the "Enable background setup" toggle on the Receive → CJIT liquidity screen - (id "ReceiveCjitLiquidity", reached via "Learn More" from the confirm screen) launches the - iOS notification authorization alert, and that allowing it checks the toggle. - - Precondition: onboarded dev wallet with the node connected to the LSP so a CJIT order can be - quoted, notification authorization still notDetermined (reinstall first — the alert is one-shot), - and "Enable background setup" starting off. Start on the wallet home screen. - - - Tap the "Receive" button (id "Receive") on the home screen - Tap the "Spending" tab in the Receive sheet - Tap "Receive Lightning funds" - On the amount screen (id "ReceiveCjitAmount"), enter an amount above the CJIT minimum (e.g. 100 000 sats) using the number pad - Tap "Continue" (id "ReceiveCjitAmountContinue") and wait for the confirm screen to appear - Tap "Learn More" to open the liquidity screen - Verify the liquidity screen (id "ReceiveCjitLiquidity") is visible with the lightning channel illustration and the "Enable background setup" toggle (id "ReceiveLiquidityNotificationSwitch") off - Tap the "Enable background setup" toggle (id "ReceiveLiquidityNotificationSwitch") - Verify the iOS notification authorization alert appears (text like "Bitkit" Would Like to Send You Notifications") - Tap "Allow" - Verify the "Enable background setup" toggle is now on - - diff --git a/journeys/notification-permission/toggle-off-and-system-settings-route.xml b/journeys/notification-permission/toggle-off-and-system-settings-route.xml deleted file mode 100644 index 0683e3dfd..000000000 --- a/journeys/notification-permission/toggle-off-and-system-settings-route.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - iOS counterpart of the Android "toggle off opens system settings" journey. On iOS the - "Enable background setup" toggle does NOT deep link anywhere: switching it off just unregisters - push and leaves the user on the same screen, with no alert (the authorization alert is one-shot - and already answered). The route into the iOS Settings app lives on Settings → Notifications - instead, behind the "Customize in iOS Bitkit Settings" button. - - Precondition: onboarded dev wallet with the node connected to the LSP so a CJIT order can be - quoted, notification authorization ALREADY GRANTED, and "Enable background setup" already on so - the toggle starts checked. Start on the wallet home screen. - - - Tap the "Receive" button (id "Receive") on the home screen - Tap the "Spending" tab in the Receive sheet - Tap "Receive Lightning funds" - On the amount screen (id "ReceiveCjitAmount"), enter an amount above the CJIT minimum (e.g. 100 000 sats) using the number pad - Tap "Continue" (id "ReceiveCjitAmountContinue") and wait for the confirm screen (id "ReceiveCjitConfirm") to appear - Verify the "Enable background setup" toggle (id "ReceiveConfirmNotificationSwitch") is visible and on - Tap the "Enable background setup" toggle (id "ReceiveConfirmNotificationSwitch") - Verify the toggle turns off, no permission alert appears, the app does not leave the foreground, and the confirm screen is still shown - Dismiss the Receive sheet and return to the wallet home screen - Open the menu, tap "Settings" (id "DrawerSettings"), then open "Notifications" - Verify the "Get paid when Bitkit is closed" row reflects the toggle being off - Tap the "Customize in iOS Bitkit Settings" button (id "NotificationsOpenSystemSettings") - Verify the iOS Settings app opens on the Bitkit entry, with no in-app navigation and no permission alert - Return to Bitkit and verify the Notifications settings screen is still shown - - diff --git a/journeys/notification-permission/transfer-spending-confirm-notification-toggle.xml b/journeys/notification-permission/transfer-spending-confirm-notification-toggle.xml deleted file mode 100644 index 86013c37b..000000000 --- a/journeys/notification-permission/transfer-spending-confirm-notification-toggle.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - Verifies that the "Set up in background" toggle on the Transfer → Spending confirm screen - launches the iOS notification authorization alert, and that allowing it checks the toggle. - - Precondition: onboarded dev wallet with a POSITIVE on-chain Savings balance and the node - connected to the LSP (so a real max can be quoted), notification authorization still - notDetermined (reinstall first — the alert is one-shot), and "Set up in background" starting - off. The spending max starts at 0 behind a spinner — wait for it to populate. Start on the - wallet home screen. Enter via the Savings card, not the Spending card — the Spending screen - only offers "Transfer From Savings" while the spending balance is zero. - - - Tap the Savings balance card on the home screen (id "ActivitySavings") - Tap "Transfer To Spending" (id "TransferToSpending") - If a transfer intro screen appears, tap "Get Started" - On the spending amount screen (id "SpendingAmount"), wait until the available/maximum amount (id "SpendingAmountAvailable") finishes loading and shows a positive value - Enter an amount within the maximum using the number pad - Tap Continue (id "SpendingAmountContinue") and wait for the spending confirm screen to appear - Verify the "Set up in background" toggle (id "SpendingConfirmNotificationSwitch") is visible and off - Tap the "Set up in background" toggle (id "SpendingConfirmNotificationSwitch") - Verify the iOS notification authorization alert appears (text like "Bitkit" Would Like to Send You Notifications") - Tap "Allow" - Verify the "Set up in background" toggle is now on - - diff --git a/journeys/receive/receive-onchain-only-without-channel.xml b/journeys/receive/receive-onchain-only-without-channel.xml new file mode 100644 index 000000000..212457b54 --- /dev/null +++ b/journeys/receive/receive-onchain-only-without-channel.xml @@ -0,0 +1,31 @@ + + + Verifies that a funded wallet with no Lightning channel can only receive on-chain: the Receive sheet + offers the Savings tab and nothing that buys Lightning liquidity, and Home shows neither the Spend + suggestion card nor the channels-closing banner. Blocktank channel purchases were removed in + synonymdev/bitkit-ios#1097: there is no "Receive Lightning funds" (CJIT) button or onboarding, + and the Spending and Auto tabs appear only when the wallet can create a Lightning invoice over a + ready channel with inbound liquidity. + + iOS: "Close the sheet" is a swipe down on the Receive sheet. The Spend card is asserted absent by + its identifier, which iOS suggestion cards expose as "Suggestion-<id>". + + Precondition: a freshly onboarded dev wallet on the regtest backend with NO Lightning + channel (open, pending or closed), USDT disabled and no hardware wallet paired, funded with an + on-chain deposit: ../bitkit-android/lsp POST /regtest/chain/deposit with the Savings address from + Receive, Show Details (id "ShowDetails"), then ../bitkit-android/lsp POST /regtest/chain/mine + '{"count":3}', and wait about 20 seconds for the balance and the activity row to appear (see + ../README.md, Backend preconditions). Start on the wallet home screen. + + + Verify the Savings balance card (id "ActivitySavings") shows the deposit and the Spending balance card (id "ActivitySpending") shows 0 + Verify the home screen has no Spend suggestion card (id "Suggestion-lightning") and no channels-closing banner (id "ChannelsClosingBanner"), scrolling the suggestions row if needed + Tap the Receive button (id "Receive") + Verify the Savings tab (id "Tab-savings") is selected and the QR code (id "QRCode") is visible + Verify there is no Spending tab (id "Tab-spending") and no Auto tab (id "Tab-auto") + Verify there is no "Receive Lightning funds" button anywhere on the sheet + Tap "Edit" (id "SpecifyInvoiceButton"), tap the amount field (id "ReceiveNumberPadTextField"), enter a small amount (for example "N1" then "N0" twice), tap Continue (id "ReceiveNumberPadSubmit") and then "QR Code" (id "ShowQrReceive") + Verify the sheet returns to the QR screen with no liquidity or channel confirm screen in between, the Savings tab is still selected and there is still no Spending tab (id "Tab-spending") + Close the sheet, open the Savings wallet (id "ActivitySavings") and verify no "Transfer To Spending" button (id "TransferToSpending") is shown, then open the Spending wallet (id "ActivitySpending") and verify no "Transfer from Savings" button (id "TransferFromSavings") and no notice (id "ChannelsClosingNotice") is shown + + diff --git a/journeys/receive/receive-tabs-wraparound.xml b/journeys/receive/receive-tabs-wraparound.xml index 9c4dcac6a..5fae8f811 100644 --- a/journeys/receive/receive-tabs-wraparound.xml +++ b/journeys/receive/receive-tabs-wraparound.xml @@ -6,12 +6,13 @@ the Receive sheet, also when it is released above the sheet, and a tap on a QR still copies it. Precondition: onboarded dev wallet with USDT enabled (introductory sheet or Settings, General, - Payments), no open Spending channel, no CJIT invoice pending and no hardware wallet paired, so the - tabs are Savings, Spending and USDT; a usable channel adds the Auto tab. Start on the wallet home + Payments), no Lightning channel and no hardware wallet paired, so the tabs are Savings and USDT. + Without a channel with inbound liquidity the Receive sheet has no Spending or Auto tab, since + Blocktank channel purchases were removed (synonymdev/bitkit-ios#1097). Start on the wallet home screen. The USDT address loads without funding; wait for the QR before swiping. Swipe with AXe across the middle of the QR, about 60% of its width, in 300 ms (`axe swipe - --start-x 80 --start-y 440 --end-x 330 --end-y 440 --duration 0.3 --udid ` to the right on a + --start-x 80 --start-y 440 --end-x 330 --end-y 440 --duration 0.3 --udid <udid>` to the right on a 402 pt wide simulator). iOS: the paged `TabView` holds an extra page before the first tab and after the last one; paging @@ -24,16 +25,11 @@ Swipe left across the USDT QR; verify the Savings tab is selected Start a screen recording, tap the USDT tab (id "Tab-usdt") and wait until the USDT QR is visible, then swipe left across the USDT QR; verify the Savings tab is selected and the recording goes straight from the USDT QR to the Savings QR, with no Auto or Spending QR and no second USDT screen in between Start a screen recording, tap the USDT tab and wait until the USDT QR is visible, then tap the Savings tab; verify the Savings tab is selected and the recording shows the same direct change from USDT to Savings - Swipe left across the Savings QR in 1200 ms (`axe swipe --start-x 330 --start-y 440 --end-x 80 --end-y 440 --duration 1.2`); verify the Spending tab (id "Tab-spending") is selected - Swipe left across the Spending content in 1200 ms; verify the USDT tab is selected and wait until its QR is visible + Verify there is no Spending tab (id "Tab-spending") and no Auto tab (id "Tab-auto") in the tab row + Swipe left across the Savings QR in 1200 ms (`axe swipe --start-x 330 --start-y 440 --end-x 80 --end-y 440 --duration 1.2`); verify the USDT tab is selected and wait until its QR is visible Swipe left from the top right of the USDT QR to above the sheet (`axe swipe --start-x 360 --start-y 300 --end-x 30 --end-y 60 --duration 0.3`); verify the Receive sheet is still open and the Savings tab is selected Swipe right from the top left of the Savings QR to above the sheet (`axe swipe --start-x 40 --start-y 300 --end-x 380 --end-y 60 --duration 0.3`); verify the Receive sheet is still open and the USDT tab is selected Swipe left across the USDT QR, then tap the Savings QR once; verify the "copied" tooltip appears - Close Receive, turn USDT off (Settings, General, Payments), return to the wallet home screen and tap Receive; verify the Savings tab is selected and only Savings and Spending show - Swipe right across the Savings QR; verify the Spending tab is selected - Swipe right across the Spending content; verify the Savings tab is selected - Swipe left across the Savings QR; verify the Spending tab is selected - Swipe left across the Spending content; verify the Savings tab is selected - Swipe left across the Savings QR; verify the Spending tab is selected + Close Receive, turn USDT off (Settings, General, Payments), return to the wallet home screen and tap Receive; verify the Savings tab is selected and there is no Spending tab (id "Tab-spending"), no Auto tab and no USDT tab (id "Tab-usdt") diff --git a/journeys/receive/usdt-receive-swipe-tabs.xml b/journeys/receive/usdt-receive-swipe-tabs.xml index 24480ede7..1d57da608 100644 --- a/journeys/receive/usdt-receive-swipe-tabs.xml +++ b/journeys/receive/usdt-receive-swipe-tabs.xml @@ -1,30 +1,31 @@ On the Receive sheet, swiping sideways over the QR changes tabs on every tab, USDT included. - With USDT selected, a swipe to the right over its QR selects the tab before it (Spending) and - does not copy the address; a swipe to the left wraps around to the first tab (Savings), since - USDT is the last tab. A single tap on the USDT QR still copies the address and shows the "copied" + With USDT selected, a swipe to the right over its QR selects the tab before it (Savings, since a + wallet without a Lightning channel has no Spending tab) and does not copy the address; a swipe to + the left wraps around to the first tab (Savings), since USDT is the last tab. A single tap on the USDT QR still copies the address and shows the "copied" tooltip. Precondition: onboarded dev wallet with USDT enabled (introductory sheet or Settings, General, - Payments), no CJIT invoice pending, start on the wallet home screen. The USDT address loads + Payments) and no Lightning channel, so the tabs are Savings and USDT (Blocktank channel purchases + were removed in synonymdev/bitkit-ios#1097, and the Spending tab appears only over a channel + with inbound liquidity). Start on the wallet home screen. The USDT address loads without funding; wait for the QR before swiping. The copy shows as a tooltip over the QR for about two seconds, so a check for "no copy" reads a screenshot taken right after the swipe. Swipe with AXe (`axe swipe --start-x 80 --start-y 420 - --end-x 320 --end-y 420 --duration 0.3 --udid ` on a 390 pt wide simulator) across the - middle of the QR, about 60% of its width. On a wallet without a Lightning channel, Spending shows - its setup view instead of a QR. + --end-x 320 --end-y 420 --duration 0.3 --udid <udid>` on a 390 pt wide simulator) across the + middle of the QR, about 60% of its width. - iOS: the return from Spending to USDT goes through the paged `TabView`, which moves on a 300 ms + iOS: the return from Savings to USDT goes through the paged `TabView`, which moves on a 300 ms swipe as well; the 600 ms in that step is kept so the two platforms run the same action. Tap Receive (id "Receive") Tap the USDT tab (id "Tab-usdt") and wait until the USDT QR is visible Swipe right across the QR and take a screenshot right away - Verify the Spending tab (id "Tab-spending") is selected, the Lightning receive content or the Spending setup is shown, and the screenshot shows no "copied" tooltip - Swipe left across the middle of the Spending content, whether it shows a Lightning QR or the Spending setup, in 600 ms (`axe swipe --start-x 320 --start-y 420 --end-x 80 --end-y 420 --duration 0.6`); verify the USDT tab is selected and wait until its QR is visible + Verify the Savings tab (id "Tab-savings") is selected, there is no Spending tab (id "Tab-spending"), and the screenshot shows no "copied" tooltip + Swipe left across the middle of the Savings QR in 600 ms (`axe swipe --start-x 320 --start-y 420 --end-x 80 --end-y 420 --duration 0.6`); verify the USDT tab is selected and wait until its QR is visible Tap the USDT QR once and verify the "copied" tooltip appears Wait until the tooltip is gone, swipe left across the USDT QR and take a screenshot right away; verify the Savings tab (id "Tab-savings") is selected and the screenshot shows no "copied" tooltip diff --git a/journeys/transfer/spending-confirm-amount-change.xml b/journeys/transfer/spending-confirm-amount-change.xml deleted file mode 100644 index 90728ae9a..000000000 --- a/journeys/transfer/spending-confirm-amount-change.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - Verifies that a transfer to spending never pays more than the total Spending Confirm shows - (synonymdev/bitkit-ios#783). After the swipe creates the order, the funding is rebuilt for it. A - plan that costs the same or less than the screen pays on that swipe. A plan that costs more pays - nothing: the screen shows the new amounts, a "Fees changed" toast explains the increase, and the - next swipe pays them. - - This journey covers the cheaper plan. Spending Confirm sizes the network fee for two inputs, then - a larger UTXO confirms before the swipe, so the plan rebuilt at swipe spends one input, costs less - and pays at once. The higher-total path (the "Fees changed" toast) is covered by unit tests and a - manual test, because spending the wallet's own UTXO outside the app is not a listed capability. - - iOS differs from Android in two steps: there is no app-log step, and closing the received sheet - re-sizes Spending Confirm, so the lower amounts can already be on screen before the swipe. The - swipe pays at once either way. - - Precondition: a freshly onboarded Debug build on the staging regtest backend (a plain Debug build, - or an E2E_BUILD with E2E_BACKEND=network; the default E2E_BUILD targets the local backend, which - cannot open a Blocktank channel) with no channel and exactly two - confirmed on-chain UTXOs of 150 000 sats each (two ../bitkit-android/lsp deposits to the - wallet's savings address, each followed by a mine). Start on the wallet home screen. - - - Tap Receive (id "Receive"), tap the "Savings" receive tab if one is shown, tap "Show Details" (id "ShowDetails"), read the address from id "ReceiveOnchainAddress", then close the sheet to return to the home screen - Tap the Savings balance card (id "ActivitySavings"), then "Transfer To Spending" (id "TransferToSpending") - If the spending intro appears, tap "Get Started" (id "SpendingIntroContinue") - Enter 200 000 on the number pad (ids "N2", "N0", "N0", "N0", "N0", "N0") and tap "Continue" (id "SpendingAmountContinue") - Verify the confirm screen is visible (id "SpendingConfirmMore") and wait until its network fee is shown - Note the amounts under "Network Fees" and "Total" - Run in one command: ../bitkit-android/lsp POST /regtest/chain/deposit '{"address":"<savings addr>","amountSat":500000}' && ../bitkit-android/lsp POST /regtest/chain/mine '{"count":1}' - Wait 30s for the wallet to sync the new UTXO - If the received sheet (id "ReceivedTransaction") is visible, tap its button (id "ReceivedTransactionButton") - Verify the confirm screen is visible; if the received sheet was shown, "Network Fees" and "Total" may already show lower amounts than noted - Swipe the "Swipe To Transfer" handle (id "GRAB") fully to the right - Wait up to 90s for the setting up screen (id "LightningSettingUp") - - diff --git a/journeys/usdt-wallet-navigation.xml b/journeys/usdt-wallet-navigation.xml index 133232997..f21f367b0 100644 --- a/journeys/usdt-wallet-navigation.xml +++ b/journeys/usdt-wallet-navigation.xml @@ -8,7 +8,7 @@ Open Receive. Verify the network is Arbitrum One. Open details, verify the address starts with 0x, and verify Details Copy contains only the address. Return to the QR and verify Copy and tapping the QR both copy only the same address, while the QR still contains the full USDT payment URI including the network and token. On Android, system Back from Details returns to the USDT QR; Back from the QR closes Receive without switching to Savings. Close Receive, open Send and manual entry. Enter an invalid Bitcoin address and verify the address error prevents continuing. Replace it with an owned valid EVM address. Continue to Amount, enter six decimal places and try a seventh; verify the amount remains limited to six. Close before requesting a quote. - Set an amount and note in Bitcoin Receive, then close it. Open a fresh USDT Receive sheet and switch to Savings and Spending. Verify Savings shows a fresh Bitcoin QR with no previous amount or note. Spending shows a fresh Lightning QR when receive liquidity is available; an unfunded wallet shows the Spending receive setup instead. Verify hardware-wallet receive remains available when paired. + Set an amount and note in Bitcoin Receive, then close it. Open a fresh USDT Receive sheet and switch to Savings and, when the wallet has a Lightning channel with inbound liquidity, Spending. Verify Savings shows a fresh Bitcoin QR with no previous amount or note. When the Spending tab (id "Tab-spending") is present it shows a fresh Lightning QR; a wallet without such a channel has no Spending tab. Verify hardware-wallet receive remains available when paired. Return Home and open Bitcoin Receive. Verify the Bitcoin address, network and amount controls remain separate from USDT. With saved USDT activity, open All Activity. Search for USDT and usdt and verify both show USDT rows. Search for u, us or t and verify a USDT row appears only if its recipient or transaction ID contains the query. Verify a matching recipient or transaction-ID fragment still finds the row, and clearing search restores the list. From 6d92437a28b4d5e9e5406714afb2c8f5c3dfec42 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Thu, 22 Oct 2026 12:45:49 +0200 Subject: [PATCH 03/12] feat: show channels unavailable sheet for transfers to spending Savings, Spending and hardware wallet Transfer To Spending buttons stay and open a sheet saying new channels are no longer offered, with the close date for wallets with Blocktank channels. --- Bitkit/MainNavView.swift | 8 +++ .../Localization/ar.lproj/Localizable.strings | 3 ++ .../Localization/ca.lproj/Localizable.strings | 3 ++ .../Localization/cs.lproj/Localizable.strings | 3 ++ .../Localization/de.lproj/Localizable.strings | 3 ++ .../Localization/el.lproj/Localizable.strings | 3 ++ .../Localization/en.lproj/Localizable.strings | 3 ++ .../es-419.lproj/Localizable.strings | 3 ++ .../Localization/es.lproj/Localizable.strings | 3 ++ .../Localization/fr.lproj/Localizable.strings | 3 ++ .../Localization/it.lproj/Localizable.strings | 3 ++ .../Localization/nl.lproj/Localizable.strings | 3 ++ .../Localization/pl.lproj/Localizable.strings | 3 ++ .../pt-BR.lproj/Localizable.strings | 3 ++ .../Localization/pt.lproj/Localizable.strings | 3 ++ .../Localization/ru.lproj/Localizable.strings | 3 ++ Bitkit/ViewModels/SheetViewModel.swift | 13 +++++ .../Views/Wallets/HardwareWalletScreen.swift | 30 ++++++++++- .../Views/Wallets/SavingsWalletScreen.swift | 22 ++++++++ .../Sheets/ChannelsUnavailableSheet.swift | 51 +++++++++++++++++++ .../Views/Wallets/SpendingWalletScreen.swift | 22 ++++++++ BitkitTests/PubkyAuthURLSchemeTests.swift | 7 --- journeys/README.md | 4 +- journeys/amount-limits/README.md | 6 +-- journeys/channels-closing/README.md | 16 +++--- .../channels-closing-notice.xml | 11 ++-- journeys/hardware-wallet/README.md | 8 +-- journeys/hardware-wallet/detail-overview.xml | 6 +-- .../receive-onchain-only-without-channel.xml | 4 +- 29 files changed, 222 insertions(+), 31 deletions(-) create mode 100644 Bitkit/Views/Wallets/Sheets/ChannelsUnavailableSheet.swift diff --git a/Bitkit/MainNavView.swift b/Bitkit/MainNavView.swift index 569c1b64f..e6b410d39 100644 --- a/Bitkit/MainNavView.swift +++ b/Bitkit/MainNavView.swift @@ -115,6 +115,14 @@ struct MainNavView: View { ) { config in BackupSheet(config: config) } + .sheet( + item: $sheets.channelsUnavailableSheetItem, + onDismiss: { + sheets.hideSheetIfActive(.channelsUnavailable, reason: "Channels unavailable sheet dismissed") + } + ) { + config in ChannelsUnavailableSheet(config: config) + } .sheet( item: $sheets.connectionClosedSheetItem, onDismiss: { diff --git a/Bitkit/Resources/Localization/ar.lproj/Localizable.strings b/Bitkit/Resources/Localization/ar.lproj/Localizable.strings index fce3b58b4..daf2f220a 100644 --- a/Bitkit/Resources/Localization/ar.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/ar.lproj/Localizable.strings @@ -201,6 +201,7 @@ "lightning__transfer_hw__sign_title" = "وقّع باستخدام\nجهازك"; "lightning__transfer_hw__signed_title" = "تم توقيع\nالمعاملة"; "lightning__transfer_to_savings_button" = "تحويل إلى المدخرات"; +"lightning__transfer_to_spending_button" = "تحويل إلى الإنفاق"; "lightning__spending_intro__title" = "تحويل\nإلى الإنفاق"; "lightning__spending_intro__text" = "حوّل الأموال إلى رصيد إنفاقك للاستمتاع بمعاملات فورية ورخيصة مع الأصدقاء والعائلة والتجار."; "lightning__spending_intro__button" = "ابدأ"; @@ -318,6 +319,8 @@ "lightning__channel_point" = "نقطة القناة"; "lightning__channels_closing_banner" = "تُغلق القنوات في {date}"; "lightning__channels_closing_notice" = "ستُغلق قنوات Lightning الخاصة بك في {date}. انقل رصيد الإنفاق إلى المدخرات قبل ذلك."; +"lightning__channels_unavailable__text" = "لم يعد Bitkit يوفّر قنوات Lightning جديدة، لذا لا يمكنك نقل الأموال من المدخرات إلى الإنفاق."; +"lightning__channels_unavailable__title" = "القنوات الجديدة غير متاحة"; "lightning__closure_reason" = "سبب الإغلاق"; "lightning__support" = "الدعم"; "lightning__connection_closed__title" = "تم إغلاق الاتصال"; diff --git a/Bitkit/Resources/Localization/ca.lproj/Localizable.strings b/Bitkit/Resources/Localization/ca.lproj/Localizable.strings index 61820c0d0..e8e6bf3c3 100644 --- a/Bitkit/Resources/Localization/ca.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/ca.lproj/Localizable.strings @@ -227,6 +227,7 @@ "lightning__transfer_hw__sign_title" = "Signa amb\nel teu dispositiu"; "lightning__transfer_hw__signed_title" = "Transacció\nsignada"; "lightning__transfer_to_savings_button" = "Transferir a estalvis"; +"lightning__transfer_to_spending_button" = "Transferir a despesa"; "lightning__spending_intro__title" = "Transferir\na despesa"; "lightning__spending_intro__text" = "Transfereix fons al teu saldo de despesa per gaudir de transaccions instantànies i barates amb amics, família i comerciants."; "lightning__spending_intro__button" = "Començar"; @@ -355,6 +356,8 @@ "lightning__channel_point" = "Punt del canal"; "lightning__channels_closing_banner" = "ELS CANALS ES TANQUEN EL {date}"; "lightning__channels_closing_notice" = "Els teus canals Lightning es tancaran el {date}. Transfereix el teu saldo de despesa a estalvis abans d'aquesta data."; +"lightning__channels_unavailable__text" = "Bitkit ja no ofereix canals Lightning nous, així que no pots transferir fons d'estalvis a despesa."; +"lightning__channels_unavailable__title" = "Canals nous no disponibles"; "lightning__closure_reason" = "Motiu del tancament"; "lightning__support" = "Suport"; "lightning__connection_closed__title" = "Connexió tancada"; diff --git a/Bitkit/Resources/Localization/cs.lproj/Localizable.strings b/Bitkit/Resources/Localization/cs.lproj/Localizable.strings index ad14061a9..ebed14314 100644 --- a/Bitkit/Resources/Localization/cs.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/cs.lproj/Localizable.strings @@ -228,6 +228,7 @@ "lightning__transfer_hw__sign_title" = "Podepište\nsvým zařízením"; "lightning__transfer_hw__signed_title" = "Transakce\npodepsána"; "lightning__transfer_to_savings_button" = "Převést do úspor"; +"lightning__transfer_to_spending_button" = "Převést do útraty"; "lightning__spending_intro__title" = "Převést\ndo výdajů"; "lightning__spending_intro__text" = "Převeďte peníze na svůj disponibilní zůstatek a využívejte okamžité a levné transakce s přáteli, rodinou a obchodníky."; "lightning__spending_intro__button" = "Začít"; @@ -371,6 +372,8 @@ "lightning__channel_point" = "Bod kanálu"; "lightning__channels_closing_banner" = "KANÁLY SE ZAVŘOU {date}"; "lightning__channels_closing_notice" = "Vaše Lightning kanály budou uzavřeny {date}. Do té doby převeďte zůstatek útrat do úspor."; +"lightning__channels_unavailable__text" = "Bitkit už nenabízí nové Lightning kanály, takže nemůžete převádět prostředky z úspor do útrat."; +"lightning__channels_unavailable__title" = "Nové kanály nejsou dostupné"; "lightning__closure_reason" = "Důvod uzavření"; "lightning__support" = "Podpora"; "lightning__connection_closed__title" = "Připojení uzavřeno"; diff --git a/Bitkit/Resources/Localization/de.lproj/Localizable.strings b/Bitkit/Resources/Localization/de.lproj/Localizable.strings index a2dc6cc8a..a82ee34df 100644 --- a/Bitkit/Resources/Localization/de.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/de.lproj/Localizable.strings @@ -228,6 +228,7 @@ "lightning__transfer_hw__sign_title" = "Mit deinem\nGerät signieren"; "lightning__transfer_hw__signed_title" = "Transaktion\nsigniert"; "lightning__transfer_to_savings_button" = "Zu Ersparnissen transferieren"; +"lightning__transfer_to_spending_button" = "Zu Ausgaben transferieren"; "lightning__spending_intro__title" = "zu den Ausgaben"; "lightning__spending_intro__text" = "Überweisen Sie Geld auf Ihr Guthaben, um sofortige und günstige Transaktionen mit Freunden, Familie und Händlern zu tätigen."; "lightning__spending_intro__button" = "Los geht\'s"; @@ -369,6 +370,8 @@ "lightning__channel_point" = "Kanalpunkt"; "lightning__channels_closing_banner" = "KANÄLE SCHLIESSEN AM {date}"; "lightning__channels_closing_notice" = "Deine Lightning-Kanäle werden am {date} geschlossen. Übertrage dein Guthaben vorher auf das Sparkonto."; +"lightning__channels_unavailable__text" = "Bitkit bietet keine neuen Lightning-Kanäle mehr an, daher kannst du kein Guthaben vom Sparkonto auf das Ausgabenkonto übertragen."; +"lightning__channels_unavailable__title" = "Neue Kanäle nicht verfügbar"; "lightning__closure_reason" = "Grund für die Schließung"; "lightning__support" = "Support"; "lightning__connection_closed__title" = "Verbindung geschlossen"; diff --git a/Bitkit/Resources/Localization/el.lproj/Localizable.strings b/Bitkit/Resources/Localization/el.lproj/Localizable.strings index fd0361605..7f28d4ea0 100644 --- a/Bitkit/Resources/Localization/el.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/el.lproj/Localizable.strings @@ -223,6 +223,7 @@ "lightning__transfer_hw__sign_title" = "Υπόγραψε με\nτη συσκευή σου"; "lightning__transfer_hw__signed_title" = "Η συναλλαγή\nυπογράφηκε"; "lightning__transfer_to_savings_button" = "Μεταφορά στις Αποταμιεύσεις"; +"lightning__transfer_to_spending_button" = "Μεταφορά στις Δαπάνες"; "lightning__spending_intro__title" = "Μεταφορά\nστις δαπάνες"; "lightning__spending_intro__text" = "Μεταφέρετε κεφάλαια στο υπόλοιπο δαπανών σας για να απολαύσετε άμεσες και φθηνές συναλλαγές με φίλους, οικογένεια και εμπόρους."; "lightning__spending_intro__button" = "Ξεκινήστε"; @@ -342,6 +343,8 @@ "lightning__channel_point" = "Channel point"; "lightning__channels_closing_banner" = "ΤΑ ΚΑΝΑΛΙΑ ΚΛΕΙΝΟΥΝ ΣΤΙΣ {date}"; "lightning__channels_closing_notice" = "Τα κανάλια Lightning σας θα κλείσουν στις {date}. Μεταφέρετε το υπόλοιπο δαπανών σε αποταμιεύσεις πριν από τότε."; +"lightning__channels_unavailable__text" = "Το Bitkit δεν προσφέρει πλέον νέα κανάλια Lightning, οπότε δεν μπορείτε να μεταφέρετε κεφάλαια από τις αποταμιεύσεις στις δαπάνες."; +"lightning__channels_unavailable__title" = "Νέα κανάλια μη διαθέσιμα"; "lightning__closure_reason" = "Λόγος κλεισίματος"; "lightning__support" = "Υποστήριξη"; "lightning__connection_closed__title" = "Η Σύνδεση Έκλεισε"; diff --git a/Bitkit/Resources/Localization/en.lproj/Localizable.strings b/Bitkit/Resources/Localization/en.lproj/Localizable.strings index 56a9391e2..ba1eb2b97 100644 --- a/Bitkit/Resources/Localization/en.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/en.lproj/Localizable.strings @@ -221,6 +221,7 @@ "lightning__transfer_hw__sign_title" = "Sign with\nyour device"; "lightning__transfer_hw__signed_title" = "Transaction\nsigned"; "lightning__transfer_to_savings_button" = "Transfer To Savings"; +"lightning__transfer_to_spending_button" = "Transfer To Spending"; "lightning__spending_intro__title" = "Transfer\nto spending"; "lightning__spending_intro__text" = "Transfer funds to your spending balance to enjoy instant and cheap transactions with friends, family, and merchants."; "lightning__spending_intro__button" = "Get Started"; @@ -364,6 +365,8 @@ "lightning__channel_point" = "Channel point"; "lightning__channels_closing_banner" = "CHANNELS CLOSE ON {date}"; "lightning__channels_closing_notice" = "Your Lightning channels will be closed on {date}. Move your spending balance to savings before then."; +"lightning__channels_unavailable__text" = "Bitkit no longer offers new Lightning channels, so you can't move funds from savings to spending."; +"lightning__channels_unavailable__title" = "New Channels Unavailable"; "lightning__closure_reason" = "Closure reason"; "lightning__support" = "Support"; "lightning__connection_closed__title" = "Connection Closed"; diff --git a/Bitkit/Resources/Localization/es-419.lproj/Localizable.strings b/Bitkit/Resources/Localization/es-419.lproj/Localizable.strings index ef4c4e819..87308a4bf 100644 --- a/Bitkit/Resources/Localization/es-419.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/es-419.lproj/Localizable.strings @@ -229,6 +229,7 @@ "lightning__transfer_hw__sign_title" = "Firme con\nsu dispositivo"; "lightning__transfer_hw__signed_title" = "Transacción\nfirmada"; "lightning__transfer_to_savings_button" = "Transferir a ahorros"; +"lightning__transfer_to_spending_button" = "Transferir a gastos"; "lightning__spending_intro__title" = "Transferir\na gastos"; "lightning__spending_intro__text" = "Transfiera fondos a su saldo de gastos para disfrutar de transacciones baratas e instantáneas con amigos y comercios."; "lightning__spending_intro__button" = "Empezar"; @@ -372,6 +373,8 @@ "lightning__channel_point" = "Punto del canal"; "lightning__channels_closing_banner" = "LOS CANALES CIERRAN EL {date}"; "lightning__channels_closing_notice" = "Tus canales Lightning se cerrarán el {date}. Transfiere tu saldo de gasto a ahorros antes de esa fecha."; +"lightning__channels_unavailable__text" = "Bitkit ya no ofrece canales Lightning nuevos, así que no puedes transferir fondos de ahorros a gasto."; +"lightning__channels_unavailable__title" = "Canales nuevos no disponibles"; "lightning__closure_reason" = "Motivo del cierre"; "lightning__support" = "Soporte"; "lightning__connection_closed__title" = "Conexión cerrada"; diff --git a/Bitkit/Resources/Localization/es.lproj/Localizable.strings b/Bitkit/Resources/Localization/es.lproj/Localizable.strings index 5ddb7d2b7..132752d1e 100644 --- a/Bitkit/Resources/Localization/es.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/es.lproj/Localizable.strings @@ -225,6 +225,7 @@ "lightning__transfer_hw__sign_title" = "Firme con\nsu dispositivo"; "lightning__transfer_hw__signed_title" = "Transacción\nfirmada"; "lightning__transfer_to_savings_button" = "Transferir a Ahorros"; +"lightning__transfer_to_spending_button" = "Transferir a Gasto"; "lightning__spending_intro__title" = "Transferir\na gasto"; "lightning__spending_intro__text" = "Transfiera fondos a su saldo de gasto para disfrutar de transacciones baratas e instantáneas con amigos, familia y comerciantes."; "lightning__spending_intro__button" = "Empezar"; @@ -356,6 +357,8 @@ "lightning__channel_point" = "Punto del canal"; "lightning__channels_closing_banner" = "LOS CANALES CIERRAN EL {date}"; "lightning__channels_closing_notice" = "Tus canales Lightning se cerrarán el {date}. Transfiere tu saldo de gasto a ahorros antes de esa fecha."; +"lightning__channels_unavailable__text" = "Bitkit ya no ofrece canales Lightning nuevos, así que no puedes transferir fondos de ahorro a gasto."; +"lightning__channels_unavailable__title" = "Canales nuevos no disponibles"; "lightning__closure_reason" = "Motivo del cierre"; "lightning__support" = "Soporte"; "lightning__connection_closed__title" = "Conexión Cerrada"; diff --git a/Bitkit/Resources/Localization/fr.lproj/Localizable.strings b/Bitkit/Resources/Localization/fr.lproj/Localizable.strings index caba4cfa6..aea09ed56 100644 --- a/Bitkit/Resources/Localization/fr.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/fr.lproj/Localizable.strings @@ -229,6 +229,7 @@ "lightning__transfer_hw__sign_title" = "Signez avec\nvotre appareil"; "lightning__transfer_hw__signed_title" = "Transaction\nsignée"; "lightning__transfer_to_savings_button" = "Transférer vers l\'épargne"; +"lightning__transfer_to_spending_button" = "Transférer vers les dépenses"; "lightning__spending_intro__title" = "Transfert\naux dépenses"; "lightning__spending_intro__text" = "Transférez des fonds vers votre solde Dépenses pour profiter de transactions instantanées et moins chers avec vos amis, votre famille et les commerçants."; "lightning__spending_intro__button" = "Commencer"; @@ -372,6 +373,8 @@ "lightning__channel_point" = "Point du canal"; "lightning__channels_closing_banner" = "LES CANAUX FERMENT LE {date}"; "lightning__channels_closing_notice" = "Vos canaux Lightning seront fermés le {date}. Transférez votre solde de dépenses vers l'Épargne avant cette date."; +"lightning__channels_unavailable__text" = "Bitkit ne propose plus de nouveaux canaux Lightning, vous ne pouvez donc pas transférer de fonds de l'Épargne vers les Dépenses."; +"lightning__channels_unavailable__title" = "Nouveaux canaux indisponibles"; "lightning__closure_reason" = "Motif de la fermeture"; "lightning__support" = "Support"; "lightning__connection_closed__title" = "Connexion fermée"; diff --git a/Bitkit/Resources/Localization/it.lproj/Localizable.strings b/Bitkit/Resources/Localization/it.lproj/Localizable.strings index 9d929f19e..def94cf9b 100644 --- a/Bitkit/Resources/Localization/it.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/it.lproj/Localizable.strings @@ -226,6 +226,7 @@ "lightning__transfer_hw__sign_title" = "Firma con\nil tuo dispositivo"; "lightning__transfer_hw__signed_title" = "Transazione\nfirmata"; "lightning__transfer_to_savings_button" = "Trasferisci Ai Risparmi"; +"lightning__transfer_to_spending_button" = "Trasferisci A Spesa"; "lightning__spending_intro__title" = "Trasferisci\na spesa"; "lightning__spending_intro__text" = "Trasferisci fondi al tuo conto di spesa per usufruire di transazioni istantanee ed economiche con amici, familiari e commercianti."; "lightning__spending_intro__button" = "Inizia"; @@ -365,6 +366,8 @@ "lightning__channel_point" = "Punto canale"; "lightning__channels_closing_banner" = "I CANALI CHIUDONO IL {date}"; "lightning__channels_closing_notice" = "I tuoi canali Lightning verranno chiusi il {date}. Trasferisci il saldo di spesa ai risparmi prima di allora."; +"lightning__channels_unavailable__text" = "Bitkit non offre più nuovi canali Lightning, quindi non puoi trasferire fondi dai risparmi alla spesa."; +"lightning__channels_unavailable__title" = "Nuovi canali non disponibili"; "lightning__closure_reason" = "Motivo della chiusura"; "lightning__support" = "Supporto"; "lightning__connection_closed__title" = "Connessione chiusa"; diff --git a/Bitkit/Resources/Localization/nl.lproj/Localizable.strings b/Bitkit/Resources/Localization/nl.lproj/Localizable.strings index 205b57d0e..bbfdec24c 100644 --- a/Bitkit/Resources/Localization/nl.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/nl.lproj/Localizable.strings @@ -228,6 +228,7 @@ "lightning__transfer_hw__sign_title" = "Onderteken met\nuw apparaat"; "lightning__transfer_hw__signed_title" = "Transactie\nondertekend"; "lightning__transfer_to_savings_button" = "Overboeken Naar Spaargeld"; +"lightning__transfer_to_spending_button" = "Overboeken Naar Bestedingssaldo"; "lightning__spending_intro__title" = "Overboeking\nnaar bestedingssaldo"; "lightning__spending_intro__text" = "Boek geld over naar uw bestedingssaldo om te genieten van directe en goedkope transacties met vrienden, familie en winkeliers."; "lightning__spending_intro__button" = "Starten"; @@ -369,6 +370,8 @@ "lightning__channel_point" = "Kanaalpunt"; "lightning__channels_closing_banner" = "KANALEN SLUITEN OP {date}"; "lightning__channels_closing_notice" = "Je Lightning-kanalen worden op {date} gesloten. Boek je bestedingssaldo vóór die datum over naar spaargeld."; +"lightning__channels_unavailable__text" = "Bitkit biedt geen nieuwe Lightning-kanalen meer aan, dus je kunt geen geld van spaargeld naar je bestedingssaldo overboeken."; +"lightning__channels_unavailable__title" = "Nieuwe kanalen niet beschikbaar"; "lightning__closure_reason" = "Reden van sluiting"; "lightning__support" = "Ondersteuning"; "lightning__connection_closed__title" = "Verbinding Gesloten"; diff --git a/Bitkit/Resources/Localization/pl.lproj/Localizable.strings b/Bitkit/Resources/Localization/pl.lproj/Localizable.strings index 6e90fa1d5..9e3375e5b 100644 --- a/Bitkit/Resources/Localization/pl.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/pl.lproj/Localizable.strings @@ -228,6 +228,7 @@ "lightning__transfer_hw__sign_title" = "Podpisz\nurządzeniem"; "lightning__transfer_hw__signed_title" = "Transakcja\npodpisana"; "lightning__transfer_to_savings_button" = "Prześlij na oszczędności"; +"lightning__transfer_to_spending_button" = "Prześlij na wydatki"; "lightning__spending_intro__title" = "Prześlij \nna saldo wydatków"; "lightning__spending_intro__text" = "Przelewaj środki na swoje saldo wydatków, aby cieszyć się natychmiastowymi i tanimi transakcjami z przyjaciółmi, rodziną i sprzedawcami."; "lightning__spending_intro__button" = "Rozpocznij"; @@ -371,6 +372,8 @@ "lightning__channel_point" = "Punkt kanału"; "lightning__channels_closing_banner" = "KANAŁY ZOSTANĄ ZAMKNIĘTE {date}"; "lightning__channels_closing_notice" = "Twoje kanały Lightning zostaną zamknięte {date}. Przed tym terminem prześlij saldo wydatków na oszczędności."; +"lightning__channels_unavailable__text" = "Bitkit nie oferuje już nowych kanałów Lightning, więc nie możesz przesyłać środków z oszczędności na wydatki."; +"lightning__channels_unavailable__title" = "Nowe kanały niedostępne"; "lightning__closure_reason" = "Powód zamknięcia"; "lightning__support" = "Wsparcie"; "lightning__connection_closed__title" = "Połączenie zamknięte"; diff --git a/Bitkit/Resources/Localization/pt-BR.lproj/Localizable.strings b/Bitkit/Resources/Localization/pt-BR.lproj/Localizable.strings index 2f6b4329d..15832cb0a 100644 --- a/Bitkit/Resources/Localization/pt-BR.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/pt-BR.lproj/Localizable.strings @@ -228,6 +228,7 @@ "lightning__transfer_hw__sign_title" = "Assine com\nseu dispositivo"; "lightning__transfer_hw__signed_title" = "Transação\nassinada"; "lightning__transfer_to_savings_button" = "Transferir para poupança"; +"lightning__transfer_to_spending_button" = "Transferir para gastos"; "lightning__spending_intro__title" = "Transferir\npara gastos"; "lightning__spending_intro__text" = "Transfira fundos para seu saldo de gastos para desfrutar de transações instantâneas e baratas com amigos, familiares e comerciantes."; "lightning__spending_intro__button" = "Começar"; @@ -371,6 +372,8 @@ "lightning__channel_point" = "Ponto do canal"; "lightning__channels_closing_banner" = "CANAIS FECHAM EM {date}"; "lightning__channels_closing_notice" = "Seus canais Lightning serão fechados em {date}. Transfira seu saldo de gastos para a poupança antes dessa data."; +"lightning__channels_unavailable__text" = "O Bitkit não oferece mais novos canais Lightning, então você não pode transferir fundos da poupança para gastos."; +"lightning__channels_unavailable__title" = "Novos canais indisponíveis"; "lightning__closure_reason" = "Motivo do fechamento"; "lightning__support" = "Suporte"; "lightning__connection_closed__title" = "Conexão Desativada"; diff --git a/Bitkit/Resources/Localization/pt.lproj/Localizable.strings b/Bitkit/Resources/Localization/pt.lproj/Localizable.strings index eb935e796..53f9ce88f 100644 --- a/Bitkit/Resources/Localization/pt.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/pt.lproj/Localizable.strings @@ -254,6 +254,7 @@ "lightning__transfer_hw__sign_title" = "Assine com\no seu dispositivo"; "lightning__transfer_hw__signed_title" = "Transação\nassinada"; "lightning__transfer_to_savings_button" = "Transferir Para Poupança"; +"lightning__transfer_to_spending_button" = "Transferir Para Gastos"; "lightning__spending_intro__title" = "Transferir\npara gastos"; "lightning__spending_intro__text" = "Transfira fundos para seu saldo de gastos para desfrutar de transações instantâneas e baratas com amigos, familiares e comerciantes."; "lightning__spending_intro__button" = "Começar"; @@ -371,6 +372,8 @@ "lightning__channel_point" = "Ponto do canal"; "lightning__channels_closing_banner" = "CANAIS FECHAM A {date}"; "lightning__channels_closing_notice" = "Os seus canais Lightning serão fechados a {date}. Transfira o seu saldo de gastos para a poupança antes dessa data."; +"lightning__channels_unavailable__text" = "O Bitkit já não oferece novos canais Lightning, por isso não pode transferir fundos da poupança para gastos."; +"lightning__channels_unavailable__title" = "Novos canais indisponíveis"; "lightning__closure_reason" = "Motivo do fechamento"; "lightning__support" = "Suporte"; "lightning__connection_closed__title" = "Ligação fechada"; diff --git a/Bitkit/Resources/Localization/ru.lproj/Localizable.strings b/Bitkit/Resources/Localization/ru.lproj/Localizable.strings index 0f5d3e7ec..0ae6858cd 100644 --- a/Bitkit/Resources/Localization/ru.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/ru.lproj/Localizable.strings @@ -227,6 +227,7 @@ "lightning__transfer_hw__sign_title" = "Подпишите на\nустройстве"; "lightning__transfer_hw__signed_title" = "Транзакция\nподписана"; "lightning__transfer_to_savings_button" = "Перевести в Сбережения"; +"lightning__transfer_to_spending_button" = "Перевести на Расходы"; "lightning__spending_intro__title" = "Перевод\nна Расходы"; "lightning__spending_intro__text" = "Переведите средства на свой баланс расходов, чтобы наслаждаться мгновенными и недорогими транзакциями с друзьями, семьёй и продавцами."; "lightning__spending_intro__button" = "Начать"; @@ -366,6 +367,8 @@ "lightning__channel_point" = "Точка Канала"; "lightning__channels_closing_banner" = "КАНАЛЫ ЗАКРОЮТСЯ {date}"; "lightning__channels_closing_notice" = "Ваши каналы Lightning будут закрыты {date}. До этой даты переведите баланс расходов в сбережения."; +"lightning__channels_unavailable__text" = "Bitkit больше не открывает новые каналы Lightning, поэтому перевести средства из сбережений в расходы нельзя."; +"lightning__channels_unavailable__title" = "Новые каналы недоступны"; "lightning__closure_reason" = "Причина Закрытия"; "lightning__support" = "Поддержка"; "lightning__connection_closed__title" = "Соединение Закрыто"; diff --git a/Bitkit/ViewModels/SheetViewModel.swift b/Bitkit/ViewModels/SheetViewModel.swift index 97e963013..26d24ff7e 100644 --- a/Bitkit/ViewModels/SheetViewModel.swift +++ b/Bitkit/ViewModels/SheetViewModel.swift @@ -11,6 +11,7 @@ enum SheetID: String, CaseIterable { case highBalance case lnurlAuth case lnurlWithdraw + case channelsUnavailable case pubkyAuthApproval case notifications case paymentRequests @@ -268,6 +269,18 @@ class SheetViewModel: ObservableObject { } } + var channelsUnavailableSheetItem: ChannelsUnavailableSheetItem? { + get { + guard let config = activeSheetConfiguration, config.id == .channelsUnavailable else { return nil } + return ChannelsUnavailableSheetItem() + } + set { + if newValue == nil { + activeSheetConfiguration = nil + } + } + } + var forgotPinSheetItem: ForgotPinSheetItem? { get { guard let config = activeSheetConfiguration, config.id == .forgotPin else { return nil } diff --git a/Bitkit/Views/Wallets/HardwareWalletScreen.swift b/Bitkit/Views/Wallets/HardwareWalletScreen.swift index ca7620123..69222c226 100644 --- a/Bitkit/Views/Wallets/HardwareWalletScreen.swift +++ b/Bitkit/Views/Wallets/HardwareWalletScreen.swift @@ -3,13 +3,15 @@ import SwiftUI /// Detail overview of a paired hardware wallet, tracked as a watch-only balance. Mirrors the /// Savings/Spending screens: device name + blue Bitcoin icon in the top bar, balance header, the -/// device's on-chain activity grouped by date (blue hardware icons), and a Remove action. Ports bitkit-android's `HardwareWalletScreen`. +/// device's on-chain activity grouped by date (blue hardware icons), a Transfer-To-Spending action +/// on funded devices, and a Remove action. Ports bitkit-android's `HardwareWalletScreen`. struct HardwareWalletScreen: View { let walletId: String @EnvironmentObject var activity: ActivityListViewModel @EnvironmentObject var app: AppViewModel @EnvironmentObject var navigation: NavigationViewModel + @EnvironmentObject var sheets: SheetViewModel @Environment(HwWalletManager.self) private var hwWalletManager @Environment(TrezorManager.self) private var trezorManager @@ -40,6 +42,9 @@ struct HardwareWalletScreen: View { .task(id: wallet?.walletId) { await loadActivities() } + .task { + await app.checkGeoStatus() + } .onReceive(activity.activitiesChangedPublisher) { _ in Task { await loadActivities() } } @@ -59,6 +64,8 @@ struct HardwareWalletScreen: View { private func content(for wallet: HwWallet) -> some View { let hasActivity = !activities.isEmpty + // Only the native-segwit account can currently fund a transfer to spending. + let canTransferToSpending = wallet.fundingBalanceSats > 0 return VStack(spacing: 0) { ScrollView(showsIndicators: false) { @@ -70,6 +77,11 @@ struct HardwareWalletScreen: View { testIdPrefix: "TotalBalance" ) + if canTransferToSpending { + transferButton + .padding(.top, 28) + } + if hasActivity { HardwareWalletActivityList(activities: activities) .padding(.top, 32) @@ -108,6 +120,22 @@ struct HardwareWalletScreen: View { .offset(x: placement.trailingOverhang, y: ScreenLayout.topPaddingWithoutSafeArea) } + private var transferButton: some View { + CustomButton( + title: t("lightning__transfer_to_spending_button"), + variant: .secondary, + icon: Image("arrow-up-down") + .resizable() + .scaledToFit() + .frame(width: 16, height: 16) + .foregroundColor(.white80), + isDisabled: GeoService.shared.isGeoBlocked + ) { + sheets.showSheet(.channelsUnavailable) + } + .accessibilityIdentifier("HardwareTransferToSpending") + } + private func removeButton(for wallet: HwWallet) -> some View { CustomButton( title: t("hardware__remove_button", variables: ["name": wallet.name]), diff --git a/Bitkit/Views/Wallets/SavingsWalletScreen.swift b/Bitkit/Views/Wallets/SavingsWalletScreen.swift index 5365caf35..ff981ad05 100644 --- a/Bitkit/Views/Wallets/SavingsWalletScreen.swift +++ b/Bitkit/Views/Wallets/SavingsWalletScreen.swift @@ -4,6 +4,7 @@ struct SavingsWalletScreen: View { @EnvironmentObject var activity: ActivityListViewModel @EnvironmentObject var app: AppViewModel @EnvironmentObject var navigation: NavigationViewModel + @EnvironmentObject var sheets: SheetViewModel @EnvironmentObject var wallet: WalletViewModel private var shouldShowOnboarding: Bool { @@ -49,6 +50,12 @@ struct SavingsWalletScreen: View { } if !shouldShowOnboarding { + if !GeoService.shared.isGeoBlocked { + transferButton + .transition(.move(edge: .leading).combined(with: .opacity)) + .padding(.top, 28) + } + ActivityList(viewType: .onchain) CustomButton(title: t("common__show_all"), variant: .tertiary) { @@ -106,6 +113,21 @@ struct SavingsWalletScreen: View { .navigationBarHidden(true) .animation(.spring(response: 0.3), value: wallet.totalOnchainSats) } + + var transferButton: some View { + CustomButton( + title: t("lightning__transfer_to_spending_button"), + variant: .secondary, + icon: Image("arrow-up-down") + .resizable() + .scaledToFit() + .frame(width: 16, height: 16) + .foregroundColor(.white80) + ) { + sheets.showSheet(.channelsUnavailable) + } + .accessibilityIdentifier("TransferToSpending") + } } #Preview { diff --git a/Bitkit/Views/Wallets/Sheets/ChannelsUnavailableSheet.swift b/Bitkit/Views/Wallets/Sheets/ChannelsUnavailableSheet.swift new file mode 100644 index 000000000..9bb866466 --- /dev/null +++ b/Bitkit/Views/Wallets/Sheets/ChannelsUnavailableSheet.swift @@ -0,0 +1,51 @@ +import SwiftUI + +struct ChannelsUnavailableSheetItem: SheetItem { + let id: SheetID = .channelsUnavailable + let size: SheetSize = .medium +} + +/// Shown where the app used to start a transfer to spending: Blocktank no longer opens new channels. +struct ChannelsUnavailableSheet: View { + let config: ChannelsUnavailableSheetItem + + @EnvironmentObject private var sheets: SheetViewModel + @EnvironmentObject private var wallet: WalletViewModel + + var body: some View { + Sheet(id: .channelsUnavailable, data: config) { + VStack(alignment: .leading, spacing: 0) { + SheetHeader(title: t("lightning__channels_unavailable__title")) + + BodyMText(t("lightning__channels_unavailable__text"), textColor: .textSecondary) + + if wallet.hasTrustedChannels { + BodyMText( + t("lightning__channels_closing_notice", variables: ["date": ChannelsClosing.formattedCloseDate()]), + textColor: .textSecondary + ) + .padding(.top, 16) + .accessibilityIdentifier("ChannelsUnavailableCloseDate") + } + + Spacer() + + Image("exclamation-mark") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 256, height: 256) + .frame(maxWidth: .infinity) + + Spacer() + + CustomButton(title: t("common__ok")) { + sheets.hideSheet() + } + .accessibilityIdentifier("ChannelsUnavailableButton") + } + .padding(.horizontal, 16) + } + .accessibilityElement(children: .contain) + .accessibilityIdentifier("ChannelsUnavailableSheet") + } +} diff --git a/Bitkit/Views/Wallets/SpendingWalletScreen.swift b/Bitkit/Views/Wallets/SpendingWalletScreen.swift index 0a62c5053..01e1073e4 100644 --- a/Bitkit/Views/Wallets/SpendingWalletScreen.swift +++ b/Bitkit/Views/Wallets/SpendingWalletScreen.swift @@ -4,6 +4,7 @@ struct SpendingWalletScreen: View { @EnvironmentObject var activity: ActivityListViewModel @EnvironmentObject var app: AppViewModel @EnvironmentObject var navigation: NavigationViewModel + @EnvironmentObject var sheets: SheetViewModel @EnvironmentObject var wallet: WalletViewModel private var shouldShowOnboarding: Bool { @@ -43,6 +44,12 @@ struct SpendingWalletScreen: View { .accessibilityIdentifier("ChannelsClosingNotice") } + if shouldShowOnboarding && wallet.totalOnchainSats > 0 && !GeoService.shared.isGeoBlocked { + transferFromSavingsButton + .transition(.move(edge: .leading).combined(with: .opacity)) + .padding(.top, 28) + } + if !shouldShowOnboarding { if let channels = wallet.channels, !channels.isEmpty { transferButton @@ -108,6 +115,21 @@ struct SpendingWalletScreen: View { .animation(.spring(response: 0.3), value: wallet.totalLightningSats) } + var transferFromSavingsButton: some View { + CustomButton( + title: t("lightning__transfer_to_spending_button"), + variant: .secondary, + icon: Image("arrow-up-down") + .resizable() + .scaledToFit() + .frame(width: 16, height: 16) + .foregroundColor(.white80) + ) { + sheets.showSheet(.channelsUnavailable) + } + .accessibilityIdentifier("TransferFromSavings") + } + var transferButton: some View { CustomButton( title: t("lightning__transfer_to_savings_button"), diff --git a/BitkitTests/PubkyAuthURLSchemeTests.swift b/BitkitTests/PubkyAuthURLSchemeTests.swift index afcd6f1b7..19adf0f75 100644 --- a/BitkitTests/PubkyAuthURLSchemeTests.swift +++ b/BitkitTests/PubkyAuthURLSchemeTests.swift @@ -140,7 +140,6 @@ final class PubkyAuthURLSchemeTests: XCTestCase { ) let bitcoinURL = try XCTUnwrap(URL(string: "bitcoin:bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq?amount=0.001")) let bolt11URL = try XCTUnwrap(URL(string: "lightning:lnbc1example")) - let giftURL = try XCTUnwrap(URL(string: "bitkit://gift-code-1000")) let lnurlURL = try XCTUnwrap(URL(string: "lnurl:lnurl1example")) app.retainDeepLink(pubkyURL) @@ -218,12 +217,6 @@ final class PubkyAuthURLSchemeTests: XCTestCase { } XCTAssertNil(app.pendingDeepLinkURL) - app.retainDeepLink(giftURL) - await app.routePendingDeepLinkIfReady(true, nodeIsRunning: false) { routedURL in - XCTAssertEqual(routedURL, giftURL) - } - XCTAssertNil(app.pendingDeepLinkURL) - app.retainDeepLink(lnurlURL) await app.routePendingDeepLinkIfReady(true, nodeIsRunning: false) { _ in XCTFail("URLs that need the node must stay pending until LDK is running") diff --git a/journeys/README.md b/journeys/README.md index 7863662c5..2baa2361b 100644 --- a/journeys/README.md +++ b/journeys/README.md @@ -114,7 +114,7 @@ wallet however you normally do against regtest and skip the helper. ### Getting a Lightning channel Blocktank is winding down, and this build no longer buys channels from it (synonymdev/bitkit-ios#1097): -Transfer To Spending, the Receive sheet's CJIT button and gift codes are gone. A journey that needs a +Transfer To Spending opens a "New Channels Unavailable" sheet, and the Receive sheet's CJIT button and gift codes are gone. A journey that needs a Lightning channel as its precondition says "a wallet with an open Lightning channel"; get one in either way: - Install a build from before that change (master before synonymdev/bitkit-ios#1097), open a channel with @@ -238,7 +238,7 @@ buys a channel went with them. They are listed here rather than left as stale fi | `amount-limits/transfer-spending-over-max.xml`, `transfer-spending-advanced-over-max.xml` | Transfer To Spending amount and Receiving Capacity number pads | same names | | `cjit-notifications/cjit-background-notification.xml` | CJIT notification with the app backgrounded | `cjit-foreground-service-notification.xml` | | `cjit-notifications/cjit-push-single-notification.xml` | CJIT notification from a push to a killed app | same name | -| `hardware-wallet/transfer-to-spending.xml`, `transfer-to-spending-max-lsp-cap.xml`, `transfer-to-spending-node-warmup.xml`, `passphrase-transfer-to-spending.xml` | Hardware wallet Transfer To Spending (the `HardwareTransferToSpending` button is gone) | same names | +| `hardware-wallet/transfer-to-spending.xml`, `transfer-to-spending-max-lsp-cap.xml`, `transfer-to-spending-node-warmup.xml`, `passphrase-transfer-to-spending.xml` | Hardware wallet Transfer To Spending (`HardwareTransferToSpending` now opens the channels unavailable sheet) | same names | | `hardware-wallet/transfer-to-spending-over-max.xml` | Hardware wallet Transfer To Spending amount cap | none — iOS-only | | `notification-permission/receive-cjit-confirm-notification-toggle.xml`, `receive-cjit-liquidity-notification-toggle.xml`, `transfer-spending-confirm-notification-toggle.xml` | The background-setup toggle on the CJIT and Spending Confirm screens, which nothing navigates to now | same names | | `notification-permission/toggle-off-and-system-settings-route.xml` | Toggle off on the CJIT confirm screen, then Settings › Notifications into the iOS Settings app | `toggle-off-opens-system-settings.xml` | diff --git a/journeys/amount-limits/README.md b/journeys/amount-limits/README.md index 8188f4dc7..70eb789a0 100644 --- a/journeys/amount-limits/README.md +++ b/journeys/amount-limits/README.md @@ -88,6 +88,6 @@ gate. `transfer-spending-over-max.xml` and `transfer-spending-advanced-over-max.xml` were retired because Blocktank channel purchases were removed (synonymdev/bitkit-ios#1097), and so was `../hardware-wallet/transfer-to-spending-over-max.xml`. Their routes started at the Savings wallet's -"Transfer To Spending" button or the hardware wallet's, which are gone. Their identifiers -(`TransferToSpending`, `SpendingAmount*`, `SpendingAdvanced*`, `HardwareTransferAmount*`) are no -longer reachable from the UI. +"Transfer To Spending" button or the hardware wallet's, which now open the "New Channels Unavailable" +sheet. The amount screens' identifiers (`SpendingAmount*`, `SpendingAdvanced*`, +`HardwareTransferAmount*`) are no longer reachable from the UI. diff --git a/journeys/channels-closing/README.md b/journeys/channels-closing/README.md index f0d1803b0..749f47667 100644 --- a/journeys/channels-closing/README.md +++ b/journeys/channels-closing/README.md @@ -15,9 +15,12 @@ The Android twin is synonymdev/bitkit-android#1669. same Blocktank-channel condition. - The date is a placeholder constant (Dec 15, 2026) until the shutdown date is final. Journeys assert the banner and the notice agree and read the date from the screen instead of hardcoding it. -- **Removed:** Savings "Transfer To Spending" (`TransferToSpending`), Spending "Transfer from Savings" - (`TransferFromSavings`), hardware wallet "Transfer To Spending" (`HardwareTransferToSpending`), the - Home "Spend" suggestion card (`Suggestion-lightning`), the Receive sheet's CJIT button and Spending +- **Channels unavailable sheet** `ChannelsUnavailableSheet`, "New Channels Unavailable": Savings + "Transfer To Spending" (`TransferToSpending`), Spending "Transfer from Savings" (`TransferFromSavings`) + and hardware wallet "Transfer To Spending" (`HardwareTransferToSpending`) keep their visibility rules + and open it instead of the transfer flow. For a wallet with a Blocktank channel it adds the close date + (`ChannelsUnavailableCloseDate`); "OK" (`ChannelsUnavailableButton`) closes it. +- **Removed:** the Home "Spend" suggestion card (`Suggestion-lightning`), the Receive sheet's CJIT button and Spending onboarding, the Edit Invoice CJIT offer, the TabBar Receive shortcut into the CJIT amount screen and gift codes. Transfer To Savings, send, receive over an existing channel, LNURL channel and manual external node stay. @@ -33,11 +36,12 @@ The Android twin is synonymdev/bitkit-android#1669. | Journey | Covers | | --- | --- | -| `channels-closing-notice.xml` | Banner on Home → tap → Spending wallet notice and Transfer To Savings; Savings wallet has no Transfer To Spending | -| [`../receive/receive-onchain-only-without-channel.xml`](../receive/receive-onchain-only-without-channel.xml) | The opposite precondition: a funded wallet with no channels shows no banner, no Spend card and a Savings-only Receive sheet | +| `channels-closing-notice.xml` | Banner on Home → tap → Spending wallet notice and Transfer To Savings; Savings Transfer To Spending opens the channels unavailable sheet with the close date | +| [`../receive/receive-onchain-only-without-channel.xml`](../receive/receive-onchain-only-without-channel.xml) | The opposite precondition: a funded wallet with no channels shows no banner, no Spend card and a Savings-only Receive sheet; the transfer buttons open the sheet without the close date | ## Identifiers used `ChannelsClosingBanner`, `ChannelsClosingNotice`, `ActivitySavings`, `ActivitySpending`, `TotalBalance-primary` (Android `TotalBalance`), `TransferToSavings`, `NavigationBack`, -`TransferToSpending` (asserted absent). +`TransferToSpending`, `TransferFromSavings`, `HardwareTransferToSpending`, `ChannelsUnavailableSheet`, +`ChannelsUnavailableCloseDate`, `ChannelsUnavailableButton`. diff --git a/journeys/channels-closing/channels-closing-notice.xml b/journeys/channels-closing/channels-closing-notice.xml index c5e68d6b5..9c48ca11b 100644 --- a/journeys/channels-closing/channels-closing-notice.xml +++ b/journeys/channels-closing/channels-closing-notice.xml @@ -5,9 +5,10 @@ "CHANNELS CLOSE ON DEC 15, 2026" (id "ChannelsClosingBanner"), tapping it opens the Spending wallet, which shows "Your Lightning channels will be closed on Dec 15, 2026. Move your spending balance to savings before then." (id "ChannelsClosingNotice") under the balance header, and - Transfer To Savings (id "TransferToSavings") is still offered. The Savings wallet has no - Transfer To Spending button (id "TransferToSpending"), since new channels can no longer be - bought. The date is a placeholder constant until the shutdown date is final; assert whatever + Transfer To Savings (id "TransferToSavings") is still offered. The Savings wallet's + Transfer To Spending button (id "TransferToSpending") opens the "New Channels Unavailable" sheet + (id "ChannelsUnavailableSheet") instead of the transfer flow, since new channels can no longer be + bought; for this wallet the sheet also shows the close date (id "ChannelsUnavailableCloseDate"). The date is a placeholder constant until the shutdown date is final; assert whatever date the build shows, and keep the banner and the notice in agreement. iOS: "Press the system back button" is the navigation bar back button (id "NavigationBack"). @@ -27,7 +28,9 @@ Verify the notice (id "ChannelsClosingNotice") reads "Your Lightning channels will be closed on Dec 15, 2026. Move your spending balance to savings before then." with the same date the banner showed Verify the "Transfer To Savings" button (id "TransferToSavings") is visible Press the system back button, then tap the Savings balance card (id "ActivitySavings") - Verify the Savings wallet screen is open and no "Transfer To Spending" button (id "TransferToSpending") is shown + Verify the Savings wallet screen is open, then tap "Transfer To Spending" (id "TransferToSpending") + Verify the "New Channels Unavailable" sheet (id "ChannelsUnavailableSheet") opens with "Bitkit no longer offers new Lightning channels, so you can't move funds from savings to spending." and the close date paragraph (id "ChannelsUnavailableCloseDate") with the same date the banner showed, and no transfer amount screen opens + Tap "OK" (id "ChannelsUnavailableButton") and verify the sheet closes and the Savings wallet screen is still shown Press the system back button, tap the Spending balance card (id "ActivitySpending") and verify the notice (id "ChannelsClosingNotice") is still shown diff --git a/journeys/hardware-wallet/README.md b/journeys/hardware-wallet/README.md index 89e12bd4c..ef87083d5 100644 --- a/journeys/hardware-wallet/README.md +++ b/journeys/hardware-wallet/README.md @@ -129,7 +129,7 @@ Walked as far as a simulator allows, against a wallet with two Trezor identities `RenameHardwareWalletInput`, `RenameHardwareWalletSave`. - Home and detail: tile `ActivityHardware`, screen `HardwareWalletScreen`, `RemoveHardwareWallet`, `RemoveHwWalletDialog`, `HwRemoveKeepBackupToggle`. -- Transfer: `HardwareTransferToSpending`, asserted absent by `detail-overview.xml`. +- Transfer: `HardwareTransferToSpending`, which opens `ChannelsUnavailableSheet` (`detail-overview.xml`). - Activity: `ActivityShowAll` (home only). Rows use **two different schemes** — `ActivityShort-` in the home recent list, `Activity-` in All Activity and on the hardware wallet screen. Tabs `Tab-all`/`Tab-sent`/`Tab-received`/`Tab-other`, @@ -142,9 +142,9 @@ Walked as far as a simulator allows, against a wallet with two Trezor identities `transfer-to-spending.xml`, `transfer-to-spending-max-lsp-cap.xml`, `transfer-to-spending-node-warmup.xml`, `passphrase-transfer-to-spending.xml` and the iOS-only `transfer-to-spending-over-max.xml` were retired because Blocktank channel purchases were removed -(synonymdev/bitkit-ios#1097): the hardware wallet detail screen no longer has a Transfer To Spending -button (`HardwareTransferToSpending`). The `HardwareTransfer*` and `HwTransferPassphrase*` screens and -identifiers are still in code but nothing in the UI reaches them. +(synonymdev/bitkit-ios#1097): the hardware wallet's Transfer To Spending button (`HardwareTransferToSpending`) now +opens the "New Channels Unavailable" sheet. The `HardwareTransfer*` and `HwTransferPassphrase*` screens +and identifiers are still in code but nothing in the UI reaches them. ## Local Payment Completion diff --git a/journeys/hardware-wallet/detail-overview.xml b/journeys/hardware-wallet/detail-overview.xml index fd057c59e..3810049c7 100644 --- a/journeys/hardware-wallet/detail-overview.xml +++ b/journeys/hardware-wallet/detail-overview.xml @@ -1,8 +1,8 @@ Opens the hardware wallet detail screen from the home tile and verifies its overview: - the top bar, balance header, the absence of a Transfer To Spending entry (removed with - Blocktank channel purchases, synonymdev/bitkit-ios#1097), the grouped activity list, and the Remove device confirm dialog. The final + the top bar, balance header, the Transfer To Spending entry, which opens the + "New Channels Unavailable" sheet since Blocktank channel purchases were removed (synonymdev/bitkit-ios#1097), the grouped activity list, and the Remove device confirm dialog. The final Remove step forgets the device, so run this last (re-run connect-home-tile.xml to pair again). Requires a paired Bridge emulator (run connect-home-tile.xml first). @@ -10,7 +10,7 @@ Launch the Bitkit app and go to the wallet home screen Tap the hardware wallet tile (id "ActivityHardware") beneath the SAVINGS and SPENDING tiles Verify the hardware wallet detail screen opens (id "HardwareWalletScreen"), showing the device name with a blue bitcoin icon in the top bar and a balance header - Verify no "Transfer To Spending" button (id "HardwareTransferToSpending") is shown, whether or not the wallet holds funds + If a "Transfer To Spending" button is shown (id "HardwareTransferToSpending"), tap it, verify the "New Channels Unavailable" sheet opens (id "ChannelsUnavailableSheet") and no transfer amount screen opens, then tap "OK" (id "ChannelsUnavailableButton"); otherwise skip this step If the activity list shows transactions, verify their circular icons are blue, then tap the first one, verify an activity detail screen opens, and navigate back Tap the "Remove" button labelled with the device name near the bottom of the screen (id "RemoveHardwareWallet") Verify a confirm dialog appears (id "RemoveHwWalletDialog") explaining that your funds are safe and your coins won't be deleted diff --git a/journeys/receive/receive-onchain-only-without-channel.xml b/journeys/receive/receive-onchain-only-without-channel.xml index 212457b54..548766f56 100644 --- a/journeys/receive/receive-onchain-only-without-channel.xml +++ b/journeys/receive/receive-onchain-only-without-channel.xml @@ -26,6 +26,8 @@ Verify there is no "Receive Lightning funds" button anywhere on the sheet Tap "Edit" (id "SpecifyInvoiceButton"), tap the amount field (id "ReceiveNumberPadTextField"), enter a small amount (for example "N1" then "N0" twice), tap Continue (id "ReceiveNumberPadSubmit") and then "QR Code" (id "ShowQrReceive") Verify the sheet returns to the QR screen with no liquidity or channel confirm screen in between, the Savings tab is still selected and there is still no Spending tab (id "Tab-spending") - Close the sheet, open the Savings wallet (id "ActivitySavings") and verify no "Transfer To Spending" button (id "TransferToSpending") is shown, then open the Spending wallet (id "ActivitySpending") and verify no "Transfer from Savings" button (id "TransferFromSavings") and no notice (id "ChannelsClosingNotice") is shown + Close the sheet, open the Savings wallet (id "ActivitySavings") and tap "Transfer To Spending" (id "TransferToSpending") + Verify the "New Channels Unavailable" sheet (id "ChannelsUnavailableSheet") opens with its text and without the close date paragraph (id "ChannelsUnavailableCloseDate"), then tap "OK" (id "ChannelsUnavailableButton") + Open the Spending wallet (id "ActivitySpending"), verify no notice (id "ChannelsClosingNotice") is shown, tap "Transfer from Savings" (id "TransferFromSavings") and verify the same sheet opens, then tap "OK" From 186bf10f90e7081d3f317bb796c3c3041eea3f91 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Thu, 22 Oct 2026 13:14:54 +0200 Subject: [PATCH 04/12] feat: offer third-party lightning providers from transfer to spending The sheet now says automatic transfers are gone and opens a Choose LSP screen with third-party providers, manual setup and the scanner. --- Bitkit/MainNavView.swift | 1 + .../Localization/ar.lproj/Localizable.strings | 8 +- .../Localization/ca.lproj/Localizable.strings | 8 +- .../Localization/cs.lproj/Localizable.strings | 8 +- .../Localization/de.lproj/Localizable.strings | 8 +- .../Localization/el.lproj/Localizable.strings | 8 +- .../Localization/en.lproj/Localizable.strings | 8 +- .../es-419.lproj/Localizable.strings | 8 +- .../Localization/es.lproj/Localizable.strings | 8 +- .../Localization/fr.lproj/Localizable.strings | 8 +- .../Localization/it.lproj/Localizable.strings | 8 +- .../Localization/nl.lproj/Localizable.strings | 8 +- .../Localization/pl.lproj/Localizable.strings | 8 +- .../pt-BR.lproj/Localizable.strings | 8 +- .../Localization/pt.lproj/Localizable.strings | 8 +- .../Localization/ru.lproj/Localizable.strings | 8 +- Bitkit/ViewModels/NavigationViewModel.swift | 1 + Bitkit/Views/Transfer/ChooseLspView.swift | 89 +++++++++++++++++++ .../Sheets/ChannelsUnavailableSheet.swift | 18 +++- journeys/README.md | 2 +- journeys/amount-limits/README.md | 2 +- journeys/channels-closing/README.md | 20 +++-- .../channels-closing-notice.xml | 6 +- .../choose-lsp-from-transfer.xml | 27 ++++++ journeys/hardware-wallet/README.md | 2 +- journeys/hardware-wallet/detail-overview.xml | 4 +- .../receive-onchain-only-without-channel.xml | 5 +- 27 files changed, 247 insertions(+), 50 deletions(-) create mode 100644 Bitkit/Views/Transfer/ChooseLspView.swift create mode 100644 journeys/channels-closing/choose-lsp-from-transfer.xml diff --git a/Bitkit/MainNavView.swift b/Bitkit/MainNavView.swift index e6b410d39..3bcdaf5fb 100644 --- a/Bitkit/MainNavView.swift +++ b/Bitkit/MainNavView.swift @@ -560,6 +560,7 @@ struct MainNavView: View { case .transferLearnMore: TransferLearnMoreView() case .settingUp: SettingUpView() case .fundingAdvanced: FundAdvancedOptions() + case .chooseLsp: ChooseLspView() case let .fundManual(nodeUri): FundManualSetupView(initialNodeUri: nodeUri) case let .fundManualAmount(lnPeer): FundManualAmountView(lnPeer: lnPeer) case let .fundManualConfirm(lnPeer, amountSats): FundManualConfirmView(lnPeer: lnPeer, amountSats: amountSats) diff --git a/Bitkit/Resources/Localization/ar.lproj/Localizable.strings b/Bitkit/Resources/Localization/ar.lproj/Localizable.strings index daf2f220a..5ca67a015 100644 --- a/Bitkit/Resources/Localization/ar.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/ar.lproj/Localizable.strings @@ -319,8 +319,12 @@ "lightning__channel_point" = "نقطة القناة"; "lightning__channels_closing_banner" = "تُغلق القنوات في {date}"; "lightning__channels_closing_notice" = "ستُغلق قنوات Lightning الخاصة بك في {date}. انقل رصيد الإنفاق إلى المدخرات قبل ذلك."; -"lightning__channels_unavailable__text" = "لم يعد Bitkit يوفّر قنوات Lightning جديدة، لذا لا يمكنك نقل الأموال من المدخرات إلى الإنفاق."; -"lightning__channels_unavailable__title" = "القنوات الجديدة غير متاحة"; +"lightning__channels_unavailable__button" = "فتح قناة"; +"lightning__channels_unavailable__text" = "للأسف، لم تعد خدمة التحويل التلقائي للأموال إلى رصيد الإنفاق متاحة. لا يزال بإمكانك فتح قناة Lightning يدويًا لتعبئة رصيد الإنفاق."; +"lightning__channels_unavailable__title" = "لا يوجد تحويل تلقائي"; +"lightning__choose_lsp__explanation" = "عندما يقدّمون لك رمز QR للاتصال بقناة Lightning أو المطالبة بها، اضغط أدناه لمسح رمز QR أو لصقه."; +"lightning__choose_lsp__text" = "اطلب قناة من مزوّد خدمة Lightning ‏(LSP). فيما يلي بعض الخيارات. لا يدير Bitkit هذه الخدمات ولا يؤيدها ولا يدعمها."; +"lightning__choose_lsp__title" = "اختر\nمزوّد Lightning"; "lightning__closure_reason" = "سبب الإغلاق"; "lightning__support" = "الدعم"; "lightning__connection_closed__title" = "تم إغلاق الاتصال"; diff --git a/Bitkit/Resources/Localization/ca.lproj/Localizable.strings b/Bitkit/Resources/Localization/ca.lproj/Localizable.strings index e8e6bf3c3..50c23eb43 100644 --- a/Bitkit/Resources/Localization/ca.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/ca.lproj/Localizable.strings @@ -356,8 +356,12 @@ "lightning__channel_point" = "Punt del canal"; "lightning__channels_closing_banner" = "ELS CANALS ES TANQUEN EL {date}"; "lightning__channels_closing_notice" = "Els teus canals Lightning es tancaran el {date}. Transfereix el teu saldo de despesa a estalvis abans d'aquesta data."; -"lightning__channels_unavailable__text" = "Bitkit ja no ofereix canals Lightning nous, així que no pots transferir fons d'estalvis a despesa."; -"lightning__channels_unavailable__title" = "Canals nous no disponibles"; +"lightning__channels_unavailable__button" = "Obrir canal"; +"lightning__channels_unavailable__text" = "Malauradament, el servei per transferir fons automàticament al teu saldo de despesa ja no està disponible. Encara pots obrir manualment un canal Lightning per recarregar el teu saldo de despesa."; +"lightning__channels_unavailable__title" = "Sense transferència automàtica"; +"lightning__choose_lsp__explanation" = "Quan et donin un QR per connectar o reclamar el canal Lightning, toca a sota per escanejar o enganxar el QR."; +"lightning__choose_lsp__text" = "Demana un canal a un proveïdor de serveis Lightning (LSP). A sota hi ha algunes opcions. Bitkit no les gestiona, avala ni hi dona suport."; +"lightning__choose_lsp__title" = "Tria un\nproveïdor Lightning"; "lightning__closure_reason" = "Motiu del tancament"; "lightning__support" = "Suport"; "lightning__connection_closed__title" = "Connexió tancada"; diff --git a/Bitkit/Resources/Localization/cs.lproj/Localizable.strings b/Bitkit/Resources/Localization/cs.lproj/Localizable.strings index ebed14314..03d287c31 100644 --- a/Bitkit/Resources/Localization/cs.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/cs.lproj/Localizable.strings @@ -372,8 +372,12 @@ "lightning__channel_point" = "Bod kanálu"; "lightning__channels_closing_banner" = "KANÁLY SE ZAVŘOU {date}"; "lightning__channels_closing_notice" = "Vaše Lightning kanály budou uzavřeny {date}. Do té doby převeďte zůstatek útrat do úspor."; -"lightning__channels_unavailable__text" = "Bitkit už nenabízí nové Lightning kanály, takže nemůžete převádět prostředky z úspor do útrat."; -"lightning__channels_unavailable__title" = "Nové kanály nejsou dostupné"; +"lightning__channels_unavailable__button" = "Otevřít kanál"; +"lightning__channels_unavailable__text" = "Služba pro automatický převod prostředků na zůstatek útrat bohužel již není k dispozici. Stále můžete ručně otevřít Lightning kanál a doplnit si zůstatek útrat."; +"lightning__channels_unavailable__title" = "Žádný automatický převod"; +"lightning__choose_lsp__explanation" = "Až vám dají QR kód pro připojení nebo převzetí Lightning kanálu, klepněte níže a QR kód naskenujte nebo vložte."; +"lightning__choose_lsp__text" = "Objednejte si kanál u poskytovatele služeb Lightning (LSP). Níže je několik možností. Bitkit je neprovozuje, nedoporučuje ani nepodporuje."; +"lightning__choose_lsp__title" = "Vyberte\nposkytovatele Lightning"; "lightning__closure_reason" = "Důvod uzavření"; "lightning__support" = "Podpora"; "lightning__connection_closed__title" = "Připojení uzavřeno"; diff --git a/Bitkit/Resources/Localization/de.lproj/Localizable.strings b/Bitkit/Resources/Localization/de.lproj/Localizable.strings index a82ee34df..de657351b 100644 --- a/Bitkit/Resources/Localization/de.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/de.lproj/Localizable.strings @@ -370,8 +370,12 @@ "lightning__channel_point" = "Kanalpunkt"; "lightning__channels_closing_banner" = "KANÄLE SCHLIESSEN AM {date}"; "lightning__channels_closing_notice" = "Deine Lightning-Kanäle werden am {date} geschlossen. Übertrage dein Guthaben vorher auf das Sparkonto."; -"lightning__channels_unavailable__text" = "Bitkit bietet keine neuen Lightning-Kanäle mehr an, daher kannst du kein Guthaben vom Sparkonto auf das Ausgabenkonto übertragen."; -"lightning__channels_unavailable__title" = "Neue Kanäle nicht verfügbar"; +"lightning__channels_unavailable__button" = "Kanal öffnen"; +"lightning__channels_unavailable__text" = "Leider ist der Dienst, der Guthaben automatisch auf dein Ausgabenkonto überträgt, nicht mehr verfügbar. Du kannst weiterhin manuell einen Lightning-Kanal öffnen, um dein Ausgabenkonto aufzuladen."; +"lightning__channels_unavailable__title" = "Keine automatische Übertragung"; +"lightning__choose_lsp__explanation" = "Wenn du einen QR-Code zum Verbinden oder Einlösen des Lightning-Kanals erhältst, tippe unten, um ihn zu scannen oder einzufügen."; +"lightning__choose_lsp__text" = "Bestelle einen Kanal bei einem Lightning Service Provider (LSP). Unten findest du einige Optionen. Bitkit betreibt, empfiehlt oder unterstützt diese nicht."; +"lightning__choose_lsp__title" = "Wähle einen\nLightning-Anbieter"; "lightning__closure_reason" = "Grund für die Schließung"; "lightning__support" = "Support"; "lightning__connection_closed__title" = "Verbindung geschlossen"; diff --git a/Bitkit/Resources/Localization/el.lproj/Localizable.strings b/Bitkit/Resources/Localization/el.lproj/Localizable.strings index 7f28d4ea0..c0e76d461 100644 --- a/Bitkit/Resources/Localization/el.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/el.lproj/Localizable.strings @@ -343,8 +343,12 @@ "lightning__channel_point" = "Channel point"; "lightning__channels_closing_banner" = "ΤΑ ΚΑΝΑΛΙΑ ΚΛΕΙΝΟΥΝ ΣΤΙΣ {date}"; "lightning__channels_closing_notice" = "Τα κανάλια Lightning σας θα κλείσουν στις {date}. Μεταφέρετε το υπόλοιπο δαπανών σε αποταμιεύσεις πριν από τότε."; -"lightning__channels_unavailable__text" = "Το Bitkit δεν προσφέρει πλέον νέα κανάλια Lightning, οπότε δεν μπορείτε να μεταφέρετε κεφάλαια από τις αποταμιεύσεις στις δαπάνες."; -"lightning__channels_unavailable__title" = "Νέα κανάλια μη διαθέσιμα"; +"lightning__channels_unavailable__button" = "Άνοιγμα καναλιού"; +"lightning__channels_unavailable__text" = "Δυστυχώς, η υπηρεσία αυτόματης μεταφοράς κεφαλαίων στο υπόλοιπο δαπανών δεν είναι πλέον διαθέσιμη. Μπορείτε ακόμα να ανοίξετε χειροκίνητα ένα κανάλι Lightning για να αναπληρώσετε το υπόλοιπο δαπανών."; +"lightning__channels_unavailable__title" = "Χωρίς αυτόματη μεταφορά"; +"lightning__choose_lsp__explanation" = "Όταν σας δώσουν ένα QR για σύνδεση ή διεκδίκηση του καναλιού Lightning, πατήστε παρακάτω για σάρωση ή επικόλληση του QR."; +"lightning__choose_lsp__text" = "Παραγγείλετε ένα κανάλι από έναν πάροχο υπηρεσιών Lightning (LSP). Παρακάτω υπάρχουν μερικές επιλογές. Το Bitkit δεν τις λειτουργεί, δεν τις προτείνει και δεν τις υποστηρίζει."; +"lightning__choose_lsp__title" = "Επιλέξτε\nπάροχο Lightning"; "lightning__closure_reason" = "Λόγος κλεισίματος"; "lightning__support" = "Υποστήριξη"; "lightning__connection_closed__title" = "Η Σύνδεση Έκλεισε"; diff --git a/Bitkit/Resources/Localization/en.lproj/Localizable.strings b/Bitkit/Resources/Localization/en.lproj/Localizable.strings index ba1eb2b97..8168c36d4 100644 --- a/Bitkit/Resources/Localization/en.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/en.lproj/Localizable.strings @@ -365,8 +365,12 @@ "lightning__channel_point" = "Channel point"; "lightning__channels_closing_banner" = "CHANNELS CLOSE ON {date}"; "lightning__channels_closing_notice" = "Your Lightning channels will be closed on {date}. Move your spending balance to savings before then."; -"lightning__channels_unavailable__text" = "Bitkit no longer offers new Lightning channels, so you can't move funds from savings to spending."; -"lightning__channels_unavailable__title" = "New Channels Unavailable"; +"lightning__channels_unavailable__button" = "Open Channel"; +"lightning__channels_unavailable__text" = "Unfortunately, the service to automatically transfer funds to your spending balance is no longer available. You can still manually open a Lightning channel to top up your spending balance."; +"lightning__channels_unavailable__title" = "No automatic transfer"; +"lightning__choose_lsp__explanation" = "When they give you a QR to connect or claim the Lightning channel, tap below to scan or paste the QR."; +"lightning__choose_lsp__text" = "Order a channel from a Lightning Service Provider (LSP). Below are some options. Bitkit does not run, endorse, or support these."; +"lightning__choose_lsp__title" = "Choose a\nLightning provider"; "lightning__closure_reason" = "Closure reason"; "lightning__support" = "Support"; "lightning__connection_closed__title" = "Connection Closed"; diff --git a/Bitkit/Resources/Localization/es-419.lproj/Localizable.strings b/Bitkit/Resources/Localization/es-419.lproj/Localizable.strings index 87308a4bf..ea824e160 100644 --- a/Bitkit/Resources/Localization/es-419.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/es-419.lproj/Localizable.strings @@ -373,8 +373,12 @@ "lightning__channel_point" = "Punto del canal"; "lightning__channels_closing_banner" = "LOS CANALES CIERRAN EL {date}"; "lightning__channels_closing_notice" = "Tus canales Lightning se cerrarán el {date}. Transfiere tu saldo de gasto a ahorros antes de esa fecha."; -"lightning__channels_unavailable__text" = "Bitkit ya no ofrece canales Lightning nuevos, así que no puedes transferir fondos de ahorros a gasto."; -"lightning__channels_unavailable__title" = "Canales nuevos no disponibles"; +"lightning__channels_unavailable__button" = "Abrir canal"; +"lightning__channels_unavailable__text" = "Lamentablemente, el servicio para transferir fondos automáticamente a tu saldo de gasto ya no está disponible. Aún puedes abrir manualmente un canal Lightning para recargar tu saldo de gasto."; +"lightning__channels_unavailable__title" = "Sin transferencia automática"; +"lightning__choose_lsp__explanation" = "Cuando te den un QR para conectar o reclamar el canal Lightning, toca abajo para escanear o pegar el QR."; +"lightning__choose_lsp__text" = "Pide un canal a un proveedor de servicios Lightning (LSP). Abajo hay algunas opciones. Bitkit no las opera, respalda ni brinda soporte."; +"lightning__choose_lsp__title" = "Elige un\nproveedor Lightning"; "lightning__closure_reason" = "Motivo del cierre"; "lightning__support" = "Soporte"; "lightning__connection_closed__title" = "Conexión cerrada"; diff --git a/Bitkit/Resources/Localization/es.lproj/Localizable.strings b/Bitkit/Resources/Localization/es.lproj/Localizable.strings index 132752d1e..acdcb7b4e 100644 --- a/Bitkit/Resources/Localization/es.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/es.lproj/Localizable.strings @@ -357,8 +357,12 @@ "lightning__channel_point" = "Punto del canal"; "lightning__channels_closing_banner" = "LOS CANALES CIERRAN EL {date}"; "lightning__channels_closing_notice" = "Tus canales Lightning se cerrarán el {date}. Transfiere tu saldo de gasto a ahorros antes de esa fecha."; -"lightning__channels_unavailable__text" = "Bitkit ya no ofrece canales Lightning nuevos, así que no puedes transferir fondos de ahorro a gasto."; -"lightning__channels_unavailable__title" = "Canales nuevos no disponibles"; +"lightning__channels_unavailable__button" = "Abrir canal"; +"lightning__channels_unavailable__text" = "Lamentablemente, el servicio para transferir fondos automáticamente a tu saldo de gasto ya no está disponible. Aún puedes abrir manualmente un canal Lightning para recargar tu saldo de gasto."; +"lightning__channels_unavailable__title" = "Sin transferencia automática"; +"lightning__choose_lsp__explanation" = "Cuando te den un QR para conectar o reclamar el canal Lightning, toca abajo para escanear o pegar el QR."; +"lightning__choose_lsp__text" = "Pide un canal a un proveedor de servicios Lightning (LSP). Abajo tienes algunas opciones. Bitkit no las gestiona, respalda ni da soporte."; +"lightning__choose_lsp__title" = "Elige un\nproveedor Lightning"; "lightning__closure_reason" = "Motivo del cierre"; "lightning__support" = "Soporte"; "lightning__connection_closed__title" = "Conexión Cerrada"; diff --git a/Bitkit/Resources/Localization/fr.lproj/Localizable.strings b/Bitkit/Resources/Localization/fr.lproj/Localizable.strings index aea09ed56..b89474cd2 100644 --- a/Bitkit/Resources/Localization/fr.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/fr.lproj/Localizable.strings @@ -373,8 +373,12 @@ "lightning__channel_point" = "Point du canal"; "lightning__channels_closing_banner" = "LES CANAUX FERMENT LE {date}"; "lightning__channels_closing_notice" = "Vos canaux Lightning seront fermés le {date}. Transférez votre solde de dépenses vers l'Épargne avant cette date."; -"lightning__channels_unavailable__text" = "Bitkit ne propose plus de nouveaux canaux Lightning, vous ne pouvez donc pas transférer de fonds de l'Épargne vers les Dépenses."; -"lightning__channels_unavailable__title" = "Nouveaux canaux indisponibles"; +"lightning__channels_unavailable__button" = "Ouvrir un canal"; +"lightning__channels_unavailable__text" = "Malheureusement, le service de transfert automatique de fonds vers votre solde de dépenses n'est plus disponible. Vous pouvez toujours ouvrir manuellement un canal Lightning pour recharger votre solde de dépenses."; +"lightning__channels_unavailable__title" = "Pas de transfert automatique"; +"lightning__choose_lsp__explanation" = "Quand ils vous donnent un QR pour connecter ou réclamer le canal Lightning, touchez ci-dessous pour scanner ou coller le QR."; +"lightning__choose_lsp__text" = "Commandez un canal auprès d'un fournisseur de services Lightning (LSP). Voici quelques options. Bitkit ne les exploite pas, ne les cautionne pas et ne les prend pas en charge."; +"lightning__choose_lsp__title" = "Choisissez un\nfournisseur Lightning"; "lightning__closure_reason" = "Motif de la fermeture"; "lightning__support" = "Support"; "lightning__connection_closed__title" = "Connexion fermée"; diff --git a/Bitkit/Resources/Localization/it.lproj/Localizable.strings b/Bitkit/Resources/Localization/it.lproj/Localizable.strings index def94cf9b..c33222406 100644 --- a/Bitkit/Resources/Localization/it.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/it.lproj/Localizable.strings @@ -366,8 +366,12 @@ "lightning__channel_point" = "Punto canale"; "lightning__channels_closing_banner" = "I CANALI CHIUDONO IL {date}"; "lightning__channels_closing_notice" = "I tuoi canali Lightning verranno chiusi il {date}. Trasferisci il saldo di spesa ai risparmi prima di allora."; -"lightning__channels_unavailable__text" = "Bitkit non offre più nuovi canali Lightning, quindi non puoi trasferire fondi dai risparmi alla spesa."; -"lightning__channels_unavailable__title" = "Nuovi canali non disponibili"; +"lightning__channels_unavailable__button" = "Apri canale"; +"lightning__channels_unavailable__text" = "Purtroppo il servizio per trasferire automaticamente fondi al saldo di spesa non è più disponibile. Puoi ancora aprire manualmente un canale Lightning per ricaricare il saldo di spesa."; +"lightning__channels_unavailable__title" = "Nessun trasferimento automatico"; +"lightning__choose_lsp__explanation" = "Quando ti danno un QR per collegare o riscattare il canale Lightning, tocca qui sotto per scansionare o incollare il QR."; +"lightning__choose_lsp__text" = "Ordina un canale da un fornitore di servizi Lightning (LSP). Qui sotto trovi alcune opzioni. Bitkit non le gestisce, non le approva e non le supporta."; +"lightning__choose_lsp__title" = "Scegli un\nfornitore Lightning"; "lightning__closure_reason" = "Motivo della chiusura"; "lightning__support" = "Supporto"; "lightning__connection_closed__title" = "Connessione chiusa"; diff --git a/Bitkit/Resources/Localization/nl.lproj/Localizable.strings b/Bitkit/Resources/Localization/nl.lproj/Localizable.strings index bbfdec24c..b1dad2708 100644 --- a/Bitkit/Resources/Localization/nl.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/nl.lproj/Localizable.strings @@ -370,8 +370,12 @@ "lightning__channel_point" = "Kanaalpunt"; "lightning__channels_closing_banner" = "KANALEN SLUITEN OP {date}"; "lightning__channels_closing_notice" = "Je Lightning-kanalen worden op {date} gesloten. Boek je bestedingssaldo vóór die datum over naar spaargeld."; -"lightning__channels_unavailable__text" = "Bitkit biedt geen nieuwe Lightning-kanalen meer aan, dus je kunt geen geld van spaargeld naar je bestedingssaldo overboeken."; -"lightning__channels_unavailable__title" = "Nieuwe kanalen niet beschikbaar"; +"lightning__channels_unavailable__button" = "Kanaal openen"; +"lightning__channels_unavailable__text" = "Helaas is de dienst om automatisch geld naar je bestedingssaldo over te boeken niet meer beschikbaar. Je kunt nog steeds handmatig een Lightning-kanaal openen om je bestedingssaldo aan te vullen."; +"lightning__channels_unavailable__title" = "Geen automatische overboeking"; +"lightning__choose_lsp__explanation" = "Als je een QR krijgt om het Lightning-kanaal te verbinden of te claimen, tik dan hieronder om de QR te scannen of te plakken."; +"lightning__choose_lsp__text" = "Bestel een kanaal bij een Lightning Service Provider (LSP). Hieronder staan enkele opties. Bitkit beheert, onderschrijft of ondersteunt deze niet."; +"lightning__choose_lsp__title" = "Kies een\nLightning-aanbieder"; "lightning__closure_reason" = "Reden van sluiting"; "lightning__support" = "Ondersteuning"; "lightning__connection_closed__title" = "Verbinding Gesloten"; diff --git a/Bitkit/Resources/Localization/pl.lproj/Localizable.strings b/Bitkit/Resources/Localization/pl.lproj/Localizable.strings index 9e3375e5b..fff44b5f2 100644 --- a/Bitkit/Resources/Localization/pl.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/pl.lproj/Localizable.strings @@ -372,8 +372,12 @@ "lightning__channel_point" = "Punkt kanału"; "lightning__channels_closing_banner" = "KANAŁY ZOSTANĄ ZAMKNIĘTE {date}"; "lightning__channels_closing_notice" = "Twoje kanały Lightning zostaną zamknięte {date}. Przed tym terminem prześlij saldo wydatków na oszczędności."; -"lightning__channels_unavailable__text" = "Bitkit nie oferuje już nowych kanałów Lightning, więc nie możesz przesyłać środków z oszczędności na wydatki."; -"lightning__channels_unavailable__title" = "Nowe kanały niedostępne"; +"lightning__channels_unavailable__button" = "Otwórz kanał"; +"lightning__channels_unavailable__text" = "Niestety usługa automatycznego przesyłania środków na saldo wydatków nie jest już dostępna. Nadal możesz ręcznie otworzyć kanał Lightning, aby doładować saldo wydatków."; +"lightning__channels_unavailable__title" = "Brak automatycznego przesłania"; +"lightning__choose_lsp__explanation" = "Gdy otrzymasz kod QR do połączenia lub odebrania kanału Lightning, dotknij poniżej, aby zeskanować lub wkleić kod QR."; +"lightning__choose_lsp__text" = "Zamów kanał u dostawcy usług Lightning (LSP). Poniżej kilka opcji. Bitkit ich nie prowadzi, nie poleca ani nie wspiera."; +"lightning__choose_lsp__title" = "Wybierz\ndostawcę Lightning"; "lightning__closure_reason" = "Powód zamknięcia"; "lightning__support" = "Wsparcie"; "lightning__connection_closed__title" = "Połączenie zamknięte"; diff --git a/Bitkit/Resources/Localization/pt-BR.lproj/Localizable.strings b/Bitkit/Resources/Localization/pt-BR.lproj/Localizable.strings index 15832cb0a..2cc16c936 100644 --- a/Bitkit/Resources/Localization/pt-BR.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/pt-BR.lproj/Localizable.strings @@ -372,8 +372,12 @@ "lightning__channel_point" = "Ponto do canal"; "lightning__channels_closing_banner" = "CANAIS FECHAM EM {date}"; "lightning__channels_closing_notice" = "Seus canais Lightning serão fechados em {date}. Transfira seu saldo de gastos para a poupança antes dessa data."; -"lightning__channels_unavailable__text" = "O Bitkit não oferece mais novos canais Lightning, então você não pode transferir fundos da poupança para gastos."; -"lightning__channels_unavailable__title" = "Novos canais indisponíveis"; +"lightning__channels_unavailable__button" = "Abrir canal"; +"lightning__channels_unavailable__text" = "Infelizmente, o serviço de transferência automática de fundos para o seu saldo de gastos não está mais disponível. Você ainda pode abrir manualmente um canal Lightning para recarregar seu saldo de gastos."; +"lightning__channels_unavailable__title" = "Sem transferência automática"; +"lightning__choose_lsp__explanation" = "Quando derem um QR para conectar ou resgatar o canal Lightning, toque abaixo para escanear ou colar o QR."; +"lightning__choose_lsp__text" = "Peça um canal a um provedor de serviços Lightning (LSP). Abaixo estão algumas opções. O Bitkit não opera, endossa nem oferece suporte a elas."; +"lightning__choose_lsp__title" = "Escolha um\nprovedor Lightning"; "lightning__closure_reason" = "Motivo do fechamento"; "lightning__support" = "Suporte"; "lightning__connection_closed__title" = "Conexão Desativada"; diff --git a/Bitkit/Resources/Localization/pt.lproj/Localizable.strings b/Bitkit/Resources/Localization/pt.lproj/Localizable.strings index 53f9ce88f..ff5dbf7f4 100644 --- a/Bitkit/Resources/Localization/pt.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/pt.lproj/Localizable.strings @@ -372,8 +372,12 @@ "lightning__channel_point" = "Ponto do canal"; "lightning__channels_closing_banner" = "CANAIS FECHAM A {date}"; "lightning__channels_closing_notice" = "Os seus canais Lightning serão fechados a {date}. Transfira o seu saldo de gastos para a poupança antes dessa data."; -"lightning__channels_unavailable__text" = "O Bitkit já não oferece novos canais Lightning, por isso não pode transferir fundos da poupança para gastos."; -"lightning__channels_unavailable__title" = "Novos canais indisponíveis"; +"lightning__channels_unavailable__button" = "Abrir canal"; +"lightning__channels_unavailable__text" = "Infelizmente, o serviço de transferência automática de fundos para o seu saldo de gastos já não está disponível. Ainda pode abrir manualmente um canal Lightning para carregar o seu saldo de gastos."; +"lightning__channels_unavailable__title" = "Sem transferência automática"; +"lightning__choose_lsp__explanation" = "Quando lhe derem um QR para ligar ou reclamar o canal Lightning, toque abaixo para digitalizar ou colar o QR."; +"lightning__choose_lsp__text" = "Peça um canal a um fornecedor de serviços Lightning (LSP). Abaixo estão algumas opções. O Bitkit não as gere, recomenda nem suporta."; +"lightning__choose_lsp__title" = "Escolha um\nfornecedor Lightning"; "lightning__closure_reason" = "Motivo do fechamento"; "lightning__support" = "Suporte"; "lightning__connection_closed__title" = "Ligação fechada"; diff --git a/Bitkit/Resources/Localization/ru.lproj/Localizable.strings b/Bitkit/Resources/Localization/ru.lproj/Localizable.strings index 0ae6858cd..138b394f7 100644 --- a/Bitkit/Resources/Localization/ru.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/ru.lproj/Localizable.strings @@ -367,8 +367,12 @@ "lightning__channel_point" = "Точка Канала"; "lightning__channels_closing_banner" = "КАНАЛЫ ЗАКРОЮТСЯ {date}"; "lightning__channels_closing_notice" = "Ваши каналы Lightning будут закрыты {date}. До этой даты переведите баланс расходов в сбережения."; -"lightning__channels_unavailable__text" = "Bitkit больше не открывает новые каналы Lightning, поэтому перевести средства из сбережений в расходы нельзя."; -"lightning__channels_unavailable__title" = "Новые каналы недоступны"; +"lightning__channels_unavailable__button" = "Открыть канал"; +"lightning__channels_unavailable__text" = "К сожалению, сервис автоматического перевода средств на баланс расходов больше недоступен. Вы по-прежнему можете вручную открыть канал Lightning, чтобы пополнить баланс расходов."; +"lightning__channels_unavailable__title" = "Нет автоматического перевода"; +"lightning__choose_lsp__explanation" = "Когда вам дадут QR-код для подключения или получения канала Lightning, нажмите ниже, чтобы отсканировать или вставить QR-код."; +"lightning__choose_lsp__text" = "Закажите канал у провайдера услуг Lightning (LSP). Ниже несколько вариантов. Bitkit не управляет ими, не рекомендует и не поддерживает их."; +"lightning__choose_lsp__title" = "Выберите\nпровайдера Lightning"; "lightning__closure_reason" = "Причина Закрытия"; "lightning__support" = "Поддержка"; "lightning__connection_closed__title" = "Соединение Закрыто"; diff --git a/Bitkit/ViewModels/NavigationViewModel.swift b/Bitkit/ViewModels/NavigationViewModel.swift index 5ee0826a3..4635ff7a1 100644 --- a/Bitkit/ViewModels/NavigationViewModel.swift +++ b/Bitkit/ViewModels/NavigationViewModel.swift @@ -52,6 +52,7 @@ enum Route: Hashable { case transferLearnMore case settingUp case fundingAdvanced + case chooseLsp case fundManual(nodeUri: String?) case fundManualAmount(lnPeer: LnPeer) case fundManualConfirm(lnPeer: LnPeer, amountSats: UInt64) diff --git a/Bitkit/Views/Transfer/ChooseLspView.swift b/Bitkit/Views/Transfer/ChooseLspView.swift new file mode 100644 index 000000000..7cda26aa7 --- /dev/null +++ b/Bitkit/Views/Transfer/ChooseLspView.swift @@ -0,0 +1,89 @@ +import SwiftUI + +/// Lightning service providers that sell a channel to any node, offered now that Blocktank no longer opens new channels. +/// Bitkit does not run or endorse them; each opens in the browser, and the channel is claimed through the scanner or +/// manual setup. +struct LightningProvider: Identifiable { + let name: String + let url: URL + + var id: String { + name + } + + static let all: [LightningProvider] = [ + LightningProvider(name: "LNBIG", url: URL(string: "https://lnbig.com/#/open-channel")!), + LightningProvider(name: "ZEUS Olympus", url: URL(string: "https://channels.zeuslsp.com/")!), + LightningProvider(name: "Megalithic", url: URL(string: "https://megalithic.me/get_a_channel")!), + LightningProvider(name: "Amboss Magma", url: URL(string: "https://amboss.space/magma")!), + ] +} + +struct ChooseLspView: View { + @EnvironmentObject private var navigation: NavigationViewModel + @Environment(\.openURL) private var openURL + + var body: some View { + VStack(spacing: 0) { + NavigationBar(title: t("lightning__funding_advanced__nav_title")) + .padding(.bottom, 16) + + ScrollView(showsIndicators: false) { + VStack(alignment: .leading, spacing: 0) { + DisplayText(t("lightning__choose_lsp__title"), accentColor: .purpleAccent) + .padding(.bottom, 8) + + BodyMText(t("lightning__choose_lsp__text")) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.bottom, 32) + + VStack(spacing: 0) { + ForEach(LightningProvider.all) { provider in + Button { + openURL(provider.url) + } label: { + SettingsRow(title: provider.name) + } + .buttonStyle(.plain) + .accessibilityIdentifier("ChooseLspProvider") + } + } + } + } + + BodySText(t("lightning__choose_lsp__explanation")) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.top, 16) + .padding(.bottom, 16) + + HStack(spacing: 16) { + CustomButton(title: t("lightning__funding_advanced__button2"), variant: .secondary) { + navigation.navigate(.fundManual(nodeUri: nil)) + } + .accessibilityIdentifier("ChooseLspManual") + + CustomButton( + title: t("wallet__receive_show_qr"), + icon: Image("scan") + .resizable() + .frame(width: 16, height: 16) + .foregroundColor(.textPrimary) + ) { + navigation.navigate(.scanner) + } + .accessibilityIdentifier("ChooseLspScan") + } + } + .navigationBarHidden(true) + .padding(.horizontal, 16) + .bottomSafeAreaPadding() + } +} + +#Preview { + NavigationStack { + ChooseLspView() + .environmentObject(NavigationViewModel()) + .preferredColorScheme(.dark) + } +} diff --git a/Bitkit/Views/Wallets/Sheets/ChannelsUnavailableSheet.swift b/Bitkit/Views/Wallets/Sheets/ChannelsUnavailableSheet.swift index 9bb866466..d0c842977 100644 --- a/Bitkit/Views/Wallets/Sheets/ChannelsUnavailableSheet.swift +++ b/Bitkit/Views/Wallets/Sheets/ChannelsUnavailableSheet.swift @@ -5,10 +5,12 @@ struct ChannelsUnavailableSheetItem: SheetItem { let size: SheetSize = .medium } -/// Shown where the app used to start a transfer to spending: Blocktank no longer opens new channels. +/// Shown where the app used to start a transfer to spending: Blocktank no longer opens new channels, so it points to +/// opening one with another provider instead. struct ChannelsUnavailableSheet: View { let config: ChannelsUnavailableSheetItem + @EnvironmentObject private var navigation: NavigationViewModel @EnvironmentObject private var sheets: SheetViewModel @EnvironmentObject private var wallet: WalletViewModel @@ -38,10 +40,18 @@ struct ChannelsUnavailableSheet: View { Spacer() - CustomButton(title: t("common__ok")) { - sheets.hideSheet() + HStack(spacing: 16) { + CustomButton(title: t("common__cancel"), variant: .secondary) { + sheets.hideSheet() + } + .accessibilityIdentifier("ChannelsUnavailableButton") + + CustomButton(title: t("lightning__channels_unavailable__button")) { + sheets.hideSheet() + navigation.navigate(.chooseLsp) + } + .accessibilityIdentifier("ChannelsUnavailableOpenChannel") } - .accessibilityIdentifier("ChannelsUnavailableButton") } .padding(.horizontal, 16) } diff --git a/journeys/README.md b/journeys/README.md index 2baa2361b..770f8663c 100644 --- a/journeys/README.md +++ b/journeys/README.md @@ -114,7 +114,7 @@ wallet however you normally do against regtest and skip the helper. ### Getting a Lightning channel Blocktank is winding down, and this build no longer buys channels from it (synonymdev/bitkit-ios#1097): -Transfer To Spending opens a "New Channels Unavailable" sheet, and the Receive sheet's CJIT button and gift codes are gone. A journey that needs a +Transfer To Spending opens a "No automatic transfer" sheet, and the Receive sheet's CJIT button and gift codes are gone. A journey that needs a Lightning channel as its precondition says "a wallet with an open Lightning channel"; get one in either way: - Install a build from before that change (master before synonymdev/bitkit-ios#1097), open a channel with diff --git a/journeys/amount-limits/README.md b/journeys/amount-limits/README.md index 70eb789a0..f93e106fd 100644 --- a/journeys/amount-limits/README.md +++ b/journeys/amount-limits/README.md @@ -88,6 +88,6 @@ gate. `transfer-spending-over-max.xml` and `transfer-spending-advanced-over-max.xml` were retired because Blocktank channel purchases were removed (synonymdev/bitkit-ios#1097), and so was `../hardware-wallet/transfer-to-spending-over-max.xml`. Their routes started at the Savings wallet's -"Transfer To Spending" button or the hardware wallet's, which now open the "New Channels Unavailable" +"Transfer To Spending" button or the hardware wallet's, which now open the "No automatic transfer" sheet. The amount screens' identifiers (`SpendingAmount*`, `SpendingAdvanced*`, `HardwareTransferAmount*`) are no longer reachable from the UI. diff --git a/journeys/channels-closing/README.md b/journeys/channels-closing/README.md index 749f47667..40f30976f 100644 --- a/journeys/channels-closing/README.md +++ b/journeys/channels-closing/README.md @@ -15,11 +15,17 @@ The Android twin is synonymdev/bitkit-android#1669. same Blocktank-channel condition. - The date is a placeholder constant (Dec 15, 2026) until the shutdown date is final. Journeys assert the banner and the notice agree and read the date from the screen instead of hardcoding it. -- **Channels unavailable sheet** `ChannelsUnavailableSheet`, "New Channels Unavailable": Savings - "Transfer To Spending" (`TransferToSpending`), Spending "Transfer from Savings" (`TransferFromSavings`) - and hardware wallet "Transfer To Spending" (`HardwareTransferToSpending`) keep their visibility rules - and open it instead of the transfer flow. For a wallet with a Blocktank channel it adds the close date - (`ChannelsUnavailableCloseDate`); "OK" (`ChannelsUnavailableButton`) closes it. +- **"No automatic transfer" sheet** `ChannelsUnavailableSheet`: Savings "Transfer To Spending" + (`TransferToSpending`), Spending "Transfer from Savings" (`TransferFromSavings`) and hardware wallet + "Transfer To Spending" (`HardwareTransferToSpending`) keep their visibility rules and open it instead of + the transfer flow. For a wallet with a Blocktank channel it adds the close date + (`ChannelsUnavailableCloseDate`). "Cancel" (`ChannelsUnavailableButton`) closes it; "Open Channel" + (`ChannelsUnavailableOpenChannel`) opens Choose LSP. +- **Choose LSP** (Figma "Third-party LSP › Choose LSP"): four third-party providers (`ChooseLspProvider`), + LNBIG, ZEUS Olympus, Megalithic and Amboss Magma, each opening its channel order page in the browser, + plus "Manual Setup" (`ChooseLspManual`, the existing manual channel setup) and "QR Code" + (`ChooseLspScan`, the existing scanner). Lightning Connections › add connection still opens Funding + Advanced. - **Removed:** the Home "Spend" suggestion card (`Suggestion-lightning`), the Receive sheet's CJIT button and Spending onboarding, the Edit Invoice CJIT offer, the TabBar Receive shortcut into the CJIT amount screen and gift codes. Transfer To Savings, send, receive over an existing channel, LNURL channel and manual @@ -37,6 +43,7 @@ The Android twin is synonymdev/bitkit-android#1669. | Journey | Covers | | --- | --- | | `channels-closing-notice.xml` | Banner on Home → tap → Spending wallet notice and Transfer To Savings; Savings Transfer To Spending opens the channels unavailable sheet with the close date | +| `choose-lsp-from-transfer.xml` | Transfer To Spending → sheet → Open Channel → Choose LSP: provider rows open the browser, QR Code opens the scanner, Manual Setup opens the manual channel setup | | [`../receive/receive-onchain-only-without-channel.xml`](../receive/receive-onchain-only-without-channel.xml) | The opposite precondition: a funded wallet with no channels shows no banner, no Spend card and a Savings-only Receive sheet; the transfer buttons open the sheet without the close date | ## Identifiers used @@ -44,4 +51,5 @@ The Android twin is synonymdev/bitkit-android#1669. `ChannelsClosingBanner`, `ChannelsClosingNotice`, `ActivitySavings`, `ActivitySpending`, `TotalBalance-primary` (Android `TotalBalance`), `TransferToSavings`, `NavigationBack`, `TransferToSpending`, `TransferFromSavings`, `HardwareTransferToSpending`, `ChannelsUnavailableSheet`, -`ChannelsUnavailableCloseDate`, `ChannelsUnavailableButton`. +`ChannelsUnavailableCloseDate`, `ChannelsUnavailableButton`, `ChannelsUnavailableOpenChannel`, `ChooseLspProvider`, +`ChooseLspManual`, `ChooseLspScan`, `NodeIdInput`, `HostInput`, `PortInput`. diff --git a/journeys/channels-closing/channels-closing-notice.xml b/journeys/channels-closing/channels-closing-notice.xml index 9c48ca11b..4abb11efc 100644 --- a/journeys/channels-closing/channels-closing-notice.xml +++ b/journeys/channels-closing/channels-closing-notice.xml @@ -6,7 +6,7 @@ wallet, which shows "Your Lightning channels will be closed on Dec 15, 2026. Move your spending balance to savings before then." (id "ChannelsClosingNotice") under the balance header, and Transfer To Savings (id "TransferToSavings") is still offered. The Savings wallet's - Transfer To Spending button (id "TransferToSpending") opens the "New Channels Unavailable" sheet + Transfer To Spending button (id "TransferToSpending") opens the "No automatic transfer" sheet (id "ChannelsUnavailableSheet") instead of the transfer flow, since new channels can no longer be bought; for this wallet the sheet also shows the close date (id "ChannelsUnavailableCloseDate"). The date is a placeholder constant until the shutdown date is final; assert whatever date the build shows, and keep the banner and the notice in agreement. @@ -29,8 +29,8 @@ Verify the "Transfer To Savings" button (id "TransferToSavings") is visible Press the system back button, then tap the Savings balance card (id "ActivitySavings") Verify the Savings wallet screen is open, then tap "Transfer To Spending" (id "TransferToSpending") - Verify the "New Channels Unavailable" sheet (id "ChannelsUnavailableSheet") opens with "Bitkit no longer offers new Lightning channels, so you can't move funds from savings to spending." and the close date paragraph (id "ChannelsUnavailableCloseDate") with the same date the banner showed, and no transfer amount screen opens - Tap "OK" (id "ChannelsUnavailableButton") and verify the sheet closes and the Savings wallet screen is still shown + Verify the sheet (id "ChannelsUnavailableSheet") opens with the title "No automatic transfer" and the text "Unfortunately, the service to automatically transfer funds to your spending balance is no longer available. You can still manually open a Lightning channel to top up your spending balance.", and that the line (id "ChannelsUnavailableCloseDate") reads "Your Lightning channels will be closed on Dec 15, 2026. Move your spending balance to savings before then." with the same date the banner showed + Tap "Cancel" (id "ChannelsUnavailableButton") and verify the sheet closes and the Savings wallet screen is still shown Press the system back button, tap the Spending balance card (id "ActivitySpending") and verify the notice (id "ChannelsClosingNotice") is still shown diff --git a/journeys/channels-closing/choose-lsp-from-transfer.xml b/journeys/channels-closing/choose-lsp-from-transfer.xml new file mode 100644 index 000000000..758fccdd3 --- /dev/null +++ b/journeys/channels-closing/choose-lsp-from-transfer.xml @@ -0,0 +1,27 @@ + + + Verifies the way to a channel that is left after Blocktank channel purchases were removed + (synonymdev/bitkit-ios#1097). Transfer To Spending opens the "No automatic transfer" sheet, whose + "Open Channel" button (id "ChannelsUnavailableOpenChannel") opens the "Choose a Lightning provider" + screen. That screen lists third-party providers (id "ChooseLspProvider"), and each row opens the + provider's own channel order page in the browser. Its footer has "Manual Setup" (testTag + "ChooseLspManual"), which opens the manual channel setup with an empty node, and "QR Code" (testTag + "ChooseLspScan"), which opens the scanner for the provider's LNURL channel or node QR. No step creates a + Blocktank order. The design (Figma, Bitkit - Final Review v63 › Third-party LSP › Choose LSP) shows the + provider page embedded in the app; this build opens it in the browser instead. + + iOS: the browser is Safari; return to Bitkit with the app switcher or the status bar back link. + + Precondition: onboarded dev wallet with a positive on-chain Savings balance, so the Savings wallet shows + "Transfer To Spending". A channel is not needed. Start on the wallet home screen. + + + Tap the Savings balance card (id "ActivitySavings"), then "Transfer To Spending" (id "TransferToSpending") + Verify the sheet (id "ChannelsUnavailableSheet") shows the title "No automatic transfer", then tap "Open Channel" (id "ChannelsUnavailableOpenChannel") + Verify the screen titled "Spending Balance" shows "CHOOSE A LIGHTNING PROVIDER", the text "Order a channel from a Lightning Service Provider (LSP). Below are some options. Bitkit does not run, endorse, or support these." and four provider rows (id "ChooseLspProvider"): "LNBIG", "ZEUS Olympus", "Megalithic" and "Amboss Magma" + Verify the explanation "When they give you a QR to connect or claim the Lightning channel, tap below to scan or paste the QR." and the buttons "Manual Setup" (id "ChooseLspManual") and "QR Code" (id "ChooseLspScan") are visible + Tap "Megalithic" and verify the browser opens https://megalithic.me/get_a_channel, then return to Bitkit and verify the provider screen is still shown + Tap "QR Code" (id "ChooseLspScan") and verify the scanner sheet opens, then close it + Tap "Manual Setup" (id "ChooseLspManual") and verify the manual connection screen shows empty node id (id "NodeIdInput"), host (id "HostInput") and port (id "PortInput") fields + + diff --git a/journeys/hardware-wallet/README.md b/journeys/hardware-wallet/README.md index ef87083d5..92cc5963b 100644 --- a/journeys/hardware-wallet/README.md +++ b/journeys/hardware-wallet/README.md @@ -143,7 +143,7 @@ Walked as far as a simulator allows, against a wallet with two Trezor identities `transfer-to-spending-node-warmup.xml`, `passphrase-transfer-to-spending.xml` and the iOS-only `transfer-to-spending-over-max.xml` were retired because Blocktank channel purchases were removed (synonymdev/bitkit-ios#1097): the hardware wallet's Transfer To Spending button (`HardwareTransferToSpending`) now -opens the "New Channels Unavailable" sheet. The `HardwareTransfer*` and `HwTransferPassphrase*` screens +opens the "No automatic transfer" sheet. The `HardwareTransfer*` and `HwTransferPassphrase*` screens and identifiers are still in code but nothing in the UI reaches them. ## Local Payment Completion diff --git a/journeys/hardware-wallet/detail-overview.xml b/journeys/hardware-wallet/detail-overview.xml index 3810049c7..1b1c4a722 100644 --- a/journeys/hardware-wallet/detail-overview.xml +++ b/journeys/hardware-wallet/detail-overview.xml @@ -2,7 +2,7 @@ Opens the hardware wallet detail screen from the home tile and verifies its overview: the top bar, balance header, the Transfer To Spending entry, which opens the - "New Channels Unavailable" sheet since Blocktank channel purchases were removed (synonymdev/bitkit-ios#1097), the grouped activity list, and the Remove device confirm dialog. The final + "No automatic transfer" sheet since Blocktank channel purchases were removed (synonymdev/bitkit-ios#1097), the grouped activity list, and the Remove device confirm dialog. The final Remove step forgets the device, so run this last (re-run connect-home-tile.xml to pair again). Requires a paired Bridge emulator (run connect-home-tile.xml first). @@ -10,7 +10,7 @@ Launch the Bitkit app and go to the wallet home screen Tap the hardware wallet tile (id "ActivityHardware") beneath the SAVINGS and SPENDING tiles Verify the hardware wallet detail screen opens (id "HardwareWalletScreen"), showing the device name with a blue bitcoin icon in the top bar and a balance header - If a "Transfer To Spending" button is shown (id "HardwareTransferToSpending"), tap it, verify the "New Channels Unavailable" sheet opens (id "ChannelsUnavailableSheet") and no transfer amount screen opens, then tap "OK" (id "ChannelsUnavailableButton"); otherwise skip this step + If a "Transfer To Spending" button is shown (id "HardwareTransferToSpending"), tap it, verify the "No automatic transfer" sheet opens (id "ChannelsUnavailableSheet") and no transfer amount screen opens, then tap "Cancel" (id "ChannelsUnavailableButton"); otherwise skip this step If the activity list shows transactions, verify their circular icons are blue, then tap the first one, verify an activity detail screen opens, and navigate back Tap the "Remove" button labelled with the device name near the bottom of the screen (id "RemoveHardwareWallet") Verify a confirm dialog appears (id "RemoveHwWalletDialog") explaining that your funds are safe and your coins won't be deleted diff --git a/journeys/receive/receive-onchain-only-without-channel.xml b/journeys/receive/receive-onchain-only-without-channel.xml index 548766f56..5b29b4dac 100644 --- a/journeys/receive/receive-onchain-only-without-channel.xml +++ b/journeys/receive/receive-onchain-only-without-channel.xml @@ -27,7 +27,8 @@ Tap "Edit" (id "SpecifyInvoiceButton"), tap the amount field (id "ReceiveNumberPadTextField"), enter a small amount (for example "N1" then "N0" twice), tap Continue (id "ReceiveNumberPadSubmit") and then "QR Code" (id "ShowQrReceive") Verify the sheet returns to the QR screen with no liquidity or channel confirm screen in between, the Savings tab is still selected and there is still no Spending tab (id "Tab-spending") Close the sheet, open the Savings wallet (id "ActivitySavings") and tap "Transfer To Spending" (id "TransferToSpending") - Verify the "New Channels Unavailable" sheet (id "ChannelsUnavailableSheet") opens with its text and without the close date paragraph (id "ChannelsUnavailableCloseDate"), then tap "OK" (id "ChannelsUnavailableButton") - Open the Spending wallet (id "ActivitySpending"), verify no notice (id "ChannelsClosingNotice") is shown, tap "Transfer from Savings" (id "TransferFromSavings") and verify the same sheet opens, then tap "OK" + Verify the sheet (id "ChannelsUnavailableSheet") shows the title "No automatic transfer" and the text "Unfortunately, the service to automatically transfer funds to your spending balance is no longer available. You can still manually open a Lightning channel to top up your spending balance.", and no close date line (id "ChannelsUnavailableCloseDate"); tap "Cancel" (id "ChannelsUnavailableButton") and verify the Savings wallet screen is still shown + Open the Spending wallet (id "ActivitySpending"), verify no notice (id "ChannelsClosingNotice") is shown, and tap "Transfer from Savings" (id "TransferFromSavings") + Verify the same sheet (id "ChannelsUnavailableSheet") opens with no close date line (id "ChannelsUnavailableCloseDate"), tap "Cancel" (id "ChannelsUnavailableButton") and verify the Spending wallet screen is still shown From a659cb6814be01e144a0a0c0aa8ece1a4be396c3 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Thu, 22 Oct 2026 13:36:01 +0200 Subject: [PATCH 05/12] feat: follow the transfer unavailable design for transfers to spending The sheet becomes Transfer Unavailable (manual setup required) with Cancel and Continue to Choose LSP, per the final design; the close date stays on the banner and Spending notice. --- Bitkit/Components/SheetIntro.swift | 1 + .../Localization/ar.lproj/Localizable.strings | 6 +- .../Localization/ca.lproj/Localizable.strings | 6 +- .../Localization/cs.lproj/Localizable.strings | 6 +- .../Localization/de.lproj/Localizable.strings | 6 +- .../Localization/el.lproj/Localizable.strings | 6 +- .../Localization/en.lproj/Localizable.strings | 6 +- .../es-419.lproj/Localizable.strings | 6 +- .../Localization/es.lproj/Localizable.strings | 6 +- .../Localization/fr.lproj/Localizable.strings | 6 +- .../Localization/it.lproj/Localizable.strings | 6 +- .../Localization/nl.lproj/Localizable.strings | 6 +- .../Localization/pl.lproj/Localizable.strings | 6 +- .../pt-BR.lproj/Localizable.strings | 6 +- .../Localization/pt.lproj/Localizable.strings | 6 +- .../Localization/ru.lproj/Localizable.strings | 6 +- .../Sheets/ChannelsUnavailableSheet.swift | 56 +++++-------------- journeys/README.md | 2 +- journeys/amount-limits/README.md | 2 +- journeys/channels-closing/README.md | 16 +++--- .../channels-closing-notice.xml | 8 +-- .../choose-lsp-from-transfer.xml | 6 +- journeys/hardware-wallet/README.md | 2 +- journeys/hardware-wallet/detail-overview.xml | 4 +- .../receive-onchain-only-without-channel.xml | 4 +- 25 files changed, 83 insertions(+), 108 deletions(-) diff --git a/Bitkit/Components/SheetIntro.swift b/Bitkit/Components/SheetIntro.swift index f4b8b4eac..d58448c4f 100644 --- a/Bitkit/Components/SheetIntro.swift +++ b/Bitkit/Components/SheetIntro.swift @@ -66,6 +66,7 @@ struct SheetIntro: View { DisplayText(title, accentColor: accentColor) .padding(.bottom, 14) .frame(maxWidth: .infinity, alignment: .leading) + .accessibilityIdentifier("\(baseTestID)Title") BodyMText(description, accentFont: accentFont) .frame(maxWidth: .infinity, alignment: .leading) diff --git a/Bitkit/Resources/Localization/ar.lproj/Localizable.strings b/Bitkit/Resources/Localization/ar.lproj/Localizable.strings index 5ca67a015..f2deae204 100644 --- a/Bitkit/Resources/Localization/ar.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/ar.lproj/Localizable.strings @@ -319,9 +319,9 @@ "lightning__channel_point" = "نقطة القناة"; "lightning__channels_closing_banner" = "تُغلق القنوات في {date}"; "lightning__channels_closing_notice" = "ستُغلق قنوات Lightning الخاصة بك في {date}. انقل رصيد الإنفاق إلى المدخرات قبل ذلك."; -"lightning__channels_unavailable__button" = "فتح قناة"; -"lightning__channels_unavailable__text" = "للأسف، لم تعد خدمة التحويل التلقائي للأموال إلى رصيد الإنفاق متاحة. لا يزال بإمكانك فتح قناة Lightning يدويًا لتعبئة رصيد الإنفاق."; -"lightning__channels_unavailable__title" = "لا يوجد تحويل تلقائي"; +"lightning__channels_unavailable__nav_title" = "التحويل غير متاح"; +"lightning__channels_unavailable__text" = "لم تعد التحويلات التلقائية إلى رصيد الإنفاق متاحة. استخدم عقدتك الخاصة، أو أنشئ اتصالًا مع مزوّد آخر."; +"lightning__channels_unavailable__title" = "الإعداد اليدوي\nمطلوب"; "lightning__choose_lsp__explanation" = "عندما يقدّمون لك رمز QR للاتصال بقناة Lightning أو المطالبة بها، اضغط أدناه لمسح رمز QR أو لصقه."; "lightning__choose_lsp__text" = "اطلب قناة من مزوّد خدمة Lightning ‏(LSP). فيما يلي بعض الخيارات. لا يدير Bitkit هذه الخدمات ولا يؤيدها ولا يدعمها."; "lightning__choose_lsp__title" = "اختر\nمزوّد Lightning"; diff --git a/Bitkit/Resources/Localization/ca.lproj/Localizable.strings b/Bitkit/Resources/Localization/ca.lproj/Localizable.strings index 50c23eb43..285eff43d 100644 --- a/Bitkit/Resources/Localization/ca.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/ca.lproj/Localizable.strings @@ -356,9 +356,9 @@ "lightning__channel_point" = "Punt del canal"; "lightning__channels_closing_banner" = "ELS CANALS ES TANQUEN EL {date}"; "lightning__channels_closing_notice" = "Els teus canals Lightning es tancaran el {date}. Transfereix el teu saldo de despesa a estalvis abans d'aquesta data."; -"lightning__channels_unavailable__button" = "Obrir canal"; -"lightning__channels_unavailable__text" = "Malauradament, el servei per transferir fons automàticament al teu saldo de despesa ja no està disponible. Encara pots obrir manualment un canal Lightning per recarregar el teu saldo de despesa."; -"lightning__channels_unavailable__title" = "Sense transferència automàtica"; +"lightning__channels_unavailable__nav_title" = "Transferència no disponible"; +"lightning__channels_unavailable__text" = "Les transferències automàtiques al teu saldo de despesa ja no estan disponibles. Fes servir el teu propi node o configura una connexió amb un altre proveïdor."; +"lightning__channels_unavailable__title" = "Configuració manual\nnecessària"; "lightning__choose_lsp__explanation" = "Quan et donin un QR per connectar o reclamar el canal Lightning, toca a sota per escanejar o enganxar el QR."; "lightning__choose_lsp__text" = "Demana un canal a un proveïdor de serveis Lightning (LSP). A sota hi ha algunes opcions. Bitkit no les gestiona, avala ni hi dona suport."; "lightning__choose_lsp__title" = "Tria un\nproveïdor Lightning"; diff --git a/Bitkit/Resources/Localization/cs.lproj/Localizable.strings b/Bitkit/Resources/Localization/cs.lproj/Localizable.strings index 03d287c31..2111305cf 100644 --- a/Bitkit/Resources/Localization/cs.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/cs.lproj/Localizable.strings @@ -372,9 +372,9 @@ "lightning__channel_point" = "Bod kanálu"; "lightning__channels_closing_banner" = "KANÁLY SE ZAVŘOU {date}"; "lightning__channels_closing_notice" = "Vaše Lightning kanály budou uzavřeny {date}. Do té doby převeďte zůstatek útrat do úspor."; -"lightning__channels_unavailable__button" = "Otevřít kanál"; -"lightning__channels_unavailable__text" = "Služba pro automatický převod prostředků na zůstatek útrat bohužel již není k dispozici. Stále můžete ručně otevřít Lightning kanál a doplnit si zůstatek útrat."; -"lightning__channels_unavailable__title" = "Žádný automatický převod"; +"lightning__channels_unavailable__nav_title" = "Převod není dostupný"; +"lightning__channels_unavailable__text" = "Automatické převody na zůstatek útrat již nejsou k dispozici. Použijte vlastní uzel nebo si nastavte připojení u jiného poskytovatele."; +"lightning__channels_unavailable__title" = "Vyžadováno ruční\nnastavení"; "lightning__choose_lsp__explanation" = "Až vám dají QR kód pro připojení nebo převzetí Lightning kanálu, klepněte níže a QR kód naskenujte nebo vložte."; "lightning__choose_lsp__text" = "Objednejte si kanál u poskytovatele služeb Lightning (LSP). Níže je několik možností. Bitkit je neprovozuje, nedoporučuje ani nepodporuje."; "lightning__choose_lsp__title" = "Vyberte\nposkytovatele Lightning"; diff --git a/Bitkit/Resources/Localization/de.lproj/Localizable.strings b/Bitkit/Resources/Localization/de.lproj/Localizable.strings index de657351b..d380b71ae 100644 --- a/Bitkit/Resources/Localization/de.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/de.lproj/Localizable.strings @@ -370,9 +370,9 @@ "lightning__channel_point" = "Kanalpunkt"; "lightning__channels_closing_banner" = "KANÄLE SCHLIESSEN AM {date}"; "lightning__channels_closing_notice" = "Deine Lightning-Kanäle werden am {date} geschlossen. Übertrage dein Guthaben vorher auf das Sparkonto."; -"lightning__channels_unavailable__button" = "Kanal öffnen"; -"lightning__channels_unavailable__text" = "Leider ist der Dienst, der Guthaben automatisch auf dein Ausgabenkonto überträgt, nicht mehr verfügbar. Du kannst weiterhin manuell einen Lightning-Kanal öffnen, um dein Ausgabenkonto aufzuladen."; -"lightning__channels_unavailable__title" = "Keine automatische Übertragung"; +"lightning__channels_unavailable__nav_title" = "Übertragung nicht verfügbar"; +"lightning__channels_unavailable__text" = "Automatische Übertragungen auf dein Ausgabenkonto sind nicht mehr verfügbar. Nutze deinen eigenen Node oder richte eine Verbindung mit einem anderen Anbieter ein."; +"lightning__channels_unavailable__title" = "Manuelle Einrichtung\nerforderlich"; "lightning__choose_lsp__explanation" = "Wenn du einen QR-Code zum Verbinden oder Einlösen des Lightning-Kanals erhältst, tippe unten, um ihn zu scannen oder einzufügen."; "lightning__choose_lsp__text" = "Bestelle einen Kanal bei einem Lightning Service Provider (LSP). Unten findest du einige Optionen. Bitkit betreibt, empfiehlt oder unterstützt diese nicht."; "lightning__choose_lsp__title" = "Wähle einen\nLightning-Anbieter"; diff --git a/Bitkit/Resources/Localization/el.lproj/Localizable.strings b/Bitkit/Resources/Localization/el.lproj/Localizable.strings index c0e76d461..94eecb983 100644 --- a/Bitkit/Resources/Localization/el.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/el.lproj/Localizable.strings @@ -343,9 +343,9 @@ "lightning__channel_point" = "Channel point"; "lightning__channels_closing_banner" = "ΤΑ ΚΑΝΑΛΙΑ ΚΛΕΙΝΟΥΝ ΣΤΙΣ {date}"; "lightning__channels_closing_notice" = "Τα κανάλια Lightning σας θα κλείσουν στις {date}. Μεταφέρετε το υπόλοιπο δαπανών σε αποταμιεύσεις πριν από τότε."; -"lightning__channels_unavailable__button" = "Άνοιγμα καναλιού"; -"lightning__channels_unavailable__text" = "Δυστυχώς, η υπηρεσία αυτόματης μεταφοράς κεφαλαίων στο υπόλοιπο δαπανών δεν είναι πλέον διαθέσιμη. Μπορείτε ακόμα να ανοίξετε χειροκίνητα ένα κανάλι Lightning για να αναπληρώσετε το υπόλοιπο δαπανών."; -"lightning__channels_unavailable__title" = "Χωρίς αυτόματη μεταφορά"; +"lightning__channels_unavailable__nav_title" = "Μεταφορά μη διαθέσιμη"; +"lightning__channels_unavailable__text" = "Οι αυτόματες μεταφορές στο υπόλοιπο δαπανών δεν είναι πλέον διαθέσιμες. Χρησιμοποιήστε τον δικό σας κόμβο ή ρυθμίστε σύνδεση με άλλον πάροχο."; +"lightning__channels_unavailable__title" = "Απαιτείται χειροκίνητη\nρύθμιση"; "lightning__choose_lsp__explanation" = "Όταν σας δώσουν ένα QR για σύνδεση ή διεκδίκηση του καναλιού Lightning, πατήστε παρακάτω για σάρωση ή επικόλληση του QR."; "lightning__choose_lsp__text" = "Παραγγείλετε ένα κανάλι από έναν πάροχο υπηρεσιών Lightning (LSP). Παρακάτω υπάρχουν μερικές επιλογές. Το Bitkit δεν τις λειτουργεί, δεν τις προτείνει και δεν τις υποστηρίζει."; "lightning__choose_lsp__title" = "Επιλέξτε\nπάροχο Lightning"; diff --git a/Bitkit/Resources/Localization/en.lproj/Localizable.strings b/Bitkit/Resources/Localization/en.lproj/Localizable.strings index 8168c36d4..2a5f1da63 100644 --- a/Bitkit/Resources/Localization/en.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/en.lproj/Localizable.strings @@ -365,9 +365,9 @@ "lightning__channel_point" = "Channel point"; "lightning__channels_closing_banner" = "CHANNELS CLOSE ON {date}"; "lightning__channels_closing_notice" = "Your Lightning channels will be closed on {date}. Move your spending balance to savings before then."; -"lightning__channels_unavailable__button" = "Open Channel"; -"lightning__channels_unavailable__text" = "Unfortunately, the service to automatically transfer funds to your spending balance is no longer available. You can still manually open a Lightning channel to top up your spending balance."; -"lightning__channels_unavailable__title" = "No automatic transfer"; +"lightning__channels_unavailable__nav_title" = "Transfer Unavailable"; +"lightning__channels_unavailable__text" = "Automatic transfers to your spending balance are no longer available. Use your own node, or set up a connection with another provider."; +"lightning__channels_unavailable__title" = "Manual setup\nrequired"; "lightning__choose_lsp__explanation" = "When they give you a QR to connect or claim the Lightning channel, tap below to scan or paste the QR."; "lightning__choose_lsp__text" = "Order a channel from a Lightning Service Provider (LSP). Below are some options. Bitkit does not run, endorse, or support these."; "lightning__choose_lsp__title" = "Choose a\nLightning provider"; diff --git a/Bitkit/Resources/Localization/es-419.lproj/Localizable.strings b/Bitkit/Resources/Localization/es-419.lproj/Localizable.strings index ea824e160..2d3836338 100644 --- a/Bitkit/Resources/Localization/es-419.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/es-419.lproj/Localizable.strings @@ -373,9 +373,9 @@ "lightning__channel_point" = "Punto del canal"; "lightning__channels_closing_banner" = "LOS CANALES CIERRAN EL {date}"; "lightning__channels_closing_notice" = "Tus canales Lightning se cerrarán el {date}. Transfiere tu saldo de gasto a ahorros antes de esa fecha."; -"lightning__channels_unavailable__button" = "Abrir canal"; -"lightning__channels_unavailable__text" = "Lamentablemente, el servicio para transferir fondos automáticamente a tu saldo de gasto ya no está disponible. Aún puedes abrir manualmente un canal Lightning para recargar tu saldo de gasto."; -"lightning__channels_unavailable__title" = "Sin transferencia automática"; +"lightning__channels_unavailable__nav_title" = "Transferencia no disponible"; +"lightning__channels_unavailable__text" = "Las transferencias automáticas a tu saldo de gasto ya no están disponibles. Usa tu propio nodo o configura una conexión con otro proveedor."; +"lightning__channels_unavailable__title" = "Configuración manual\nnecesaria"; "lightning__choose_lsp__explanation" = "Cuando te den un QR para conectar o reclamar el canal Lightning, toca abajo para escanear o pegar el QR."; "lightning__choose_lsp__text" = "Pide un canal a un proveedor de servicios Lightning (LSP). Abajo hay algunas opciones. Bitkit no las opera, respalda ni brinda soporte."; "lightning__choose_lsp__title" = "Elige un\nproveedor Lightning"; diff --git a/Bitkit/Resources/Localization/es.lproj/Localizable.strings b/Bitkit/Resources/Localization/es.lproj/Localizable.strings index acdcb7b4e..4b0be72b0 100644 --- a/Bitkit/Resources/Localization/es.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/es.lproj/Localizable.strings @@ -357,9 +357,9 @@ "lightning__channel_point" = "Punto del canal"; "lightning__channels_closing_banner" = "LOS CANALES CIERRAN EL {date}"; "lightning__channels_closing_notice" = "Tus canales Lightning se cerrarán el {date}. Transfiere tu saldo de gasto a ahorros antes de esa fecha."; -"lightning__channels_unavailable__button" = "Abrir canal"; -"lightning__channels_unavailable__text" = "Lamentablemente, el servicio para transferir fondos automáticamente a tu saldo de gasto ya no está disponible. Aún puedes abrir manualmente un canal Lightning para recargar tu saldo de gasto."; -"lightning__channels_unavailable__title" = "Sin transferencia automática"; +"lightning__channels_unavailable__nav_title" = "Transferencia no disponible"; +"lightning__channels_unavailable__text" = "Las transferencias automáticas a tu saldo de gasto ya no están disponibles. Usa tu propio nodo o configura una conexión con otro proveedor."; +"lightning__channels_unavailable__title" = "Configuración manual\nnecesaria"; "lightning__choose_lsp__explanation" = "Cuando te den un QR para conectar o reclamar el canal Lightning, toca abajo para escanear o pegar el QR."; "lightning__choose_lsp__text" = "Pide un canal a un proveedor de servicios Lightning (LSP). Abajo tienes algunas opciones. Bitkit no las gestiona, respalda ni da soporte."; "lightning__choose_lsp__title" = "Elige un\nproveedor Lightning"; diff --git a/Bitkit/Resources/Localization/fr.lproj/Localizable.strings b/Bitkit/Resources/Localization/fr.lproj/Localizable.strings index b89474cd2..b22db9a47 100644 --- a/Bitkit/Resources/Localization/fr.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/fr.lproj/Localizable.strings @@ -373,9 +373,9 @@ "lightning__channel_point" = "Point du canal"; "lightning__channels_closing_banner" = "LES CANAUX FERMENT LE {date}"; "lightning__channels_closing_notice" = "Vos canaux Lightning seront fermés le {date}. Transférez votre solde de dépenses vers l'Épargne avant cette date."; -"lightning__channels_unavailable__button" = "Ouvrir un canal"; -"lightning__channels_unavailable__text" = "Malheureusement, le service de transfert automatique de fonds vers votre solde de dépenses n'est plus disponible. Vous pouvez toujours ouvrir manuellement un canal Lightning pour recharger votre solde de dépenses."; -"lightning__channels_unavailable__title" = "Pas de transfert automatique"; +"lightning__channels_unavailable__nav_title" = "Transfert indisponible"; +"lightning__channels_unavailable__text" = "Les transferts automatiques vers votre solde de dépenses ne sont plus disponibles. Utilisez votre propre nœud ou configurez une connexion avec un autre fournisseur."; +"lightning__channels_unavailable__title" = "Configuration manuelle\nrequise"; "lightning__choose_lsp__explanation" = "Quand ils vous donnent un QR pour connecter ou réclamer le canal Lightning, touchez ci-dessous pour scanner ou coller le QR."; "lightning__choose_lsp__text" = "Commandez un canal auprès d'un fournisseur de services Lightning (LSP). Voici quelques options. Bitkit ne les exploite pas, ne les cautionne pas et ne les prend pas en charge."; "lightning__choose_lsp__title" = "Choisissez un\nfournisseur Lightning"; diff --git a/Bitkit/Resources/Localization/it.lproj/Localizable.strings b/Bitkit/Resources/Localization/it.lproj/Localizable.strings index c33222406..a657b4306 100644 --- a/Bitkit/Resources/Localization/it.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/it.lproj/Localizable.strings @@ -366,9 +366,9 @@ "lightning__channel_point" = "Punto canale"; "lightning__channels_closing_banner" = "I CANALI CHIUDONO IL {date}"; "lightning__channels_closing_notice" = "I tuoi canali Lightning verranno chiusi il {date}. Trasferisci il saldo di spesa ai risparmi prima di allora."; -"lightning__channels_unavailable__button" = "Apri canale"; -"lightning__channels_unavailable__text" = "Purtroppo il servizio per trasferire automaticamente fondi al saldo di spesa non è più disponibile. Puoi ancora aprire manualmente un canale Lightning per ricaricare il saldo di spesa."; -"lightning__channels_unavailable__title" = "Nessun trasferimento automatico"; +"lightning__channels_unavailable__nav_title" = "Trasferimento non disponibile"; +"lightning__channels_unavailable__text" = "I trasferimenti automatici al saldo di spesa non sono più disponibili. Usa il tuo nodo o configura una connessione con un altro fornitore."; +"lightning__channels_unavailable__title" = "Configurazione manuale\nrichiesta"; "lightning__choose_lsp__explanation" = "Quando ti danno un QR per collegare o riscattare il canale Lightning, tocca qui sotto per scansionare o incollare il QR."; "lightning__choose_lsp__text" = "Ordina un canale da un fornitore di servizi Lightning (LSP). Qui sotto trovi alcune opzioni. Bitkit non le gestisce, non le approva e non le supporta."; "lightning__choose_lsp__title" = "Scegli un\nfornitore Lightning"; diff --git a/Bitkit/Resources/Localization/nl.lproj/Localizable.strings b/Bitkit/Resources/Localization/nl.lproj/Localizable.strings index b1dad2708..c4afa937b 100644 --- a/Bitkit/Resources/Localization/nl.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/nl.lproj/Localizable.strings @@ -370,9 +370,9 @@ "lightning__channel_point" = "Kanaalpunt"; "lightning__channels_closing_banner" = "KANALEN SLUITEN OP {date}"; "lightning__channels_closing_notice" = "Je Lightning-kanalen worden op {date} gesloten. Boek je bestedingssaldo vóór die datum over naar spaargeld."; -"lightning__channels_unavailable__button" = "Kanaal openen"; -"lightning__channels_unavailable__text" = "Helaas is de dienst om automatisch geld naar je bestedingssaldo over te boeken niet meer beschikbaar. Je kunt nog steeds handmatig een Lightning-kanaal openen om je bestedingssaldo aan te vullen."; -"lightning__channels_unavailable__title" = "Geen automatische overboeking"; +"lightning__channels_unavailable__nav_title" = "Overboeking niet beschikbaar"; +"lightning__channels_unavailable__text" = "Automatische overboekingen naar je bestedingssaldo zijn niet meer beschikbaar. Gebruik je eigen node of stel een verbinding in met een andere aanbieder."; +"lightning__channels_unavailable__title" = "Handmatige instelling\nvereist"; "lightning__choose_lsp__explanation" = "Als je een QR krijgt om het Lightning-kanaal te verbinden of te claimen, tik dan hieronder om de QR te scannen of te plakken."; "lightning__choose_lsp__text" = "Bestel een kanaal bij een Lightning Service Provider (LSP). Hieronder staan enkele opties. Bitkit beheert, onderschrijft of ondersteunt deze niet."; "lightning__choose_lsp__title" = "Kies een\nLightning-aanbieder"; diff --git a/Bitkit/Resources/Localization/pl.lproj/Localizable.strings b/Bitkit/Resources/Localization/pl.lproj/Localizable.strings index fff44b5f2..485b9536c 100644 --- a/Bitkit/Resources/Localization/pl.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/pl.lproj/Localizable.strings @@ -372,9 +372,9 @@ "lightning__channel_point" = "Punkt kanału"; "lightning__channels_closing_banner" = "KANAŁY ZOSTANĄ ZAMKNIĘTE {date}"; "lightning__channels_closing_notice" = "Twoje kanały Lightning zostaną zamknięte {date}. Przed tym terminem prześlij saldo wydatków na oszczędności."; -"lightning__channels_unavailable__button" = "Otwórz kanał"; -"lightning__channels_unavailable__text" = "Niestety usługa automatycznego przesyłania środków na saldo wydatków nie jest już dostępna. Nadal możesz ręcznie otworzyć kanał Lightning, aby doładować saldo wydatków."; -"lightning__channels_unavailable__title" = "Brak automatycznego przesłania"; +"lightning__channels_unavailable__nav_title" = "Przesłanie niedostępne"; +"lightning__channels_unavailable__text" = "Automatyczne przesyłanie środków na saldo wydatków nie jest już dostępne. Użyj własnego węzła lub skonfiguruj połączenie z innym dostawcą."; +"lightning__channels_unavailable__title" = "Wymagana ręczna\nkonfiguracja"; "lightning__choose_lsp__explanation" = "Gdy otrzymasz kod QR do połączenia lub odebrania kanału Lightning, dotknij poniżej, aby zeskanować lub wkleić kod QR."; "lightning__choose_lsp__text" = "Zamów kanał u dostawcy usług Lightning (LSP). Poniżej kilka opcji. Bitkit ich nie prowadzi, nie poleca ani nie wspiera."; "lightning__choose_lsp__title" = "Wybierz\ndostawcę Lightning"; diff --git a/Bitkit/Resources/Localization/pt-BR.lproj/Localizable.strings b/Bitkit/Resources/Localization/pt-BR.lproj/Localizable.strings index 2cc16c936..e83b07e91 100644 --- a/Bitkit/Resources/Localization/pt-BR.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/pt-BR.lproj/Localizable.strings @@ -372,9 +372,9 @@ "lightning__channel_point" = "Ponto do canal"; "lightning__channels_closing_banner" = "CANAIS FECHAM EM {date}"; "lightning__channels_closing_notice" = "Seus canais Lightning serão fechados em {date}. Transfira seu saldo de gastos para a poupança antes dessa data."; -"lightning__channels_unavailable__button" = "Abrir canal"; -"lightning__channels_unavailable__text" = "Infelizmente, o serviço de transferência automática de fundos para o seu saldo de gastos não está mais disponível. Você ainda pode abrir manualmente um canal Lightning para recarregar seu saldo de gastos."; -"lightning__channels_unavailable__title" = "Sem transferência automática"; +"lightning__channels_unavailable__nav_title" = "Transferência indisponível"; +"lightning__channels_unavailable__text" = "As transferências automáticas para seu saldo de gastos não estão mais disponíveis. Use seu próprio nó ou configure uma conexão com outro provedor."; +"lightning__channels_unavailable__title" = "Configuração manual\nnecessária"; "lightning__choose_lsp__explanation" = "Quando derem um QR para conectar ou resgatar o canal Lightning, toque abaixo para escanear ou colar o QR."; "lightning__choose_lsp__text" = "Peça um canal a um provedor de serviços Lightning (LSP). Abaixo estão algumas opções. O Bitkit não opera, endossa nem oferece suporte a elas."; "lightning__choose_lsp__title" = "Escolha um\nprovedor Lightning"; diff --git a/Bitkit/Resources/Localization/pt.lproj/Localizable.strings b/Bitkit/Resources/Localization/pt.lproj/Localizable.strings index ff5dbf7f4..ef27e47a7 100644 --- a/Bitkit/Resources/Localization/pt.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/pt.lproj/Localizable.strings @@ -372,9 +372,9 @@ "lightning__channel_point" = "Ponto do canal"; "lightning__channels_closing_banner" = "CANAIS FECHAM A {date}"; "lightning__channels_closing_notice" = "Os seus canais Lightning serão fechados a {date}. Transfira o seu saldo de gastos para a poupança antes dessa data."; -"lightning__channels_unavailable__button" = "Abrir canal"; -"lightning__channels_unavailable__text" = "Infelizmente, o serviço de transferência automática de fundos para o seu saldo de gastos já não está disponível. Ainda pode abrir manualmente um canal Lightning para carregar o seu saldo de gastos."; -"lightning__channels_unavailable__title" = "Sem transferência automática"; +"lightning__channels_unavailable__nav_title" = "Transferência indisponível"; +"lightning__channels_unavailable__text" = "As transferências automáticas para o seu saldo de gastos já não estão disponíveis. Use o seu próprio nó ou configure uma ligação com outro fornecedor."; +"lightning__channels_unavailable__title" = "Configuração manual\nnecessária"; "lightning__choose_lsp__explanation" = "Quando lhe derem um QR para ligar ou reclamar o canal Lightning, toque abaixo para digitalizar ou colar o QR."; "lightning__choose_lsp__text" = "Peça um canal a um fornecedor de serviços Lightning (LSP). Abaixo estão algumas opções. O Bitkit não as gere, recomenda nem suporta."; "lightning__choose_lsp__title" = "Escolha um\nfornecedor Lightning"; diff --git a/Bitkit/Resources/Localization/ru.lproj/Localizable.strings b/Bitkit/Resources/Localization/ru.lproj/Localizable.strings index 138b394f7..1b4046bcf 100644 --- a/Bitkit/Resources/Localization/ru.lproj/Localizable.strings +++ b/Bitkit/Resources/Localization/ru.lproj/Localizable.strings @@ -367,9 +367,9 @@ "lightning__channel_point" = "Точка Канала"; "lightning__channels_closing_banner" = "КАНАЛЫ ЗАКРОЮТСЯ {date}"; "lightning__channels_closing_notice" = "Ваши каналы Lightning будут закрыты {date}. До этой даты переведите баланс расходов в сбережения."; -"lightning__channels_unavailable__button" = "Открыть канал"; -"lightning__channels_unavailable__text" = "К сожалению, сервис автоматического перевода средств на баланс расходов больше недоступен. Вы по-прежнему можете вручную открыть канал Lightning, чтобы пополнить баланс расходов."; -"lightning__channels_unavailable__title" = "Нет автоматического перевода"; +"lightning__channels_unavailable__nav_title" = "Перевод недоступен"; +"lightning__channels_unavailable__text" = "Автоматические переводы на баланс расходов больше недоступны. Используйте собственный узел или настройте подключение к другому провайдеру."; +"lightning__channels_unavailable__title" = "Требуется ручная\nнастройка"; "lightning__choose_lsp__explanation" = "Когда вам дадут QR-код для подключения или получения канала Lightning, нажмите ниже, чтобы отсканировать или вставить QR-код."; "lightning__choose_lsp__text" = "Закажите канал у провайдера услуг Lightning (LSP). Ниже несколько вариантов. Bitkit не управляет ими, не рекомендует и не поддерживает их."; "lightning__choose_lsp__title" = "Выберите\nпровайдера Lightning"; diff --git a/Bitkit/Views/Wallets/Sheets/ChannelsUnavailableSheet.swift b/Bitkit/Views/Wallets/Sheets/ChannelsUnavailableSheet.swift index d0c842977..5f1d58bd1 100644 --- a/Bitkit/Views/Wallets/Sheets/ChannelsUnavailableSheet.swift +++ b/Bitkit/Views/Wallets/Sheets/ChannelsUnavailableSheet.swift @@ -2,7 +2,7 @@ import SwiftUI struct ChannelsUnavailableSheetItem: SheetItem { let id: SheetID = .channelsUnavailable - let size: SheetSize = .medium + let size: SheetSize = .large } /// Shown where the app used to start a transfer to spending: Blocktank no longer opens new channels, so it points to @@ -12,50 +12,24 @@ struct ChannelsUnavailableSheet: View { @EnvironmentObject private var navigation: NavigationViewModel @EnvironmentObject private var sheets: SheetViewModel - @EnvironmentObject private var wallet: WalletViewModel var body: some View { Sheet(id: .channelsUnavailable, data: config) { - VStack(alignment: .leading, spacing: 0) { - SheetHeader(title: t("lightning__channels_unavailable__title")) - - BodyMText(t("lightning__channels_unavailable__text"), textColor: .textSecondary) - - if wallet.hasTrustedChannels { - BodyMText( - t("lightning__channels_closing_notice", variables: ["date": ChannelsClosing.formattedCloseDate()]), - textColor: .textSecondary - ) - .padding(.top, 16) - .accessibilityIdentifier("ChannelsUnavailableCloseDate") - } - - Spacer() - - Image("exclamation-mark") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 256, height: 256) - .frame(maxWidth: .infinity) - - Spacer() - - HStack(spacing: 16) { - CustomButton(title: t("common__cancel"), variant: .secondary) { - sheets.hideSheet() - } - .accessibilityIdentifier("ChannelsUnavailableButton") - - CustomButton(title: t("lightning__channels_unavailable__button")) { - sheets.hideSheet() - navigation.navigate(.chooseLsp) - } - .accessibilityIdentifier("ChannelsUnavailableOpenChannel") + SheetIntro( + navTitle: t("lightning__channels_unavailable__nav_title"), + title: t("lightning__channels_unavailable__title"), + description: t("lightning__channels_unavailable__text"), + image: "lightning", + continueText: t("common__continue"), + cancelText: t("common__cancel"), + accentColor: .purpleAccent, + testID: "ChannelsUnavailableSheet", + onCancel: { sheets.hideSheet() }, + onContinue: { + sheets.hideSheet() + navigation.navigate(.chooseLsp) } - } - .padding(.horizontal, 16) + ) } - .accessibilityElement(children: .contain) - .accessibilityIdentifier("ChannelsUnavailableSheet") } } diff --git a/journeys/README.md b/journeys/README.md index 770f8663c..3c75125e5 100644 --- a/journeys/README.md +++ b/journeys/README.md @@ -114,7 +114,7 @@ wallet however you normally do against regtest and skip the helper. ### Getting a Lightning channel Blocktank is winding down, and this build no longer buys channels from it (synonymdev/bitkit-ios#1097): -Transfer To Spending opens a "No automatic transfer" sheet, and the Receive sheet's CJIT button and gift codes are gone. A journey that needs a +Transfer To Spending opens a "Transfer Unavailable" sheet, and the Receive sheet's CJIT button and gift codes are gone. A journey that needs a Lightning channel as its precondition says "a wallet with an open Lightning channel"; get one in either way: - Install a build from before that change (master before synonymdev/bitkit-ios#1097), open a channel with diff --git a/journeys/amount-limits/README.md b/journeys/amount-limits/README.md index f93e106fd..f4879becb 100644 --- a/journeys/amount-limits/README.md +++ b/journeys/amount-limits/README.md @@ -88,6 +88,6 @@ gate. `transfer-spending-over-max.xml` and `transfer-spending-advanced-over-max.xml` were retired because Blocktank channel purchases were removed (synonymdev/bitkit-ios#1097), and so was `../hardware-wallet/transfer-to-spending-over-max.xml`. Their routes started at the Savings wallet's -"Transfer To Spending" button or the hardware wallet's, which now open the "No automatic transfer" +"Transfer To Spending" button or the hardware wallet's, which now open the "Transfer Unavailable" sheet. The amount screens' identifiers (`SpendingAmount*`, `SpendingAdvanced*`, `HardwareTransferAmount*`) are no longer reachable from the UI. diff --git a/journeys/channels-closing/README.md b/journeys/channels-closing/README.md index 40f30976f..106ba1a74 100644 --- a/journeys/channels-closing/README.md +++ b/journeys/channels-closing/README.md @@ -15,12 +15,11 @@ The Android twin is synonymdev/bitkit-android#1669. same Blocktank-channel condition. - The date is a placeholder constant (Dec 15, 2026) until the shutdown date is final. Journeys assert the banner and the notice agree and read the date from the screen instead of hardcoding it. -- **"No automatic transfer" sheet** `ChannelsUnavailableSheet`: Savings "Transfer To Spending" +- **"Transfer Unavailable" sheet** `ChannelsUnavailableSheet` ("Manual setup required"): Savings "Transfer To Spending" (`TransferToSpending`), Spending "Transfer from Savings" (`TransferFromSavings`) and hardware wallet "Transfer To Spending" (`HardwareTransferToSpending`) keep their visibility rules and open it instead of - the transfer flow. For a wallet with a Blocktank channel it adds the close date - (`ChannelsUnavailableCloseDate`). "Cancel" (`ChannelsUnavailableButton`) closes it; "Open Channel" - (`ChannelsUnavailableOpenChannel`) opens Choose LSP. + the transfer flow. "Cancel" (`ChannelsUnavailableSheetCancel`) closes it; "Continue" + (`ChannelsUnavailableSheetContinue`) opens Choose LSP. - **Choose LSP** (Figma "Third-party LSP › Choose LSP"): four third-party providers (`ChooseLspProvider`), LNBIG, ZEUS Olympus, Megalithic and Amboss Magma, each opening its channel order page in the browser, plus "Manual Setup" (`ChooseLspManual`, the existing manual channel setup) and "QR Code" @@ -42,14 +41,15 @@ The Android twin is synonymdev/bitkit-android#1669. | Journey | Covers | | --- | --- | -| `channels-closing-notice.xml` | Banner on Home → tap → Spending wallet notice and Transfer To Savings; Savings Transfer To Spending opens the channels unavailable sheet with the close date | -| `choose-lsp-from-transfer.xml` | Transfer To Spending → sheet → Open Channel → Choose LSP: provider rows open the browser, QR Code opens the scanner, Manual Setup opens the manual channel setup | -| [`../receive/receive-onchain-only-without-channel.xml`](../receive/receive-onchain-only-without-channel.xml) | The opposite precondition: a funded wallet with no channels shows no banner, no Spend card and a Savings-only Receive sheet; the transfer buttons open the sheet without the close date | +| `channels-closing-notice.xml` | Banner on Home → tap → Spending wallet notice and Transfer To Savings; Savings Transfer To Spending opens the Transfer Unavailable sheet | +| `choose-lsp-from-transfer.xml` | Transfer To Spending → sheet → Continue → Choose LSP: provider rows open the browser, QR Code opens the scanner, Manual Setup opens the manual channel setup | +| [`../receive/receive-onchain-only-without-channel.xml`](../receive/receive-onchain-only-without-channel.xml) | The opposite precondition: a funded wallet with no channels shows no banner, no Spend card and a Savings-only Receive sheet; the transfer buttons open the Transfer Unavailable sheet | ## Identifiers used `ChannelsClosingBanner`, `ChannelsClosingNotice`, `ActivitySavings`, `ActivitySpending`, `TotalBalance-primary` (Android `TotalBalance`), `TransferToSavings`, `NavigationBack`, `TransferToSpending`, `TransferFromSavings`, `HardwareTransferToSpending`, `ChannelsUnavailableSheet`, -`ChannelsUnavailableCloseDate`, `ChannelsUnavailableButton`, `ChannelsUnavailableOpenChannel`, `ChooseLspProvider`, +`ChannelsUnavailableSheetTitle`, `ChannelsUnavailableSheetImage`, `ChannelsUnavailableSheetDescription`, +`ChannelsUnavailableSheetCancel`, `ChannelsUnavailableSheetContinue`, `ChooseLspProvider`, `ChooseLspManual`, `ChooseLspScan`, `NodeIdInput`, `HostInput`, `PortInput`. diff --git a/journeys/channels-closing/channels-closing-notice.xml b/journeys/channels-closing/channels-closing-notice.xml index 4abb11efc..bbdbb2bec 100644 --- a/journeys/channels-closing/channels-closing-notice.xml +++ b/journeys/channels-closing/channels-closing-notice.xml @@ -6,9 +6,9 @@ wallet, which shows "Your Lightning channels will be closed on Dec 15, 2026. Move your spending balance to savings before then." (id "ChannelsClosingNotice") under the balance header, and Transfer To Savings (id "TransferToSavings") is still offered. The Savings wallet's - Transfer To Spending button (id "TransferToSpending") opens the "No automatic transfer" sheet + Transfer To Spending button (id "TransferToSpending") opens the "Transfer Unavailable" sheet (id "ChannelsUnavailableSheet") instead of the transfer flow, since new channels can no longer be - bought; for this wallet the sheet also shows the close date (id "ChannelsUnavailableCloseDate"). The date is a placeholder constant until the shutdown date is final; assert whatever + bought. The date is a placeholder constant until the shutdown date is final; assert whatever date the build shows, and keep the banner and the notice in agreement. iOS: "Press the system back button" is the navigation bar back button (id "NavigationBack"). @@ -29,8 +29,8 @@ Verify the "Transfer To Savings" button (id "TransferToSavings") is visible Press the system back button, then tap the Savings balance card (id "ActivitySavings") Verify the Savings wallet screen is open, then tap "Transfer To Spending" (id "TransferToSpending") - Verify the sheet (id "ChannelsUnavailableSheet") opens with the title "No automatic transfer" and the text "Unfortunately, the service to automatically transfer funds to your spending balance is no longer available. You can still manually open a Lightning channel to top up your spending balance.", and that the line (id "ChannelsUnavailableCloseDate") reads "Your Lightning channels will be closed on Dec 15, 2026. Move your spending balance to savings before then." with the same date the banner showed - Tap "Cancel" (id "ChannelsUnavailableButton") and verify the sheet closes and the Savings wallet screen is still shown + Verify the sheet (id "ChannelsUnavailableSheet") opens with the nav title "Transfer Unavailable", the lightning-bolt image (id "ChannelsUnavailableSheetImage"), the title "MANUAL SETUP REQUIRED" with "MANUAL SETUP" in purple (id "ChannelsUnavailableSheetTitle") and the text "Automatic transfers to your spending balance are no longer available. Use your own node, or set up a connection with another provider." (id "ChannelsUnavailableSheetDescription") + Tap "Cancel" (id "ChannelsUnavailableSheetCancel") and verify the sheet closes and the Savings wallet screen is still shown Press the system back button, tap the Spending balance card (id "ActivitySpending") and verify the notice (id "ChannelsClosingNotice") is still shown diff --git a/journeys/channels-closing/choose-lsp-from-transfer.xml b/journeys/channels-closing/choose-lsp-from-transfer.xml index 758fccdd3..83a284db3 100644 --- a/journeys/channels-closing/choose-lsp-from-transfer.xml +++ b/journeys/channels-closing/choose-lsp-from-transfer.xml @@ -1,8 +1,8 @@ Verifies the way to a channel that is left after Blocktank channel purchases were removed - (synonymdev/bitkit-ios#1097). Transfer To Spending opens the "No automatic transfer" sheet, whose - "Open Channel" button (id "ChannelsUnavailableOpenChannel") opens the "Choose a Lightning provider" + (synonymdev/bitkit-ios#1097). Transfer To Spending opens the "Transfer Unavailable" sheet, whose + "Continue" button (id "ChannelsUnavailableSheetContinue") opens the "Choose a Lightning provider" screen. That screen lists third-party providers (id "ChooseLspProvider"), and each row opens the provider's own channel order page in the browser. Its footer has "Manual Setup" (testTag "ChooseLspManual"), which opens the manual channel setup with an empty node, and "QR Code" (testTag @@ -17,7 +17,7 @@ Tap the Savings balance card (id "ActivitySavings"), then "Transfer To Spending" (id "TransferToSpending") - Verify the sheet (id "ChannelsUnavailableSheet") shows the title "No automatic transfer", then tap "Open Channel" (id "ChannelsUnavailableOpenChannel") + Verify the sheet (id "ChannelsUnavailableSheet") shows the title "MANUAL SETUP REQUIRED" (id "ChannelsUnavailableSheetTitle"), then tap "Continue" (id "ChannelsUnavailableSheetContinue") and verify the sheet closes Verify the screen titled "Spending Balance" shows "CHOOSE A LIGHTNING PROVIDER", the text "Order a channel from a Lightning Service Provider (LSP). Below are some options. Bitkit does not run, endorse, or support these." and four provider rows (id "ChooseLspProvider"): "LNBIG", "ZEUS Olympus", "Megalithic" and "Amboss Magma" Verify the explanation "When they give you a QR to connect or claim the Lightning channel, tap below to scan or paste the QR." and the buttons "Manual Setup" (id "ChooseLspManual") and "QR Code" (id "ChooseLspScan") are visible Tap "Megalithic" and verify the browser opens https://megalithic.me/get_a_channel, then return to Bitkit and verify the provider screen is still shown diff --git a/journeys/hardware-wallet/README.md b/journeys/hardware-wallet/README.md index 92cc5963b..ae32c42cd 100644 --- a/journeys/hardware-wallet/README.md +++ b/journeys/hardware-wallet/README.md @@ -143,7 +143,7 @@ Walked as far as a simulator allows, against a wallet with two Trezor identities `transfer-to-spending-node-warmup.xml`, `passphrase-transfer-to-spending.xml` and the iOS-only `transfer-to-spending-over-max.xml` were retired because Blocktank channel purchases were removed (synonymdev/bitkit-ios#1097): the hardware wallet's Transfer To Spending button (`HardwareTransferToSpending`) now -opens the "No automatic transfer" sheet. The `HardwareTransfer*` and `HwTransferPassphrase*` screens +opens the "Transfer Unavailable" sheet. The `HardwareTransfer*` and `HwTransferPassphrase*` screens and identifiers are still in code but nothing in the UI reaches them. ## Local Payment Completion diff --git a/journeys/hardware-wallet/detail-overview.xml b/journeys/hardware-wallet/detail-overview.xml index 1b1c4a722..6a48587a9 100644 --- a/journeys/hardware-wallet/detail-overview.xml +++ b/journeys/hardware-wallet/detail-overview.xml @@ -2,7 +2,7 @@ Opens the hardware wallet detail screen from the home tile and verifies its overview: the top bar, balance header, the Transfer To Spending entry, which opens the - "No automatic transfer" sheet since Blocktank channel purchases were removed (synonymdev/bitkit-ios#1097), the grouped activity list, and the Remove device confirm dialog. The final + "Transfer Unavailable" sheet since Blocktank channel purchases were removed (synonymdev/bitkit-ios#1097), the grouped activity list, and the Remove device confirm dialog. The final Remove step forgets the device, so run this last (re-run connect-home-tile.xml to pair again). Requires a paired Bridge emulator (run connect-home-tile.xml first). @@ -10,7 +10,7 @@ Launch the Bitkit app and go to the wallet home screen Tap the hardware wallet tile (id "ActivityHardware") beneath the SAVINGS and SPENDING tiles Verify the hardware wallet detail screen opens (id "HardwareWalletScreen"), showing the device name with a blue bitcoin icon in the top bar and a balance header - If a "Transfer To Spending" button is shown (id "HardwareTransferToSpending"), tap it, verify the "No automatic transfer" sheet opens (id "ChannelsUnavailableSheet") and no transfer amount screen opens, then tap "Cancel" (id "ChannelsUnavailableButton"); otherwise skip this step + If the wallet holds funds, verify the "Transfer To Spending" button (id "HardwareTransferToSpending") is shown, tap it, verify the sheet (id "ChannelsUnavailableSheet") opens with the nav title "Transfer Unavailable", the lightning-bolt image (id "ChannelsUnavailableSheetImage"), the title "MANUAL SETUP REQUIRED" with "MANUAL SETUP" in purple (id "ChannelsUnavailableSheetTitle") and the text "Automatic transfers to your spending balance are no longer available. Use your own node, or set up a connection with another provider." (id "ChannelsUnavailableSheetDescription"), then tap "Cancel" (id "ChannelsUnavailableSheetCancel"); otherwise skip this step If the activity list shows transactions, verify their circular icons are blue, then tap the first one, verify an activity detail screen opens, and navigate back Tap the "Remove" button labelled with the device name near the bottom of the screen (id "RemoveHardwareWallet") Verify a confirm dialog appears (id "RemoveHwWalletDialog") explaining that your funds are safe and your coins won't be deleted diff --git a/journeys/receive/receive-onchain-only-without-channel.xml b/journeys/receive/receive-onchain-only-without-channel.xml index 5b29b4dac..8c48a86bb 100644 --- a/journeys/receive/receive-onchain-only-without-channel.xml +++ b/journeys/receive/receive-onchain-only-without-channel.xml @@ -27,8 +27,8 @@ Tap "Edit" (id "SpecifyInvoiceButton"), tap the amount field (id "ReceiveNumberPadTextField"), enter a small amount (for example "N1" then "N0" twice), tap Continue (id "ReceiveNumberPadSubmit") and then "QR Code" (id "ShowQrReceive") Verify the sheet returns to the QR screen with no liquidity or channel confirm screen in between, the Savings tab is still selected and there is still no Spending tab (id "Tab-spending") Close the sheet, open the Savings wallet (id "ActivitySavings") and tap "Transfer To Spending" (id "TransferToSpending") - Verify the sheet (id "ChannelsUnavailableSheet") shows the title "No automatic transfer" and the text "Unfortunately, the service to automatically transfer funds to your spending balance is no longer available. You can still manually open a Lightning channel to top up your spending balance.", and no close date line (id "ChannelsUnavailableCloseDate"); tap "Cancel" (id "ChannelsUnavailableButton") and verify the Savings wallet screen is still shown + Verify the sheet (id "ChannelsUnavailableSheet") opens with the nav title "Transfer Unavailable", the lightning-bolt image (id "ChannelsUnavailableSheetImage"), the title "MANUAL SETUP REQUIRED" with "MANUAL SETUP" in purple (id "ChannelsUnavailableSheetTitle") and the text "Automatic transfers to your spending balance are no longer available. Use your own node, or set up a connection with another provider." (id "ChannelsUnavailableSheetDescription"); tap "Cancel" (id "ChannelsUnavailableSheetCancel") and verify the Savings wallet screen is still shown Open the Spending wallet (id "ActivitySpending"), verify no notice (id "ChannelsClosingNotice") is shown, and tap "Transfer from Savings" (id "TransferFromSavings") - Verify the same sheet (id "ChannelsUnavailableSheet") opens with no close date line (id "ChannelsUnavailableCloseDate"), tap "Cancel" (id "ChannelsUnavailableButton") and verify the Spending wallet screen is still shown + Verify the same sheet (id "ChannelsUnavailableSheet") opens, tap "Cancel" (id "ChannelsUnavailableSheetCancel") and verify the Spending wallet screen is still shown From 4878fe58a3c95f8ba6333cd450c6e7d43406bc5b Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Thu, 22 Oct 2026 13:45:01 +0200 Subject: [PATCH 06/12] fix: make the whole provider row tappable --- Bitkit/Views/Transfer/ChooseLspView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Bitkit/Views/Transfer/ChooseLspView.swift b/Bitkit/Views/Transfer/ChooseLspView.swift index 7cda26aa7..853117687 100644 --- a/Bitkit/Views/Transfer/ChooseLspView.swift +++ b/Bitkit/Views/Transfer/ChooseLspView.swift @@ -43,6 +43,7 @@ struct ChooseLspView: View { openURL(provider.url) } label: { SettingsRow(title: provider.name) + .contentShape(Rectangle()) } .buttonStyle(.plain) .accessibilityIdentifier("ChooseLspProvider") From de1f21c604dcb232ec0a18a1b6dbd1c40c8f01c5 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Thu, 22 Oct 2026 13:55:20 +0200 Subject: [PATCH 07/12] docs: describe receive without cjit --- Docs/receive-liquidity.md | 53 +++++++++++++-------------------------- 1 file changed, 18 insertions(+), 35 deletions(-) diff --git a/Docs/receive-liquidity.md b/Docs/receive-liquidity.md index 0dbe905f2..5ff5e7079 100644 --- a/Docs/receive-liquidity.md +++ b/Docs/receive-liquidity.md @@ -1,6 +1,8 @@ # Receive Liquidity Behavior -This document describes how the receive flow decides whether to show a normal Lightning invoice or route the user into CJIT liquidity setup. +This document describes which Receive tabs the app offers and what it does when an invoice amount does not fit +the wallet's Lightning liquidity. The app no longer buys channels or instant receive (CJIT) liquidity from +Blocktank (synonymdev/bitkit-ios#1097), so a receive that needs liquidity falls back to Savings/onchain. ## Cases @@ -8,13 +10,14 @@ This document describes how the receive flow decides whether to show a normal Li - A new Receive sheet session starts from a fresh tab state. - If Auto is available, the default tab is Auto. - If Auto is unavailable, the default tab is Savings. - - Temporary receive-session state, such as selected tab, nested navigation, pending CJIT details, and CJIT invoice QR state, must not survive closing and reopening the Receive sheet. + - Temporary receive-session state, such as selected tab and nested navigation, must not survive closing and + reopening the Receive sheet. -- Editing from Savings or Auto: - - Editing sets the amount for the receive request. +- Editing from Savings, Auto or Spending: + - Editing sets the amount for the receive request and returns to the QR. - If the edited amount can be received over Lightning, the regenerated Spending invoice also includes that amount. - - If the edited amount cannot be received over Lightning, Auto falls back to the Savings tab and shows the onchain QR instead of routing to CJIT. - - The edit flow does not create CJIT or route to CJIT amount entry. + - If the edited amount cannot be received over Lightning, the Auto and Spending tabs are no longer offered and + the sheet falls back to the Savings tab with the onchain QR. - Editing from a hardware receive tab: - Editing sets the amount for the hardware/onchain receive request. @@ -23,14 +26,10 @@ This document describes how the receive flow decides whether to show a normal Li - Lightning receive unavailable because there is no ready channel or ready inbound liquidity is `0`: - No Lightning invoice is created. - - The normal QR remains Savings/onchain only. - - The Spending tab shows CJIT onboarding. - - Tapping receive spending routes to CJIT amount entry, or the CJIT geo-block screen when geo-blocked. - - Editing from Savings or Auto updates the receive amount and returns to the normal QR; it does not create or route to CJIT. - - When a channel already exists, later CJIT confirmation and learn-more screens use additional-liquidity copy. + - The sheet offers the Savings tab only (plus USDT or hardware when configured); there is no Spending or Auto tab. - Ready channel, inbound liquidity greater than `0`, zero/variable amount: - - A Lightning invoice is allowed. + - A Lightning invoice is allowed, and the Spending and Auto tabs are offered. - A zero/variable Lightning invoice is allowed when inbound liquidity is greater than `0`, even though the sender could later choose an amount above the available inbound capacity. - Ready channel, fixed amount less than or equal to inbound liquidity: @@ -38,29 +37,13 @@ This document describes how the receive flow decides whether to show a normal Li - The unified QR includes Lightning. - The Spending tab shows the normal Lightning invoice. -- Ready channel, fixed amount greater than inbound liquidity but below CJIT minimum: - - A normal Lightning invoice is not shown. - - Editing from Spending routes to CJIT amount entry. - - The user must choose at least the minimum CJIT amount. - - Editing from Savings or Auto returns to the normal QR with Savings/onchain only. - -- Ready channel, fixed amount greater than inbound liquidity and at or above CJIT minimum: - - If editing from Spending and the amount can be backed by a CJIT channel without exceeding Blocktank's maximum channel size, the edit flow creates additional CJIT. - - The user gets CJIT confirmation and then a CJIT Lightning invoice QR. - - The CJIT Lightning invoice is an invoice to the LSP and must be shown as Spending-only, not as Auto/unified receive. - - Editing from a CJIT Lightning invoice QR must replace the stale CJIT QR route before showing the updated receive result, because the previous LSP invoice is immutable. - - The direct additional CJIT path must not regenerate the normal receive invoice before creating CJIT. - - If editing from Spending and the amount is too large for CJIT, or the maximum cannot be calculated, the edit flow routes to CJIT amount entry. - - The CJIT amount screen enforces the real maximum receivable amount, calculated from `invoiceSat + defaultLspBalance(invoiceSat) <= maxChannelSizeSat`. - - If Blocktank rejects additional CJIT because the node is already at its total capacity limit, the app explains that additional spending capacity is unavailable instead of showing the per-channel maximum. - - Editing from Savings or Auto returns to the normal QR with Savings/onchain only. - -- Geo-blocked and liquidity is needed: - - The flow routes to the CJIT geo-block screen. - - No CJIT invoice is created. +- Ready channel, fixed amount greater than inbound liquidity: + - No Lightning invoice is created, and the sheet falls back to Savings/onchain only. ## Invariants -- Auto tab availability and default tab selection are based on whether a normal Lightning invoice can be created for the current receive amount. -- Ready channels alone do not imply Auto availability; fixed receive amounts must also fit within ready inbound liquidity. -- CJIT min and max limits are only needed when a Spending-origin edit needs additional inbound liquidity and the user is not geo-blocked. +- Spending and Auto tab availability, and the default tab, are based on whether a normal Lightning invoice can be + created for the current receive amount. +- Ready channels alone do not imply Spending or Auto availability; fixed receive amounts must also fit within ready + inbound liquidity. +- No receive path creates a CJIT entry or a Blocktank channel order. From 8f947922642236caaa1b76bebd53952403334833 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Thu, 22 Oct 2026 14:31:18 +0200 Subject: [PATCH 08/12] fix: fit the channels closing banner title on one line --- Bitkit/Components/Activity/ActivityBanner.swift | 7 +++++-- Bitkit/Views/Home/HomeWalletView.swift | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Bitkit/Components/Activity/ActivityBanner.swift b/Bitkit/Components/Activity/ActivityBanner.swift index ef90b2e2f..15df0c65c 100644 --- a/Bitkit/Components/Activity/ActivityBanner.swift +++ b/Bitkit/Components/Activity/ActivityBanner.swift @@ -10,15 +10,18 @@ enum ActivityBannerType { struct ActivityBanner: View { let type: ActivityBannerType let remainingDuration: String? + /// Shrinks the title to fit one line instead of truncating it. + let autoSizeTitle: Bool @State private var innerShadowOpacity: Double = 0.32 @State private var dropShadowOpacity: Double = 1.0 @State private var radialGradientOpacity: Double = 0.4 @State private var borderOpacity: Double = 0.32 - init(type: ActivityBannerType, remainingDuration: String? = nil) { + init(type: ActivityBannerType, remainingDuration: String? = nil, autoSizeTitle: Bool = false) { self.type = type self.remainingDuration = remainingDuration + self.autoSizeTitle = autoSizeTitle } private var accentColor: Color { @@ -57,7 +60,7 @@ struct ActivityBanner: View { .textCase(.uppercase) .kerning(0) .lineLimit(1) - .minimumScaleFactor(0.7) + .minimumScaleFactor(autoSizeTitle ? 12.0 / 20.0 : 1) } .frame(maxWidth: .infinity, alignment: .center) .frame(height: 72) diff --git a/Bitkit/Views/Home/HomeWalletView.swift b/Bitkit/Views/Home/HomeWalletView.swift index 3cac295a4..c1fc9ab45 100644 --- a/Bitkit/Views/Home/HomeWalletView.swift +++ b/Bitkit/Views/Home/HomeWalletView.swift @@ -95,7 +95,7 @@ struct HomeWalletView: View { if wallet.hasTrustedChannels { Button { navigation.navigate(.spendingWallet) } label: { - ActivityBanner(type: .channelsClosing) + ActivityBanner(type: .channelsClosing, autoSizeTitle: true) } .buttonStyle(.plain) .accessibilityIdentifier("ChannelsClosingBanner") From 04cc64b7debdba78f282034447c9bd73416057e3 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Thu, 22 Oct 2026 14:40:56 +0200 Subject: [PATCH 09/12] feat: remove blocktank calls that only served purchases Drops the geocheck, the trusted-peer list from Blocktank info (peers come from Env), the refund address for new orders, the order polling timer and the info refresh when Receive opens. Info for fee estimates, FX rates, push registration and the watch of already paid orders stay. --- .github/workflows/integration-tests.yml | 1 - .github/workflows/unit-tests.yml | 1 - Bitkit.xcodeproj/project.pbxproj | 6 +- Bitkit/Constants/Env.swift | 12 - .../BlocktankRefundAddressProvider.swift | 275 --------- .../BlocktankRefundAddressStore.swift | 131 +++++ Bitkit/Services/CoreService.swift | 31 - Bitkit/Services/GeoService.swift | 36 -- Bitkit/Services/LightningService.swift | 43 +- Bitkit/Services/TransferService.swift | 16 +- Bitkit/ViewModels/AppViewModel.swift | 6 - Bitkit/ViewModels/BlocktankViewModel.swift | 55 +- Bitkit/ViewModels/WalletViewModel.swift | 50 +- .../Views/Onboarding/OnboardingSlider.swift | 1 - Bitkit/Views/Settings/LdkDebugScreen.swift | 15 - Bitkit/Views/Transfer/FundingOptions.swift | 15 +- .../Views/Wallets/HardwareWalletScreen.swift | 6 +- Bitkit/Views/Wallets/Receive/ReceiveQr.swift | 12 +- .../Views/Wallets/SavingsWalletScreen.swift | 8 +- .../Views/Wallets/SpendingWalletScreen.swift | 2 +- ...ankRefundAddressLiveIntegrationTests.swift | 185 ------ .../BlocktankRefundAddressProviderTests.swift | 538 ------------------ .../BlocktankRefundAddressStoreTests.swift | 153 +++++ .../TransferServiceActivityTests.swift | 6 +- 24 files changed, 302 insertions(+), 1302 deletions(-) delete mode 100644 Bitkit/Services/BlocktankRefundAddressProvider.swift create mode 100644 Bitkit/Services/BlocktankRefundAddressStore.swift delete mode 100644 Bitkit/Services/GeoService.swift delete mode 100644 BitkitTests/BlocktankRefundAddressLiveIntegrationTests.swift delete mode 100644 BitkitTests/BlocktankRefundAddressProviderTests.swift create mode 100644 BitkitTests/BlocktankRefundAddressStoreTests.swift diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 927863b3a..7d2e253a3 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -105,7 +105,6 @@ jobs: -only-testing:BitkitTests/UtxoSelectionTests \ -only-testing:BitkitTests/BlocktankTests \ -only-testing:BitkitTests/PaymentFlowTests \ - -only-testing:BitkitTests/BlocktankRefundAddressLiveIntegrationTests \ -only-testing:BitkitTests/AddressTypeIntegrationTests \ -only-testing:BitkitTests/RNMigrationCleanupTests \ | xcbeautify --report junit diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 0a83d0d07..36999a628 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -59,7 +59,6 @@ jobs: -skip-testing:BitkitTests/UtxoSelectionTests \ -skip-testing:BitkitTests/BlocktankTests \ -skip-testing:BitkitTests/PaymentFlowTests \ - -skip-testing:BitkitTests/BlocktankRefundAddressLiveIntegrationTests \ -skip-testing:BitkitTests/AddressTypeIntegrationTests \ -skip-testing:BitkitTests/RNMigrationCleanupTests \ | xcbeautify --report junit diff --git a/Bitkit.xcodeproj/project.pbxproj b/Bitkit.xcodeproj/project.pbxproj index 65341a451..f6818e5cc 100644 --- a/Bitkit.xcodeproj/project.pbxproj +++ b/Bitkit.xcodeproj/project.pbxproj @@ -171,7 +171,6 @@ Models/TransferType.swift, Services/BackupFieldMigration.swift, Services/CoreService.swift, - Services/GeoService.swift, Services/HwActivityTagBackup.swift, Services/HwSnapshotMerge.swift, Services/LightningService.swift, @@ -205,7 +204,6 @@ Models/PubkyPublicKeyFormat.swift, Models/Toast.swift, Services/CoreService.swift, - Services/GeoService.swift, Services/HwActivityTagBackup.swift, Services/HwSnapshotMerge.swift, Services/LightningService.swift, @@ -886,7 +884,7 @@ MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -warnings-as-errors -Wwarning DeprecatedDeclaration"; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG CHECK_GEOBLOCK $(inherited)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; @@ -941,7 +939,7 @@ MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; OTHER_SWIFT_FLAGS = "$(inherited) -warnings-as-errors -Wwarning DeprecatedDeclaration"; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "CHECK_GEOBLOCK $(inherited)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; SWIFT_COMPILATION_MODE = wholemodule; }; name = Release; diff --git a/Bitkit/Constants/Env.swift b/Bitkit/Constants/Env.swift index 768856962..871452c10 100644 --- a/Bitkit/Constants/Env.swift +++ b/Bitkit/Constants/Env.swift @@ -138,14 +138,6 @@ enum Env { } } - static var isGeoblockingEnabled: Bool { - #if CHECK_GEOBLOCK - return true - #else - return ProcessInfo.processInfo.environment["GEO"] == "true" - #endif - } - /// The current execution context of the app static var currentExecutionContext: ExecutionContext { let isNotificationExtension = Bundle.main.bundleIdentifier?.lowercased().contains("notification") == true @@ -304,7 +296,6 @@ enum Env { } } - // TODO: remove this to load from BT API instead static var trustedLnPeers: [LnPeer] { switch network { case .bitcoin: @@ -357,8 +348,6 @@ enum Env { static let fxRateRefreshInterval: TimeInterval = 2 * 60 // 2 minutes static let fxRateStaleThreshold: TimeInterval = 10 * 60 // After this we notify the user that the rates are stale due to a failed refresh - static let blocktankOrderRefreshInterval: TimeInterval = 2 * 60 // 2 minutes - static var pushNotificationFeatures: [BlocktankNotificationType] = [ .incomingHtlc, .mutualClose, @@ -433,7 +422,6 @@ enum Env { static let termsOfServiceUrl = "https://www.bitkit.to/terms-of-use" static let privacyPolicyUrl = "https://www.bitkit.to/privacy-policy" - static let geoCheckUrl = "https://api1.blocktank.to/api/geocheck" static let bitrefillRef = "AL6dyZYt" static let btcMapUrl = "https://btcmap.org/map" static let helpUrl = "https://help.bitkit.to" diff --git a/Bitkit/Services/BlocktankRefundAddressProvider.swift b/Bitkit/Services/BlocktankRefundAddressProvider.swift deleted file mode 100644 index 05eaaa438..000000000 --- a/Bitkit/Services/BlocktankRefundAddressProvider.swift +++ /dev/null @@ -1,275 +0,0 @@ -import Foundation -import LDKNode - -enum BlocktankRefundAddressError: LocalizedError, Equatable { - case invalidCache - case invalidAddress - case indexOutOfRange(UInt32) - case ownershipMismatch - case persistenceFailed - case allocationDidNotAdvance - case allocationAttemptsExhausted - - var errorDescription: String? { - switch self { - case .invalidCache: - "The saved Blocktank refund address is invalid." - case .invalidAddress: - "The Blocktank refund address is empty." - case let .indexOutOfRange(index): - "The Blocktank refund address index is out of range: \(index)." - case .ownershipMismatch: - "The saved Blocktank refund address does not belong to the active wallet and network." - case .persistenceFailed: - "The Blocktank refund address could not be saved." - case .allocationDidNotAdvance: - "The Blocktank refund address index did not advance." - case .allocationAttemptsExhausted: - "No unused Blocktank refund address was found." - } - } -} - -struct BlocktankRefundAddressStore { - static let legacyKey = "blocktankRefundAddress" - static var key: String { - key(for: Env.network) - } - - private let defaults: UserDefaults - private let network: LDKNode.Network - - private var key: String { - Self.key(for: network) - } - - init(defaults: UserDefaults = .standard, network: LDKNode.Network = Env.network) { - self.defaults = defaults - self.network = network - } - - static func key(for network: LDKNode.Network) -> String { - "\(legacyKey)_\(Env.networkName(for: network))" - } - - func load() throws -> BlocktankRefundAddress? { - if let value = try load(forKey: key) { - return value - } - - guard let legacy = try load(forKey: Self.legacyKey) else { return nil } - let matchingNetworks = Self.matchingNetworks(for: legacy.address) - guard !matchingNetworks.isEmpty else { - throw BlocktankRefundAddressError.invalidCache - } - guard matchingNetworks == [network] else { return nil } - - try save(legacy) - defaults.removeObject(forKey: Self.legacyKey) - return legacy - } - - private func load(forKey key: String) throws -> BlocktankRefundAddress? { - guard defaults.object(forKey: key) != nil else { return nil } - guard let data = defaults.data(forKey: key) else { - throw BlocktankRefundAddressError.invalidCache - } - - do { - return try JSONDecoder().decode(BlocktankRefundAddress.self, from: data) - } catch { - throw BlocktankRefundAddressError.invalidCache - } - } - - func save(_ value: BlocktankRefundAddress) throws { - let data = try JSONEncoder().encode(value) - let previousValue = defaults.object(forKey: key) - defaults.set(data, forKey: key) - - do { - guard try load(forKey: key) == value else { - throw BlocktankRefundAddressError.persistenceFailed - } - } catch { - if let previousValue { - defaults.set(previousValue, forKey: key) - } else { - defaults.removeObject(forKey: key) - } - throw BlocktankRefundAddressError.persistenceFailed - } - } - - func clear() { - defaults.removeObject(forKey: key) - - guard defaults.object(forKey: Self.legacyKey) != nil else { return } - guard let legacy = try? load(forKey: Self.legacyKey) else { - defaults.removeObject(forKey: Self.legacyKey) - return - } - let matchingNetworks = Self.matchingNetworks(for: legacy.address) - if matchingNetworks.isEmpty || matchingNetworks == [network] { - defaults.removeObject(forKey: Self.legacyKey) - } - } - - private static func matchingNetworks(for address: String) -> [LDKNode.Network] { - let trimmed = address.trimmingCharacters(in: .whitespaces) - if trimmed.hasPrefix("bc1q") { - return [.bitcoin] - } - if trimmed.hasPrefix("bcrt1q") { - return [.regtest] - } - if trimmed.hasPrefix("tb1q") { - return [.testnet, .signet] - } - return [] - } -} - -@MainActor -protocol BlocktankRefundAddressProviding: AnyObject { - func addressForOrder() async throws -> String -} - -@MainActor -final class BlocktankRefundAddressProvider: BlocktankRefundAddressProviding { - static let maximumExternalIndex = UInt32(Int32.max) - static let maximumAllocationAttempts = 20 - - typealias Load = () throws -> BlocktankRefundAddress? - typealias Save = (BlocktankRefundAddress) throws -> Void - typealias Lookup = (UInt32) async throws -> BlocktankRefundAddress - typealias Reveal = (UInt32) async throws -> Void - typealias IsUsed = (String) async throws -> Bool - typealias Allocate = () async throws -> BlocktankRefundAddress - - private let load: Load - private let save: Save - private let lookup: Lookup - private let reveal: Reveal - private let isUsed: IsUsed - private let allocate: Allocate - private var inFlight: Task? - - init( - load: @escaping Load, - save: @escaping Save, - lookup: @escaping Lookup, - reveal: @escaping Reveal, - isUsed: @escaping IsUsed, - allocate: @escaping Allocate - ) { - self.load = load - self.save = save - self.lookup = lookup - self.reveal = reveal - self.isUsed = isUsed - self.allocate = allocate - } - - convenience init( - lightningService: LightningService, - utilityService: UtilityService, - store: BlocktankRefundAddressStore = .init() - ) { - self.init( - load: { try store.load() }, - save: { try store.save($0) }, - lookup: { index in - let info = try await lightningService.addressInfoForType( - .nativeSegwit, - keychain: .external, - atIndex: index - ) - return BlocktankRefundAddress(address: info.address, index: info.index) - }, - reveal: { index in - try await lightningService.revealReceiveAddresses(to: index, forType: .nativeSegwit) - }, - isUsed: { address in - try await utilityService.isAddressUsed(address: address) - }, - allocate: { - let info = try await lightningService.newAddressInfoForType(.nativeSegwit) - return BlocktankRefundAddress(address: info.address, index: info.index) - } - ) - } - - func addressForOrder() async throws -> String { - if let inFlight { - return try await inFlight.value - } - - let operation = Task { @MainActor [load, save, lookup, reveal, isUsed, allocate] in - try await Self.resolve( - load: load, - save: save, - lookup: lookup, - reveal: reveal, - isUsed: isUsed, - allocate: allocate - ) - } - inFlight = operation - defer { inFlight = nil } - return try await operation.value - } - - private static func resolve( - load: Load, - save: Save, - lookup: Lookup, - reveal: Reveal, - isUsed: IsUsed, - allocate: Allocate - ) async throws -> String { - var previousIndex: UInt32? - - if let cached = try load() { - try validate(cached) - - let derived = try await lookup(cached.index) - guard derived == cached else { - throw BlocktankRefundAddressError.ownershipMismatch - } - - try await reveal(cached.index) - if try await isUsed(cached.address) == false { - return cached.address - } - previousIndex = cached.index - } - - for _ in 0 ..< maximumAllocationAttempts { - let generated = try await allocate() - try validate(generated) - if let previousIndex, generated.index <= previousIndex { - throw BlocktankRefundAddressError.allocationDidNotAdvance - } - previousIndex = generated.index - - if try await isUsed(generated.address) { - continue - } - - try save(generated) - return generated.address - } - - throw BlocktankRefundAddressError.allocationAttemptsExhausted - } - - private static func validate(_ value: BlocktankRefundAddress) throws { - guard !value.address.isEmpty else { - throw BlocktankRefundAddressError.invalidAddress - } - guard value.index <= maximumExternalIndex else { - throw BlocktankRefundAddressError.indexOutOfRange(value.index) - } - } -} diff --git a/Bitkit/Services/BlocktankRefundAddressStore.swift b/Bitkit/Services/BlocktankRefundAddressStore.swift new file mode 100644 index 000000000..a541904b4 --- /dev/null +++ b/Bitkit/Services/BlocktankRefundAddressStore.swift @@ -0,0 +1,131 @@ +import Foundation +import LDKNode + +enum BlocktankRefundAddressError: LocalizedError, Equatable { + case invalidCache + case invalidAddress + case indexOutOfRange(UInt32) + case ownershipMismatch + case persistenceFailed + case allocationDidNotAdvance + case allocationAttemptsExhausted + + var errorDescription: String? { + switch self { + case .invalidCache: + "The saved Blocktank refund address is invalid." + case .invalidAddress: + "The Blocktank refund address is empty." + case let .indexOutOfRange(index): + "The Blocktank refund address index is out of range: \(index)." + case .ownershipMismatch: + "The saved Blocktank refund address does not belong to the active wallet and network." + case .persistenceFailed: + "The Blocktank refund address could not be saved." + case .allocationDidNotAdvance: + "The Blocktank refund address index did not advance." + case .allocationAttemptsExhausted: + "No unused Blocktank refund address was found." + } + } +} + +struct BlocktankRefundAddressStore { + static let legacyKey = "blocktankRefundAddress" + static var key: String { + key(for: Env.network) + } + + private let defaults: UserDefaults + private let network: LDKNode.Network + + private var key: String { + Self.key(for: network) + } + + init(defaults: UserDefaults = .standard, network: LDKNode.Network = Env.network) { + self.defaults = defaults + self.network = network + } + + static func key(for network: LDKNode.Network) -> String { + "\(legacyKey)_\(Env.networkName(for: network))" + } + + func load() throws -> BlocktankRefundAddress? { + if let value = try load(forKey: key) { + return value + } + + guard let legacy = try load(forKey: Self.legacyKey) else { return nil } + let matchingNetworks = Self.matchingNetworks(for: legacy.address) + guard !matchingNetworks.isEmpty else { + throw BlocktankRefundAddressError.invalidCache + } + guard matchingNetworks == [network] else { return nil } + + try save(legacy) + defaults.removeObject(forKey: Self.legacyKey) + return legacy + } + + private func load(forKey key: String) throws -> BlocktankRefundAddress? { + guard defaults.object(forKey: key) != nil else { return nil } + guard let data = defaults.data(forKey: key) else { + throw BlocktankRefundAddressError.invalidCache + } + + do { + return try JSONDecoder().decode(BlocktankRefundAddress.self, from: data) + } catch { + throw BlocktankRefundAddressError.invalidCache + } + } + + func save(_ value: BlocktankRefundAddress) throws { + let data = try JSONEncoder().encode(value) + let previousValue = defaults.object(forKey: key) + defaults.set(data, forKey: key) + + do { + guard try load(forKey: key) == value else { + throw BlocktankRefundAddressError.persistenceFailed + } + } catch { + if let previousValue { + defaults.set(previousValue, forKey: key) + } else { + defaults.removeObject(forKey: key) + } + throw BlocktankRefundAddressError.persistenceFailed + } + } + + func clear() { + defaults.removeObject(forKey: key) + + guard defaults.object(forKey: Self.legacyKey) != nil else { return } + guard let legacy = try? load(forKey: Self.legacyKey) else { + defaults.removeObject(forKey: Self.legacyKey) + return + } + let matchingNetworks = Self.matchingNetworks(for: legacy.address) + if matchingNetworks.isEmpty || matchingNetworks == [network] { + defaults.removeObject(forKey: Self.legacyKey) + } + } + + private static func matchingNetworks(for address: String) -> [LDKNode.Network] { + let trimmed = address.trimmingCharacters(in: .whitespaces) + if trimmed.hasPrefix("bc1q") { + return [.bitcoin] + } + if trimmed.hasPrefix("bcrt1q") { + return [.regtest] + } + if trimmed.hasPrefix("tb1q") { + return [.testnet, .signet] + } + return [] + } +} diff --git a/Bitkit/Services/CoreService.swift b/Bitkit/Services/CoreService.swift index 4be21d6d9..a06da20e3 100644 --- a/Bitkit/Services/CoreService.swift +++ b/Bitkit/Services/CoreService.swift @@ -2635,35 +2635,4 @@ class CoreService { } } } - - func checkGeoStatus() async throws -> Bool? { - if !Env.isGeoblockingEnabled { - return false - } - - return try await ServiceQueue.background(.core) { - Logger.info("Checking geo status...", context: "GeoCheck") - guard let url = URL(string: Env.geoCheckUrl) else { - Logger.error("Invalid geocheck URL: \(Env.geoCheckUrl)", context: "GeoCheck") - return nil as Bool? - } - - let (_, response) = try await URLSession.shared.data(from: url) - if let httpResponse = response as? HTTPURLResponse { - Logger.debug("Received geo status response: \(httpResponse.statusCode)", context: "GeoCheck") - switch httpResponse.statusCode { - case 200: - Logger.info("Region allowed", context: "GeoCheck") - return false - case 403: - Logger.warn("Region blocked", context: "GeoCheck") - return true - default: - Logger.warn("Unexpected status code: \(httpResponse.statusCode)", context: "GeoCheck") - return nil as Bool? - } - } - return nil as Bool? - } - } } diff --git a/Bitkit/Services/GeoService.swift b/Bitkit/Services/GeoService.swift deleted file mode 100644 index 725643c8d..000000000 --- a/Bitkit/Services/GeoService.swift +++ /dev/null @@ -1,36 +0,0 @@ -import Foundation -import SwiftUI - -/// Service responsible for managing geoblocking state -class GeoService: ObservableObject { - static let shared = GeoService() - - /// Current geoblocking status - /// - `false`: User is not geoblocked (default/fallback if check fails) - /// - `true`: User is geoblocked - @Published var isGeoBlocked: Bool = false - - private let coreService: CoreService - - private init(coreService: CoreService = .shared) { - self.coreService = coreService - } - - /// Checks the current geoblocking status and updates the published state - func checkGeoStatus() async { - do { - let result = try await coreService.checkGeoStatus() - - let newValue = result ?? false - - await MainActor.run { - self.isGeoBlocked = newValue - } - - Logger.info("Geo status check completed: isGeoBlocked=\(isGeoBlocked)", context: "GeoService") - } catch { - isGeoBlocked = false - Logger.error("Failed to check geo status: \(error)", context: "GeoService") - } - } -} diff --git a/Bitkit/Services/LightningService.swift b/Bitkit/Services/LightningService.swift index 0bfae088a..5c6e4d88d 100644 --- a/Bitkit/Services/LightningService.swift +++ b/Bitkit/Services/LightningService.swift @@ -366,22 +366,12 @@ class LightningService { return modificationDate } - func connectToTrustedPeers(remotePeers: [LnPeer]? = nil) async throws { + func connectToTrustedPeers() async throws { guard let node else { throw AppError(serviceError: .nodeNotSetup) } - let peers: [LnPeer] - let usingRemotePeers: Bool - if let remotePeers, !remotePeers.isEmpty { - Logger.info("Using \(remotePeers.count) trusted peers from Blocktank API") - peers = remotePeers - usingRemotePeers = true - } else { - Logger.warn("No remote peers available, falling back to preconfigured env peers") - peers = Env.trustedLnPeers - usingRemotePeers = false - } + let peers = Env.trustedLnPeers try await ServiceQueue.background(.ldk) { for peer in peers { @@ -392,35 +382,6 @@ class LightningService { Logger.error(error, context: "Peer: \(peer.nodeId)") } } - - if usingRemotePeers { - self.verifyTrustedPeersOrFallback(node: node, trustedPeers: peers) - } - } - } - - private func verifyTrustedPeersOrFallback(node: Node, trustedPeers: [LnPeer]) { - let connectedPeerIds = Set(node.listPeers().filter(\.isConnected).map(\.nodeId)) - let trustedConnected = trustedPeers.filter { connectedPeerIds.contains($0.nodeId) }.count - let trustedPeerIds = Set(trustedPeers.map(\.nodeId)) - - if trustedConnected == 0, !trustedPeers.isEmpty { - let fallbackPeers = Env.trustedLnPeers.filter { !trustedPeerIds.contains($0.nodeId) } - if fallbackPeers.isEmpty { - Logger.warn("No trusted peers connected. All preconfigured env peers overlap with API peers (not retrying with stale addresses).") - } else { - Logger.warn("No trusted peers connected, trying \(fallbackPeers.count) preconfigured env peer(s) not in API list") - for peer in fallbackPeers { - do { - try node.connect(nodeId: peer.nodeId, address: peer.address, persist: true) - Logger.info("Connected to fallback peer: \(peer.nodeId)") - } catch { - Logger.error(error, context: "Fallback peer: \(peer.nodeId)") - } - } - } - } else { - Logger.info("Connected to \(trustedConnected)/\(trustedPeers.count) trusted peers") } } diff --git a/Bitkit/Services/TransferService.swift b/Bitkit/Services/TransferService.swift index 46e83b734..d44294943 100644 --- a/Bitkit/Services/TransferService.swift +++ b/Bitkit/Services/TransferService.swift @@ -8,17 +8,14 @@ class TransferService { private let storage: TransferStorage private let lightningService: LightningService private let blocktankService: BlocktankService - private let isGeoBlocked: () -> Bool private let coreService: CoreService init( storage: TransferStorage = TransferStorage.shared, lightningService: LightningService, blocktankService: BlocktankService, - coreService: CoreService = .shared, - isGeoBlocked: @escaping () -> Bool = { GeoService.shared.isGeoBlocked } + coreService: CoreService = .shared ) { - self.isGeoBlocked = isGeoBlocked self.storage = storage self.lightningService = lightningService self.blocktankService = blocktankService @@ -58,17 +55,6 @@ class TransferService { guard existing.fundingTxId == fundingTxId else { throw OnchainSendAttemptError.duplicate } return existing.id } - // When geoblocked, block transfers to spending that involve LSP (Blocktank) - // toSpending with lspOrderId means it's a Blocktank LSP channel order - let isGeoblocked = isGeoBlocked() - if isGeoblocked && type.isToSpending() && lspOrderId != nil { - Logger.error("Cannot create LSP transfer when geoblocked", context: "TransferService") - throw AppError( - message: "Transfer unavailable", - debugMessage: "Transfer to spending via Blocktank is not available in your region." - ) - } - let id = UUID().uuidString let createdAt = UInt64(Date().timeIntervalSince1970) diff --git a/Bitkit/ViewModels/AppViewModel.swift b/Bitkit/ViewModels/AppViewModel.swift index 10049c9a3..971fba1ac 100644 --- a/Bitkit/ViewModels/AppViewModel.swift +++ b/Bitkit/ViewModels/AppViewModel.swift @@ -390,7 +390,6 @@ class AppViewModel: ObservableObject { setupManualEntryValidationDebounce() Task { - await checkGeoStatus() // Check for app updates on startup await AppUpdateService.shared.checkForAppUpdate() } @@ -529,11 +528,6 @@ class AppViewModel: ObservableObject { deinit {} - func checkGeoStatus() async { - // Delegate to GeoService singleton for centralized geo-blocking management - await GeoService.shared.checkGeoStatus() - } - func wipe() async throws { hasSeenContactsIntro = false hasSeenProfileIntro = false diff --git a/Bitkit/ViewModels/BlocktankViewModel.swift b/Bitkit/ViewModels/BlocktankViewModel.swift index 330e66cdf..47769dc30 100644 --- a/Bitkit/ViewModels/BlocktankViewModel.swift +++ b/Bitkit/ViewModels/BlocktankViewModel.swift @@ -65,69 +65,24 @@ class BlocktankViewModel: ObservableObject { private let lightningService: LightningService private let currencyService: CurrencyService private let orderClient: BlocktankOrderClient - private let refundAddressProvider: any BlocktankRefundAddressProviding - private var refreshTimer: Timer? - private var refreshTask: Task? init( coreService: CoreService = .shared, lightningService: LightningService = .shared, currencyService: CurrencyService = .shared, orderClient: BlocktankOrderClient? = nil, - refundAddressProvider: (any BlocktankRefundAddressProviding)? = nil, - startPolling: Bool = true + refreshInfoOnInit: Bool = true ) { self.coreService = coreService self.lightningService = lightningService self.currencyService = currencyService self.orderClient = orderClient ?? BlocktankOrderClient(coreService: coreService, lightningService: lightningService) - self.refundAddressProvider = refundAddressProvider ?? BlocktankRefundAddressProvider( - lightningService: lightningService, - utilityService: coreService.utility - ) - if startPolling { + if refreshInfoOnInit { Task { try? await refreshInfo() } - self.startPolling() - } - } - - deinit { - Task { @MainActor [weak self] in - Logger.debug("Stopping poll for orders") - self?.stopPolling() } } - private func startPolling() { - stopPolling() - - refreshTimer = Timer.scheduledTimer(withTimeInterval: Env.blocktankOrderRefreshInterval, repeats: true) { [weak self] _ in - Task { @MainActor [weak self] in - guard let self else { return } - refreshTask?.cancel() - refreshTask = Task { @MainActor [weak self] in - guard let self else { return } - try? await refreshOrders() - } - } - } - - // Initial refresh - refreshTask?.cancel() - refreshTask = Task { @MainActor [weak self] in - guard let self else { return } - try? await refreshOrders() - } - } - - private func stopPolling() { - refreshTimer?.invalidate() - refreshTimer = nil - refreshTask?.cancel() - refreshTask = nil - } - func refreshInfo() async throws { info = try await getInfo(refresh: false) // Instant set cached info to state before refreshing info = try await getInfo(refresh: true) @@ -352,11 +307,7 @@ class BlocktankViewModel: ObservableObject { throw CustomServiceError.nodeNotStarted } try Task.checkCancellation() - let refundAddress = try await refundAddressProvider.addressForOrder() - let options = try await defaultCreateOrderOptions( - clientBalanceSat: clientBalance, - refundOnchainAddress: refundAddress - ) + let options = try await defaultCreateOrderOptions(clientBalanceSat: clientBalance) Logger.debug( "Buying channel with lspBalanceSat: \(finalReceivingBalanceSats), clientBalanceSat: \(clientBalance), expiryWeeks: \(defaultChannelExpiryWeeks)" diff --git a/Bitkit/ViewModels/WalletViewModel.swift b/Bitkit/ViewModels/WalletViewModel.swift index 5fa2b32cb..4bf72ed13 100644 --- a/Bitkit/ViewModels/WalletViewModel.swift +++ b/Bitkit/ViewModels/WalletViewModel.swift @@ -96,14 +96,6 @@ class WalletViewModel: ObservableObject { private let feeEstimatesManager: FeeEstimatesManager private let pubkyProfile: PubkyProfileManager? - enum BlocktankPeerSimulation: String, CaseIterable { - case none = "None" - case apiFailure = "API Failure" - case unreachablePeers = "Unreachable Peers" - } - - static var peerSimulation: BlocktankPeerSimulation = .none - @Published var isRestoringWallet = false @Published var balanceInTransferToSavings: Int = 0 @Published var balanceInTransferToSpending: Int = 0 @@ -428,49 +420,9 @@ class WalletViewModel: ObservableObject { } } - private func fetchTrustedPeersFromBlocktank() async -> [LnPeer]? { - switch Self.peerSimulation { - case .apiFailure: - Logger.warn("⚠️ [DEBUG] Simulating Blocktank API failure — returning nil") - return nil - case .unreachablePeers: - Logger.warn("⚠️ [DEBUG] Simulating unreachable API peers") - return [ - LnPeer( - nodeId: "000000000000000000000000000000000000000000000000000000000000000001", - host: "192.0.2.1", - port: 9735 - ), - ] - case .none: - break - } - - var info: IBtInfo? - do { - info = try await coreService.blocktank.info(refresh: true) - } catch { - Logger.warn("Blocktank API refresh failed, trying cache: \(error)") - } - if info == nil { - info = try? await coreService.blocktank.info(refresh: false) - } - guard let nodes = info?.nodes, !nodes.isEmpty else { return nil } - let peers = nodes.compactMap { node -> LnPeer? in - guard let connString = node.connectionStrings.first else { return nil } - let address = connString.contains("@") ? String(connString.split(separator: "@").last ?? "") : connString - let parts = address.split(separator: ":") - guard parts.count == 2, let port = UInt16(parts[1]) else { return nil } - return LnPeer(nodeId: node.pubkey, host: String(parts[0]), port: port) - } - Logger.info("Fetched \(peers.count) trusted peers from Blocktank API") - return peers.isEmpty ? nil : peers - } - func reconnectTrustedPeers() async { do { - let remotePeers = await fetchTrustedPeersFromBlocktank() - try await lightningService.connectToTrustedPeers(remotePeers: remotePeers) + try await lightningService.connectToTrustedPeers() } catch { Logger.error("Failed to connect to trusted peers") } diff --git a/Bitkit/Views/Onboarding/OnboardingSlider.swift b/Bitkit/Views/Onboarding/OnboardingSlider.swift index 718fdfa16..26afa33ad 100644 --- a/Bitkit/Views/Onboarding/OnboardingSlider.swift +++ b/Bitkit/Views/Onboarding/OnboardingSlider.swift @@ -59,7 +59,6 @@ struct OnboardingSlider: View { imageName: "lightning", title: t("onboarding__slide1_header"), text: t("onboarding__slide1_text_alt"), - disclaimerText: GeoService.shared.isGeoBlocked ? t("onboarding__slide1_note") : nil, accentColor: .purpleAccent ) .tag(1) diff --git a/Bitkit/Views/Settings/LdkDebugScreen.swift b/Bitkit/Views/Settings/LdkDebugScreen.swift index e1076b378..3b36049aa 100644 --- a/Bitkit/Views/Settings/LdkDebugScreen.swift +++ b/Bitkit/Views/Settings/LdkDebugScreen.swift @@ -66,21 +66,6 @@ struct LdkDebugScreen: View { } } } - - // Peer Simulation - VStack(alignment: .leading, spacing: 8) { - SettingsSectionHeader("Peer Simulation") - - Picker("Peer Simulation", selection: Binding( - get: { WalletViewModel.peerSimulation }, - set: { WalletViewModel.peerSimulation = $0 } - )) { - ForEach(WalletViewModel.BlocktankPeerSimulation.allCases, id: \.self) { mode in - Text(mode.rawValue).tag(mode) - } - } - .pickerStyle(.segmented) - } } } } diff --git a/Bitkit/Views/Transfer/FundingOptions.swift b/Bitkit/Views/Transfer/FundingOptions.swift index 9b12354c0..7081479c5 100644 --- a/Bitkit/Views/Transfer/FundingOptions.swift +++ b/Bitkit/Views/Transfer/FundingOptions.swift @@ -8,14 +8,6 @@ struct FundingOptions: View { @State private var showNoFundsAlert = false - var text: String { - if GeoService.shared.isGeoBlocked { - return t("lightning__funding__text_blocked") - } else { - return t("lightning__funding__text") - } - } - var body: some View { VStack(spacing: 0) { NavigationBar(title: t("lightning__transfer__nav_title")) @@ -27,7 +19,7 @@ struct FundingOptions: View { ) .padding(.bottom, 8) - BodyMText(text) + BodyMText(t("lightning__funding__text")) .frame(maxWidth: .infinity, alignment: .leading) .padding(.bottom, 32) @@ -35,7 +27,6 @@ struct FundingOptions: View { RectangleButton( icon: "transfer", title: t("lightning__funding__button1"), - isDisabled: GeoService.shared.isGeoBlocked, testID: "FundTransfer" ) { if wallet.totalOnchainSats == 0 { @@ -50,7 +41,6 @@ struct FundingOptions: View { RectangleButton( icon: "qr", title: t("lightning__funding__button2"), - isDisabled: GeoService.shared.isGeoBlocked, testID: "FundReceive" ) { navigation.reset() @@ -83,8 +73,5 @@ struct FundingOptions: View { Text(t("lightning__no_funds__description")) } .tint(.purpleAccent) - .task { - await app.checkGeoStatus() - } } } diff --git a/Bitkit/Views/Wallets/HardwareWalletScreen.swift b/Bitkit/Views/Wallets/HardwareWalletScreen.swift index 69222c226..bcafd22e2 100644 --- a/Bitkit/Views/Wallets/HardwareWalletScreen.swift +++ b/Bitkit/Views/Wallets/HardwareWalletScreen.swift @@ -42,9 +42,6 @@ struct HardwareWalletScreen: View { .task(id: wallet?.walletId) { await loadActivities() } - .task { - await app.checkGeoStatus() - } .onReceive(activity.activitiesChangedPublisher) { _ in Task { await loadActivities() } } @@ -128,8 +125,7 @@ struct HardwareWalletScreen: View { .resizable() .scaledToFit() .frame(width: 16, height: 16) - .foregroundColor(.white80), - isDisabled: GeoService.shared.isGeoBlocked + .foregroundColor(.white80) ) { sheets.showSheet(.channelsUnavailable) } diff --git a/Bitkit/Views/Wallets/Receive/ReceiveQr.swift b/Bitkit/Views/Wallets/Receive/ReceiveQr.swift index 16bc0b2cf..5b7e02653 100644 --- a/Bitkit/Views/Wallets/Receive/ReceiveQr.swift +++ b/Bitkit/Views/Wallets/Receive/ReceiveQr.swift @@ -5,7 +5,6 @@ struct ReceiveQr: View { @EnvironmentObject private var settings: SettingsViewModel @EnvironmentObject private var app: AppViewModel - @EnvironmentObject private var blocktank: BlocktankViewModel @EnvironmentObject private var wallet: WalletViewModel @Environment(UsdtWalletManager.self) private var usdt @Environment(HwWalletManager.self) private var hwWalletManager @@ -297,16 +296,7 @@ struct ReceiveQr: View { abandonHardwareVerification() } .task { - do { - try await withThrowingTaskGroup(of: Void.self) { group in - group.addTask { await refreshBip21() } - group.addTask { try await blocktank.refreshInfo() } - try await group.waitForAll() - } - } catch { - app.toast(error) - } - await app.checkGeoStatus() + await refreshBip21() } .onChange(of: wallet.nodeLifecycleState) { _, newState in // They may open this view before node has started diff --git a/Bitkit/Views/Wallets/SavingsWalletScreen.swift b/Bitkit/Views/Wallets/SavingsWalletScreen.swift index ff981ad05..a4f01ec84 100644 --- a/Bitkit/Views/Wallets/SavingsWalletScreen.swift +++ b/Bitkit/Views/Wallets/SavingsWalletScreen.swift @@ -50,11 +50,9 @@ struct SavingsWalletScreen: View { } if !shouldShowOnboarding { - if !GeoService.shared.isGeoBlocked { - transferButton - .transition(.move(edge: .leading).combined(with: .opacity)) - .padding(.top, 28) - } + transferButton + .transition(.move(edge: .leading).combined(with: .opacity)) + .padding(.top, 28) ActivityList(viewType: .onchain) diff --git a/Bitkit/Views/Wallets/SpendingWalletScreen.swift b/Bitkit/Views/Wallets/SpendingWalletScreen.swift index 01e1073e4..17622a545 100644 --- a/Bitkit/Views/Wallets/SpendingWalletScreen.swift +++ b/Bitkit/Views/Wallets/SpendingWalletScreen.swift @@ -44,7 +44,7 @@ struct SpendingWalletScreen: View { .accessibilityIdentifier("ChannelsClosingNotice") } - if shouldShowOnboarding && wallet.totalOnchainSats > 0 && !GeoService.shared.isGeoBlocked { + if shouldShowOnboarding && wallet.totalOnchainSats > 0 { transferFromSavingsButton .transition(.move(edge: .leading).combined(with: .opacity)) .padding(.top, 28) diff --git a/BitkitTests/BlocktankRefundAddressLiveIntegrationTests.swift b/BitkitTests/BlocktankRefundAddressLiveIntegrationTests.swift deleted file mode 100644 index 0deb4d7fa..000000000 --- a/BitkitTests/BlocktankRefundAddressLiveIntegrationTests.swift +++ /dev/null @@ -1,185 +0,0 @@ -@testable import Bitkit -import BitkitCore -import LDKNode -import XCTest - -@MainActor -final class BlocktankRefundAddressLiveIntegrationTests: XCTestCase { - private let walletIndex = 0 - private let lightning = Bitkit.LightningService.shared - private let core = Bitkit.CoreService.shared - - override func setUp() async throws { - try await super.setUp() - // `resetToDefaults()` in both hooks writes ~30 real keys, and `clear()` drops the user's own - // refund address. The keychain wipe and LDK storage are namespaced under test; these are not. - snapshotAppDefaultsDomain() - // A running node persists address-search indexes as transactions arrive. Teardown blocks run - // last-in, first-out, so stopping it here lands before the restore above; `tearDown()` runs - // after both and would stop it too late. - addTeardownBlock { [lightning] in - let isRunning = await MainActor.run { lightning.status?.isRunning == true } - if isRunning { - try? await lightning.stop() - } - } - try Bitkit.Keychain.wipeEntireKeychain() - Bitkit.SettingsViewModel.shared.resetToDefaults() - Bitkit.BlocktankRefundAddressStore().clear() - } - - override func tearDown() async throws { - lightning.dumpLdkLogs() - if lightning.status?.isRunning == true { - try? await lightning.stop() - } - try? await lightning.wipeStorage(walletIndex: walletIndex) - try Bitkit.Keychain.wipeEntireKeychain() - try await super.tearDown() - } - - func testRefundAddressLifecycleAcrossOrdersRestartAndVssRestore() async throws { - guard Bitkit.Env.network == .regtest else { - throw XCTSkip("Blocktank refund address integration requires regtest") - } - - let settings = Bitkit.SettingsViewModel.shared - settings.selectedAddressType = .nativeSegwit - settings.addressTypesToMonitor = [.nativeSegwit] - - let mnemonic = try Bitkit.StartupHandler.createNewWallet(bip39Passphrase: nil, walletIndex: walletIndex) - XCTAssertFalse(mnemonic.isEmpty) - try await lightning.setup(walletIndex: walletIndex) - try await lightning.start() - try await lightning.sync() - XCTAssertNotNil(lightning.nodeId) - - var submittedOptions: [CreateOrderOptions] = [] - var estimatedOptions: [CreateOrderOptions] = [] - let makeViewModel = { - let client = Bitkit.BlocktankOrderClient( - nodeId: { self.lightning.nodeId }, - sign: { try await self.lightning.sign(message: $0) }, - submit: { lspBalanceSat, expiryWeeks, options in - submittedOptions.append(options) - return try await self.core.blocktank.newOrder( - lspBalanceSat: lspBalanceSat, - channelExpiryWeeks: expiryWeeks, - options: options - ) - }, - estimate: { lspBalanceSat, expiryWeeks, options in - estimatedOptions.append(options) - return try await self.core.blocktank.estimateFee( - lspBalanceSat: lspBalanceSat, - channelExpiryWeeks: expiryWeeks, - options: options - ) - } - ) - return Bitkit.BlocktankViewModel( - coreService: self.core, - lightningService: self.lightning, - orderClient: client, - startPolling: false - ) - } - - var viewModel = makeViewModel() - for _ in 0 ..< 5 { - _ = try await viewModel.estimateOrderFee(clientBalance: 0, lspBalance: 100_000) - } - XCTAssertEqual(estimatedOptions.count, 5) - XCTAssertTrue(estimatedOptions.allSatisfy { $0.refundOnchainAddress == nil }) - XCTAssertNil(try Bitkit.BlocktankRefundAddressStore().load(), "Fee estimates must not allocate refund addresses") - - let firstOrder = try await viewModel.createOrder(clientBalance: 0, lspBalance: 100_000) - let firstRefund = try XCTUnwrap(Bitkit.BlocktankRefundAddressStore().load()) - XCTAssertEqual(submittedOptions.last?.refundOnchainAddress, firstRefund.address) - XCTAssertTrue(firstRefund.address.hasPrefix("bcrt1q")) - - let clientFundedOrder = try await viewModel.createOrder(clientBalance: 25000, lspBalance: 100_000) - XCTAssertEqual(submittedOptions.last?.refundOnchainAddress, firstRefund.address) - let derivedFirst = try await lightning.addressInfoForType( - .nativeSegwit, - keychain: .external, - atIndex: firstRefund.index - ) - XCTAssertEqual(derivedFirst.address, firstRefund.address, "Client-funded orders must refund to the internal Bitkit wallet") - - do { - _ = try await viewModel.createOrder(clientBalance: 0, lspBalance: 1) - XCTFail("Expected the staging backend to reject an undersized order") - } catch { - XCTAssertEqual(try Bitkit.BlocktankRefundAddressStore().load(), firstRefund) - } - - try await lightning.stop() - try await lightning.setup(walletIndex: walletIndex) - try await lightning.start() - try await lightning.sync() - viewModel = makeViewModel() - - let restartedOrder = try await viewModel.createOrder(clientBalance: 0, lspBalance: 100_000) - XCTAssertEqual(submittedOptions.last?.refundOnchainAddress, firstRefund.address) - XCTAssertEqual(try Bitkit.BlocktankRefundAddressStore().load(), firstRefund) - - let appCache = try settings.getAppCacheData() - try await lightning.stop() - try await lightning.wipeStorage(walletIndex: walletIndex) - Bitkit.BlocktankRefundAddressStore().clear() - settings.selectedAddressType = .taproot - settings.addressTypesToMonitor = [.taproot] - try settings.restoreAppCacheData(appCache) - XCTAssertEqual(try Bitkit.BlocktankRefundAddressStore().load(), firstRefund) - - try await lightning.setup(walletIndex: walletIndex) - try await lightning.start() - try await lightning.sync() - await settings.pruneEmptyAddressTypesAfterRestore() - XCTAssertTrue(settings.addressTypesToMonitor.contains(.nativeSegwit)) - - viewModel = makeViewModel() - let restoredOrder = try await viewModel.createOrder(clientBalance: 0, lspBalance: 100_000) - XCTAssertEqual(submittedOptions.last?.refundOnchainAddress, firstRefund.address) - XCTAssertEqual(try Bitkit.BlocktankRefundAddressStore().load(), firstRefund) - let addressWasUsedBeforeDeposit = try await core.utility.isAddressUsed(address: firstRefund.address) - XCTAssertFalse(addressWasUsedBeforeDeposit) - - let refundDepositTxId = try await core.blocktank.regtestDepositFunds(address: firstRefund.address, amountSat: 10000) - XCTAssertFalse(refundDepositTxId.isEmpty) - try await core.blocktank.regtestMineBlocks(3) - - let activityDeadline = Date().addingTimeInterval(120) - var recordedPayment = false - repeat { - try await lightning.sync() - if let payments = await lightning.listPayments() { - try await core.activity.syncLdkNodePayments(payments) - } - recordedPayment = try await core.utility.isAddressUsed(address: firstRefund.address) - if !recordedPayment { - try await Task.sleep(nanoseconds: 3_000_000_000) - } - } while !recordedPayment && Date() < activityDeadline - XCTAssertTrue(recordedPayment, "Incoming refund-address payment must be recorded in the local activity database") - - let rotatedOrder = try await viewModel.createOrder(clientBalance: 0, lspBalance: 100_000) - let rotatedRefund = try XCTUnwrap(Bitkit.BlocktankRefundAddressStore().load()) - XCTAssertEqual(rotatedRefund.index, firstRefund.index + 1) - XCTAssertNotEqual(rotatedRefund.address, firstRefund.address) - XCTAssertEqual(submittedOptions.last?.refundOnchainAddress, rotatedRefund.address) - - let reusedRotatedOrder = try await viewModel.createOrder(clientBalance: 0, lspBalance: 100_000) - XCTAssertEqual(try Bitkit.BlocktankRefundAddressStore().load(), rotatedRefund) - XCTAssertEqual(submittedOptions.last?.refundOnchainAddress, rotatedRefund.address) - - Bitkit.Logger.test( - "Refund lifecycle orders: first=\(firstOrder.id), clientFunded=\(clientFundedOrder.id), restart=\(restartedOrder.id), " + - "restore=\(restoredOrder.id), rotated=\(rotatedOrder.id), rotatedReuse=\(reusedRotatedOrder.id); " + - "refundAddress=\(firstRefund.address), refundIndex=\(firstRefund.index), refundDepositTxId=\(refundDepositTxId), " + - "rotatedAddress=\(rotatedRefund.address), rotatedIndex=\(rotatedRefund.index)", - context: "BlocktankRefundAddressLiveIntegrationTests" - ) - } -} diff --git a/BitkitTests/BlocktankRefundAddressProviderTests.swift b/BitkitTests/BlocktankRefundAddressProviderTests.swift deleted file mode 100644 index 945332c8e..000000000 --- a/BitkitTests/BlocktankRefundAddressProviderTests.swift +++ /dev/null @@ -1,538 +0,0 @@ -@testable import Bitkit -import BitkitCore -import LDKNode -import XCTest - -@MainActor -final class BlocktankRefundAddressProviderTests: XCTestCase { - private enum StubError: Error { - case lookup - case reveal - case persist - case usage - case estimate - case submit - } - - private final class State { - var cached: BlocktankRefundAddress? - var usedAddresses: Set = [] - var allocationCount = 0 - var lookupCount = 0 - var revealCount = 0 - } - - private final class StubRefundProvider: BlocktankRefundAddressProviding { - var callCount = 0 - var result: Result - - init(result: Result) { - self.result = result - } - - func addressForOrder() async throws -> String { - callCount += 1 - return try result.get() - } - } - - override func setUp() { - super.setUp() - // `clear()` here and in tearDown, `restoreAppCacheData`, `resetToDefaults()` and the corrupt - // cache fixture all write UserDefaults.standard — the host app's own preferences. - snapshotAppDefaultsDomain() - BlocktankRefundAddressStore().clear() - } - - private func makeProvider( - state: State, - lookup: ((UInt32) async throws -> BlocktankRefundAddress)? = nil, - reveal: ((UInt32) async throws -> Void)? = nil, - save: ((BlocktankRefundAddress) throws -> Void)? = nil, - isUsed: ((String) async throws -> Bool)? = nil, - allocate: (() async throws -> BlocktankRefundAddress)? = nil, - allocationDelayNanoseconds: UInt64 = 0 - ) -> BlocktankRefundAddressProvider { - BlocktankRefundAddressProvider( - load: { state.cached }, - save: save ?? { state.cached = $0 }, - lookup: lookup ?? { index in - state.lookupCount += 1 - guard let cached = state.cached else { throw StubError.lookup } - return BlocktankRefundAddress(address: cached.address, index: index) - }, - reveal: reveal ?? { _ in state.revealCount += 1 }, - isUsed: isUsed ?? { state.usedAddresses.contains($0) }, - allocate: allocate ?? { - if allocationDelayNanoseconds > 0 { - try await Task.sleep(nanoseconds: allocationDelayNanoseconds) - } - let index = UInt32(state.allocationCount) - state.allocationCount += 1 - return BlocktankRefundAddress(address: "refund-\(index)", index: index) - } - ) - } - - func testUnpaidAndFailedOrdersReuseOneAddressAcrossProviderRestarts() async throws { - let state = State() - - for _ in 0 ..< 8 { - let provider = makeProvider(state: state) - let address = try await provider.addressForOrder() - XCTAssertEqual(address, "refund-0") - } - - XCTAssertEqual(state.allocationCount, 1) - XCTAssertEqual(state.cached, BlocktankRefundAddress(address: "refund-0", index: 0)) - } - - func testRecordedPaymentRotatesExactlyOnce() async throws { - let state = State() - let provider = makeProvider(state: state) - - let firstAddress = try await provider.addressForOrder() - XCTAssertEqual(firstAddress, "refund-0") - state.usedAddresses.insert("refund-0") - - let rotatedAddress = try await provider.addressForOrder() - let reusedAddress = try await provider.addressForOrder() - XCTAssertEqual(rotatedAddress, "refund-1") - XCTAssertEqual(reusedAddress, "refund-1") - XCTAssertEqual(state.allocationCount, 2) - XCTAssertEqual(state.cached, BlocktankRefundAddress(address: "refund-1", index: 1)) - } - - func testAllocationSkipsUsedCandidatesUntilFirstUnusedAddress() async throws { - let state = State() - state.usedAddresses = ["refund-0", "refund-1"] - let provider = makeProvider(state: state) - - let address = try await provider.addressForOrder() - - XCTAssertEqual(address, "refund-2") - XCTAssertEqual(state.allocationCount, 3) - XCTAssertEqual(state.cached, BlocktankRefundAddress(address: "refund-2", index: 2)) - } - - func testAllocationStopsAfterTwentyUsedCandidatesAndPreservesCache() async { - let state = State() - state.cached = BlocktankRefundAddress(address: "refund-0", index: 0) - state.allocationCount = 1 - state.usedAddresses = Set((0 ... 20).map { "refund-\($0)" }) - let provider = makeProvider(state: state) - - await XCTAssertThrowsErrorAsync({ try await provider.addressForOrder() }) { error in - XCTAssertEqual(error as? BlocktankRefundAddressError, .allocationAttemptsExhausted) - } - XCTAssertEqual(state.allocationCount, 21) - XCTAssertEqual(state.cached, BlocktankRefundAddress(address: "refund-0", index: 0)) - } - - func testAllocationUsageCheckFailurePreservesCache() async { - let state = State() - state.cached = BlocktankRefundAddress(address: "refund-0", index: 0) - state.allocationCount = 1 - let provider = makeProvider(state: state, isUsed: { address in - if address == "refund-0" { - return true - } - throw StubError.usage - }) - - await XCTAssertThrowsErrorAsync({ try await provider.addressForOrder() }) { error in - XCTAssertTrue(error is StubError) - } - XCTAssertEqual(state.allocationCount, 2) - XCTAssertEqual(state.cached, BlocktankRefundAddress(address: "refund-0", index: 0)) - } - - func testAllocationRejectsNonAdvancingIndex() async { - let state = State() - var candidates = [ - BlocktankRefundAddress(address: "refund-used", index: 7), - BlocktankRefundAddress(address: "refund-unused", index: 7), - ] - let provider = makeProvider( - state: state, - isUsed: { $0 == "refund-used" }, - allocate: { candidates.removeFirst() } - ) - - await XCTAssertThrowsErrorAsync({ try await provider.addressForOrder() }) { error in - XCTAssertEqual(error as? BlocktankRefundAddressError, .allocationDidNotAdvance) - } - XCTAssertNil(state.cached) - } - - func testConcurrentCallsCoalesceOneAllocation() async throws { - let state = State() - let provider = makeProvider(state: state, allocationDelayNanoseconds: 50_000_000) - - async let first = provider.addressForOrder() - async let second = provider.addressForOrder() - async let third = provider.addressForOrder() - let addresses = try await [first, second, third] - - XCTAssertEqual(addresses, ["refund-0", "refund-0", "refund-0"]) - XCTAssertEqual(state.allocationCount, 1) - } - - func testCachedAddressIsLookedUpAndRevealedBeforeReuse() async throws { - let state = State() - state.cached = BlocktankRefundAddress(address: "refund-7", index: 7) - let provider = makeProvider(state: state) - - let address = try await provider.addressForOrder() - XCTAssertEqual(address, "refund-7") - XCTAssertEqual(state.lookupCount, 1) - XCTAssertEqual(state.revealCount, 1) - XCTAssertEqual(state.allocationCount, 0) - } - - func testOwnershipMismatchIsRejected() async { - let state = State() - state.cached = BlocktankRefundAddress(address: "other-wallet", index: 7) - let provider = makeProvider(state: state, lookup: { index in - BlocktankRefundAddress(address: "active-wallet", index: index) - }) - - await XCTAssertThrowsErrorAsync({ try await provider.addressForOrder() }) { error in - XCTAssertEqual(error as? BlocktankRefundAddressError, .ownershipMismatch) - } - XCTAssertEqual(state.allocationCount, 0) - } - - func testHardenedIndexIsRejected() async { - let state = State() - state.cached = BlocktankRefundAddress(address: "refund", index: UInt32(Int32.max) + 1) - let provider = makeProvider(state: state) - - await XCTAssertThrowsErrorAsync({ try await provider.addressForOrder() }) { error in - XCTAssertEqual(error as? BlocktankRefundAddressError, .indexOutOfRange(UInt32(Int32.max) + 1)) - } - XCTAssertEqual(state.lookupCount, 0) - XCTAssertEqual(state.allocationCount, 0) - } - - func testLookupFailureDoesNotAllocate() async { - let state = State() - state.cached = BlocktankRefundAddress(address: "refund", index: 1) - let provider = makeProvider(state: state, lookup: { _ in throw StubError.lookup }) - - await XCTAssertThrowsErrorAsync { try await provider.addressForOrder() } - XCTAssertEqual(state.allocationCount, 0) - } - - func testRevealFailureDoesNotAllocate() async { - let state = State() - state.cached = BlocktankRefundAddress(address: "refund", index: 1) - let provider = makeProvider(state: state, reveal: { _ in throw StubError.reveal }) - - await XCTAssertThrowsErrorAsync { try await provider.addressForOrder() } - XCTAssertEqual(state.allocationCount, 0) - } - - func testCachePersistenceFailureReturnsNoAddress() async { - let state = State() - let provider = makeProvider(state: state, save: { _ in throw StubError.persist }) - - await XCTAssertThrowsErrorAsync { try await provider.addressForOrder() } - XCTAssertEqual(state.allocationCount, 1) - XCTAssertNil(state.cached) - } - - func testAppCacheSerializationMatchesSharedOptionalShape() throws { - let json = #"{"blocktankRefundAddress":{"address":"bcrt1refund","index":7}}"# - let cache = try JSONDecoder().decode(AppCacheData.self, from: Data(json.utf8)) - - XCTAssertEqual(cache.blocktankRefundAddress, BlocktankRefundAddress(address: "bcrt1refund", index: 7)) - - let encoded = try JSONEncoder().encode(cache) - let object = try XCTUnwrap(JSONSerialization.jsonObject(with: encoded) as? [String: Any]) - let refund = try XCTUnwrap(object["blocktankRefundAddress"] as? [String: Any]) - XCTAssertEqual(refund["address"] as? String, "bcrt1refund") - XCTAssertEqual(refund["index"] as? Int, 7) - } - - func testOlderAppCacheWithoutRefundAddressDecodesAsNil() throws { - let cache = try JSONDecoder().decode(AppCacheData.self, from: Data("{}".utf8)) - - XCTAssertNil(cache.blocktankRefundAddress) - } - - func testStoreKeepsRefundAddressesSeparateByNetwork() throws { - let defaults = try XCTUnwrap(UserDefaults(suiteName: #function)) - defer { defaults.removePersistentDomain(forName: #function) } - let regtestStore = BlocktankRefundAddressStore(defaults: defaults, network: .regtest) - let bitcoinStore = BlocktankRefundAddressStore(defaults: defaults, network: .bitcoin) - let regtestAddress = BlocktankRefundAddress(address: "bcrt1qrefund", index: 7) - let bitcoinAddress = BlocktankRefundAddress(address: "bc1qrefund", index: 9) - - try regtestStore.save(regtestAddress) - XCTAssertEqual(try regtestStore.load(), regtestAddress) - XCTAssertNil(try bitcoinStore.load()) - - try bitcoinStore.save(bitcoinAddress) - XCTAssertEqual(try bitcoinStore.load(), bitcoinAddress) - XCTAssertEqual(try regtestStore.load(), regtestAddress) - } - - func testLegacyCacheMigratesOnlyWhenPrefixIdentifiesNetwork() throws { - let cases: [(LDKNode.Network, String)] = [ - (.bitcoin, " bc1qrefund "), - (.regtest, " bcrt1qrefund "), - ] - - for (offset, testCase) in cases.enumerated() { - let suiteName = "\(#function)-\(offset)" - let defaults = try XCTUnwrap(UserDefaults(suiteName: suiteName)) - defer { defaults.removePersistentDomain(forName: suiteName) } - let legacy = BlocktankRefundAddress(address: testCase.1, index: 7) - try defaults.set(JSONEncoder().encode(legacy), forKey: BlocktankRefundAddressStore.legacyKey) - - let store = BlocktankRefundAddressStore(defaults: defaults, network: testCase.0) - XCTAssertEqual(try store.load(), legacy) - XCTAssertNil(defaults.object(forKey: BlocktankRefundAddressStore.legacyKey)) - let migrated = try XCTUnwrap( - defaults.data(forKey: BlocktankRefundAddressStore.key(for: testCase.0)) - ) - XCTAssertEqual( - try JSONDecoder().decode(BlocktankRefundAddress.self, from: migrated), - legacy - ) - } - } - - func testClearPreservesLegacyCacheFromAnotherNetwork() throws { - let defaults = try XCTUnwrap(UserDefaults(suiteName: #function)) - defer { defaults.removePersistentDomain(forName: #function) } - let legacy = BlocktankRefundAddress(address: "bcrt1qrefund", index: 7) - try defaults.set(JSONEncoder().encode(legacy), forKey: BlocktankRefundAddressStore.legacyKey) - - BlocktankRefundAddressStore(defaults: defaults, network: .bitcoin).clear() - XCTAssertNotNil(defaults.data(forKey: BlocktankRefundAddressStore.legacyKey)) - - BlocktankRefundAddressStore(defaults: defaults, network: .regtest).clear() - XCTAssertNil(defaults.object(forKey: BlocktankRefundAddressStore.legacyKey)) - } - - func testAmbiguousTestnetLegacyCacheIsPreserved() throws { - let defaults = try XCTUnwrap(UserDefaults(suiteName: #function)) - defer { defaults.removePersistentDomain(forName: #function) } - let legacy = BlocktankRefundAddress(address: "tb1qrefund", index: 7) - try defaults.set(JSONEncoder().encode(legacy), forKey: BlocktankRefundAddressStore.legacyKey) - - XCTAssertNil(try BlocktankRefundAddressStore(defaults: defaults, network: .testnet).load()) - XCTAssertNil(try BlocktankRefundAddressStore(defaults: defaults, network: .signet).load()) - BlocktankRefundAddressStore(defaults: defaults, network: .testnet).clear() - BlocktankRefundAddressStore(defaults: defaults, network: .signet).clear() - XCTAssertNotNil(defaults.data(forKey: BlocktankRefundAddressStore.legacyKey)) - } - - func testScopedCacheTakesPrecedenceOverLegacyCache() throws { - let defaults = try XCTUnwrap(UserDefaults(suiteName: #function)) - defer { defaults.removePersistentDomain(forName: #function) } - let store = BlocktankRefundAddressStore(defaults: defaults, network: .regtest) - let scoped = BlocktankRefundAddress(address: "bcrt1qscoped", index: 8) - let legacy = BlocktankRefundAddress(address: "bc1qlegacy", index: 7) - try store.save(scoped) - try defaults.set(JSONEncoder().encode(legacy), forKey: BlocktankRefundAddressStore.legacyKey) - - XCTAssertEqual(try store.load(), scoped) - XCTAssertNotNil(defaults.data(forKey: BlocktankRefundAddressStore.legacyKey)) - } - - func testUnknownLegacyCacheFailsClosedAndIsCleared() throws { - let defaults = try XCTUnwrap(UserDefaults(suiteName: #function)) - defer { defaults.removePersistentDomain(forName: #function) } - let legacy = BlocktankRefundAddress(address: "unknown", index: 7) - try defaults.set(JSONEncoder().encode(legacy), forKey: BlocktankRefundAddressStore.legacyKey) - let store = BlocktankRefundAddressStore(defaults: defaults, network: .regtest) - - XCTAssertThrowsError(try store.load()) { error in - XCTAssertEqual(error as? BlocktankRefundAddressError, .invalidCache) - } - store.clear() - XCTAssertNil(defaults.object(forKey: BlocktankRefundAddressStore.legacyKey)) - } - - func testSettingsCacheRestoreRoundTripAndWipe() throws { - let json = #"{"blocktankRefundAddress":{"address":"bcrt1refund","index":7}}"# - let restored = try JSONDecoder().decode(AppCacheData.self, from: Data(json.utf8)) - - try SettingsViewModel.shared.restoreAppCacheData(restored) - - XCTAssertEqual( - try SettingsViewModel.shared.getAppCacheData().blocktankRefundAddress, - BlocktankRefundAddress(address: "bcrt1refund", index: 7) - ) - XCTAssertTrue(SettingsBackupConfig.appStateKeys.contains(BlocktankRefundAddressStore.key)) - - SettingsViewModel.shared.resetToDefaults() - XCTAssertNil(try BlocktankRefundAddressStore().load()) - } - - func testCorruptLocalCacheIsRejected() throws { - UserDefaults.standard.set(Data("not-json".utf8), forKey: BlocktankRefundAddressStore.key) - - XCTAssertThrowsError(try BlocktankRefundAddressStore().load()) { error in - XCTAssertEqual(error as? BlocktankRefundAddressError, .invalidCache) - } - } - - func testEstimatesNeverResolveRefundAddress() async { - let refundProvider = StubRefundProvider(result: .success("refund")) - var estimateCount = 0 - let viewModel = makeViewModel(refundProvider: refundProvider, estimate: { _, _, _ in - estimateCount += 1 - throw StubError.estimate - }) - - for _ in 0 ..< 8 { - await XCTAssertThrowsErrorAsync { - try await viewModel.estimateOrderFee(clientBalance: 1, lspBalance: 2) - } - } - - XCTAssertEqual(refundProvider.callCount, 0) - XCTAssertEqual(estimateCount, 8) - } - - func testCreateOrderSubmitsResolvedRefundAddress() async { - let refundProvider = StubRefundProvider(result: .success("refund-address")) - var submittedOptions: CreateOrderOptions? - let viewModel = makeViewModel(refundProvider: refundProvider, submit: { _, _, options in - submittedOptions = options - throw StubError.submit - }) - - await XCTAssertThrowsErrorAsync { - try await viewModel.createOrder(clientBalance: 1, lspBalance: 2) - } - - XCTAssertEqual(refundProvider.callCount, 1) - XCTAssertEqual(submittedOptions?.refundOnchainAddress, "refund-address") - } - - func testRefundResolutionFailureBlocksSubmission() async { - let refundProvider = StubRefundProvider(result: .failure(StubError.lookup)) - var submitCount = 0 - let viewModel = makeViewModel(refundProvider: refundProvider, submit: { _, _, _ in - submitCount += 1 - throw StubError.submit - }) - - await XCTAssertThrowsErrorAsync { - try await viewModel.createOrder(clientBalance: 1, lspBalance: 2) - } - - XCTAssertEqual(submitCount, 0) - } - - func testMissingNodeBlocksResolutionAndSubmission() async { - let refundProvider = StubRefundProvider(result: .success("refund")) - var submitCount = 0 - let orderClient = BlocktankOrderClient( - nodeId: { nil }, - sign: { _ in "signature" }, - submit: { _, _, _ in - submitCount += 1 - throw StubError.submit - }, - estimate: { _, _, _ in throw StubError.estimate } - ) - let viewModel = BlocktankViewModel( - orderClient: orderClient, - refundAddressProvider: refundProvider, - startPolling: false - ) - - await XCTAssertThrowsErrorAsync { - try await viewModel.createOrder(clientBalance: 1, lspBalance: 2) - } - - XCTAssertEqual(refundProvider.callCount, 0) - XCTAssertEqual(submitCount, 0) - } - - func testCancellationBeforeResolutionBlocksAllocationAndSubmission() async { - let refundProvider = StubRefundProvider(result: .success("refund")) - var submitCount = 0 - let viewModel = makeViewModel(refundProvider: refundProvider, submit: { _, _, _ in - submitCount += 1 - throw StubError.submit - }) - - let task = Task { @MainActor in - withUnsafeCurrentTask { $0?.cancel() } - return try await viewModel.createOrder(clientBalance: 1, lspBalance: 2) - } - - await XCTAssertThrowsErrorAsync({ try await task.value }) { error in - XCTAssertTrue(error is CancellationError) - } - XCTAssertEqual(refundProvider.callCount, 0) - XCTAssertEqual(submitCount, 0) - } - - func testCancelledWaiterDoesNotSubmitWhileOtherWaiterCompletesResolution() async { - let state = State() - let provider = makeProvider(state: state, allocationDelayNanoseconds: 100_000_000) - var submitCount = 0 - let viewModel = makeViewModel(refundProvider: provider, submit: { _, _, _ in - submitCount += 1 - throw StubError.submit - }) - - let cancelled = Task { @MainActor in - try await viewModel.createOrder(clientBalance: 1, lspBalance: 2) - } - await Task.yield() - let active = Task { @MainActor in - try await viewModel.createOrder(clientBalance: 1, lspBalance: 2) - } - cancelled.cancel() - - await XCTAssertThrowsErrorAsync({ try await cancelled.value }) { error in - XCTAssertTrue(error is CancellationError) - } - await XCTAssertThrowsErrorAsync({ try await active.value }) { error in - XCTAssertTrue(error is StubError) - } - XCTAssertEqual(state.allocationCount, 1) - XCTAssertEqual(submitCount, 1) - } - - private func makeViewModel( - refundProvider: any BlocktankRefundAddressProviding, - submit: @escaping BlocktankOrderClient.Submit = { _, _, _ in throw StubError.submit }, - estimate: @escaping BlocktankOrderClient.Estimate = { _, _, _ in throw StubError.estimate } - ) -> BlocktankViewModel { - let orderClient = BlocktankOrderClient( - nodeId: { "node-id" }, - sign: { _ in "signature" }, - submit: submit, - estimate: estimate - ) - return BlocktankViewModel( - orderClient: orderClient, - refundAddressProvider: refundProvider, - startPolling: false - ) - } - - private func XCTAssertThrowsErrorAsync( - _ expression: () async throws -> some Any, - _ errorHandler: (Error) -> Void = { _ in }, - file: StaticString = #filePath, - line: UInt = #line - ) async { - do { - _ = try await expression() - XCTFail("Expected error", file: file, line: line) - } catch { - errorHandler(error) - } - } -} diff --git a/BitkitTests/BlocktankRefundAddressStoreTests.swift b/BitkitTests/BlocktankRefundAddressStoreTests.swift new file mode 100644 index 000000000..28d311839 --- /dev/null +++ b/BitkitTests/BlocktankRefundAddressStoreTests.swift @@ -0,0 +1,153 @@ +@testable import Bitkit +import LDKNode +import XCTest + +@MainActor +final class BlocktankRefundAddressStoreTests: XCTestCase { + override func setUp() { + super.setUp() + // `clear()` here and in tearDown, `restoreAppCacheData`, `resetToDefaults()` and the corrupt + // cache fixture all write UserDefaults.standard — the host app's own preferences. + snapshotAppDefaultsDomain() + BlocktankRefundAddressStore().clear() + } + + func testAppCacheSerializationMatchesSharedOptionalShape() throws { + let json = #"{"blocktankRefundAddress":{"address":"bcrt1refund","index":7}}"# + let cache = try JSONDecoder().decode(AppCacheData.self, from: Data(json.utf8)) + + XCTAssertEqual(cache.blocktankRefundAddress, BlocktankRefundAddress(address: "bcrt1refund", index: 7)) + + let encoded = try JSONEncoder().encode(cache) + let object = try XCTUnwrap(JSONSerialization.jsonObject(with: encoded) as? [String: Any]) + let refund = try XCTUnwrap(object["blocktankRefundAddress"] as? [String: Any]) + XCTAssertEqual(refund["address"] as? String, "bcrt1refund") + XCTAssertEqual(refund["index"] as? Int, 7) + } + + func testOlderAppCacheWithoutRefundAddressDecodesAsNil() throws { + let cache = try JSONDecoder().decode(AppCacheData.self, from: Data("{}".utf8)) + + XCTAssertNil(cache.blocktankRefundAddress) + } + + func testStoreKeepsRefundAddressesSeparateByNetwork() throws { + let defaults = try XCTUnwrap(UserDefaults(suiteName: #function)) + defer { defaults.removePersistentDomain(forName: #function) } + let regtestStore = BlocktankRefundAddressStore(defaults: defaults, network: .regtest) + let bitcoinStore = BlocktankRefundAddressStore(defaults: defaults, network: .bitcoin) + let regtestAddress = BlocktankRefundAddress(address: "bcrt1qrefund", index: 7) + let bitcoinAddress = BlocktankRefundAddress(address: "bc1qrefund", index: 9) + + try regtestStore.save(regtestAddress) + XCTAssertEqual(try regtestStore.load(), regtestAddress) + XCTAssertNil(try bitcoinStore.load()) + + try bitcoinStore.save(bitcoinAddress) + XCTAssertEqual(try bitcoinStore.load(), bitcoinAddress) + XCTAssertEqual(try regtestStore.load(), regtestAddress) + } + + func testLegacyCacheMigratesOnlyWhenPrefixIdentifiesNetwork() throws { + let cases: [(LDKNode.Network, String)] = [ + (.bitcoin, " bc1qrefund "), + (.regtest, " bcrt1qrefund "), + ] + + for (offset, testCase) in cases.enumerated() { + let suiteName = "\(#function)-\(offset)" + let defaults = try XCTUnwrap(UserDefaults(suiteName: suiteName)) + defer { defaults.removePersistentDomain(forName: suiteName) } + let legacy = BlocktankRefundAddress(address: testCase.1, index: 7) + try defaults.set(JSONEncoder().encode(legacy), forKey: BlocktankRefundAddressStore.legacyKey) + + let store = BlocktankRefundAddressStore(defaults: defaults, network: testCase.0) + XCTAssertEqual(try store.load(), legacy) + XCTAssertNil(defaults.object(forKey: BlocktankRefundAddressStore.legacyKey)) + let migrated = try XCTUnwrap( + defaults.data(forKey: BlocktankRefundAddressStore.key(for: testCase.0)) + ) + XCTAssertEqual( + try JSONDecoder().decode(BlocktankRefundAddress.self, from: migrated), + legacy + ) + } + } + + func testClearPreservesLegacyCacheFromAnotherNetwork() throws { + let defaults = try XCTUnwrap(UserDefaults(suiteName: #function)) + defer { defaults.removePersistentDomain(forName: #function) } + let legacy = BlocktankRefundAddress(address: "bcrt1qrefund", index: 7) + try defaults.set(JSONEncoder().encode(legacy), forKey: BlocktankRefundAddressStore.legacyKey) + + BlocktankRefundAddressStore(defaults: defaults, network: .bitcoin).clear() + XCTAssertNotNil(defaults.data(forKey: BlocktankRefundAddressStore.legacyKey)) + + BlocktankRefundAddressStore(defaults: defaults, network: .regtest).clear() + XCTAssertNil(defaults.object(forKey: BlocktankRefundAddressStore.legacyKey)) + } + + func testAmbiguousTestnetLegacyCacheIsPreserved() throws { + let defaults = try XCTUnwrap(UserDefaults(suiteName: #function)) + defer { defaults.removePersistentDomain(forName: #function) } + let legacy = BlocktankRefundAddress(address: "tb1qrefund", index: 7) + try defaults.set(JSONEncoder().encode(legacy), forKey: BlocktankRefundAddressStore.legacyKey) + + XCTAssertNil(try BlocktankRefundAddressStore(defaults: defaults, network: .testnet).load()) + XCTAssertNil(try BlocktankRefundAddressStore(defaults: defaults, network: .signet).load()) + BlocktankRefundAddressStore(defaults: defaults, network: .testnet).clear() + BlocktankRefundAddressStore(defaults: defaults, network: .signet).clear() + XCTAssertNotNil(defaults.data(forKey: BlocktankRefundAddressStore.legacyKey)) + } + + func testScopedCacheTakesPrecedenceOverLegacyCache() throws { + let defaults = try XCTUnwrap(UserDefaults(suiteName: #function)) + defer { defaults.removePersistentDomain(forName: #function) } + let store = BlocktankRefundAddressStore(defaults: defaults, network: .regtest) + let scoped = BlocktankRefundAddress(address: "bcrt1qscoped", index: 8) + let legacy = BlocktankRefundAddress(address: "bc1qlegacy", index: 7) + try store.save(scoped) + try defaults.set(JSONEncoder().encode(legacy), forKey: BlocktankRefundAddressStore.legacyKey) + + XCTAssertEqual(try store.load(), scoped) + XCTAssertNotNil(defaults.data(forKey: BlocktankRefundAddressStore.legacyKey)) + } + + func testUnknownLegacyCacheFailsClosedAndIsCleared() throws { + let defaults = try XCTUnwrap(UserDefaults(suiteName: #function)) + defer { defaults.removePersistentDomain(forName: #function) } + let legacy = BlocktankRefundAddress(address: "unknown", index: 7) + try defaults.set(JSONEncoder().encode(legacy), forKey: BlocktankRefundAddressStore.legacyKey) + let store = BlocktankRefundAddressStore(defaults: defaults, network: .regtest) + + XCTAssertThrowsError(try store.load()) { error in + XCTAssertEqual(error as? BlocktankRefundAddressError, .invalidCache) + } + store.clear() + XCTAssertNil(defaults.object(forKey: BlocktankRefundAddressStore.legacyKey)) + } + + func testSettingsCacheRestoreRoundTripAndWipe() throws { + let json = #"{"blocktankRefundAddress":{"address":"bcrt1refund","index":7}}"# + let restored = try JSONDecoder().decode(AppCacheData.self, from: Data(json.utf8)) + + try SettingsViewModel.shared.restoreAppCacheData(restored) + + XCTAssertEqual( + try SettingsViewModel.shared.getAppCacheData().blocktankRefundAddress, + BlocktankRefundAddress(address: "bcrt1refund", index: 7) + ) + XCTAssertTrue(SettingsBackupConfig.appStateKeys.contains(BlocktankRefundAddressStore.key)) + + SettingsViewModel.shared.resetToDefaults() + XCTAssertNil(try BlocktankRefundAddressStore().load()) + } + + func testCorruptLocalCacheIsRejected() throws { + UserDefaults.standard.set(Data("not-json".utf8), forKey: BlocktankRefundAddressStore.key) + + XCTAssertThrowsError(try BlocktankRefundAddressStore().load()) { error in + XCTAssertEqual(error as? BlocktankRefundAddressError, .invalidCache) + } + } +} diff --git a/BitkitTests/TransferServiceActivityTests.swift b/BitkitTests/TransferServiceActivityTests.swift index 245fde35d..7cf7a04e0 100644 --- a/BitkitTests/TransferServiceActivityTests.swift +++ b/BitkitTests/TransferServiceActivityTests.swift @@ -91,8 +91,7 @@ final class TransferServiceActivityTests: XCTestCase { Bitkit.TransferService( storage: Bitkit.TransferStorage(defaults: transferDefaults), lightningService: .shared, - blocktankService: Bitkit.CoreService.shared.blocktank, - isGeoBlocked: { false } + blocktankService: Bitkit.CoreService.shared.blocktank ) } @@ -1473,8 +1472,7 @@ final class TransferServiceActivityTests: XCTestCase { func testConcurrentPaidOrderFollowupCreatesOneOriginalRecord() async throws { let storage = ConcurrentPaidOrderStorage(defaults: transferDefaults) let service = Bitkit.TransferService( - storage: storage, lightningService: .shared, blocktankService: Bitkit.CoreService.shared.blocktank, - isGeoBlocked: { false } + storage: storage, lightningService: .shared, blocktankService: Bitkit.CoreService.shared.blocktank ) let txid = String(repeating: "ab", count: 32) let first = Task.detached { From 6c393b74c615260c821eb043f07b350282e6b867 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Thu, 22 Oct 2026 15:08:20 +0200 Subject: [PATCH 10/12] feat: stop watching pending blocktank orders Order and CJIT reads use the local cache only; the app no longer polls a paid order or opens its channel at start, so Blocktank opens or refunds orders paid before the upgrade. --- Bitkit/AppScene.swift | 3 -- Bitkit/ViewModels/BlocktankViewModel.swift | 42 ------------------- .../ViewModels/ChannelDetailsViewModel.swift | 2 +- Bitkit/ViewModels/TransferViewModel.swift | 12 ------ .../Wallets/Send/SendPendingScreen.swift | 2 +- 5 files changed, 2 insertions(+), 59 deletions(-) diff --git a/Bitkit/AppScene.swift b/Bitkit/AppScene.swift index 5e17f7792..598b7f632 100644 --- a/Bitkit/AppScene.swift +++ b/Bitkit/AppScene.swift @@ -1027,9 +1027,6 @@ struct AppScene: View { await retryPendingPaykitEndpointRemoval() } - // Start watching pending orders after wallet is ready - await blocktank.startWatchingPendingOrders(transferViewModel: transfer) - // Open the swap updates stream so any pending LN -> onchain swaps resume // and auto-claim once their lockup confirms. Retries until the stream starts. wallet.ensureSwapUpdatesRunning() diff --git a/Bitkit/ViewModels/BlocktankViewModel.swift b/Bitkit/ViewModels/BlocktankViewModel.swift index 47769dc30..835acbae4 100644 --- a/Bitkit/ViewModels/BlocktankViewModel.swift +++ b/Bitkit/ViewModels/BlocktankViewModel.swift @@ -94,31 +94,9 @@ class BlocktankViewModel: ObservableObject { isRefreshing = true defer { isRefreshing = false } - Logger.debug("Refreshing orders...") - - // Sync UI instantly from cache orders = try await coreService.blocktank.orders(refresh: false) cJitEntries = try await coreService.blocktank.cjitOrders(refresh: false) - - // The update from server - orders = try await coreService.blocktank.orders(refresh: true) - cJitEntries = try await coreService.blocktank.cjitOrders(refresh: true) - - Logger.debug("Orders refreshed") - coreService.blocktank.notifyStateChanged() - } - - func refreshOrder(id: String) async throws -> IBtOrder? { - let refreshedOrders = try await coreService.blocktank.orders(orderIds: [id], refresh: true) - guard let refreshedOrder = refreshedOrders.first else { return nil } - - // Update the order in the published array if it exists - if let index = orders?.firstIndex(where: { $0.id == id }) { - orders?[index] = refreshedOrder - } - coreService.blocktank.notifyStateChanged() - return refreshedOrder } func createCjit(amountSats: UInt64, description: String) async throws -> IcJitEntry { @@ -420,24 +398,4 @@ class BlocktankViewModel: ObservableObject { throw error } } - - /// Checks for pending orders and notifies TransferViewModel to start watching them - /// This should be called on app startup to resume watching orders after app restart - func startWatchingPendingOrders(transferViewModel: TransferViewModel) async { - guard let orders else { return } - - let pendingOrders = orders.filter { order in - // Watch orders that are created or paid but not yet completed - order.state2 == .created || order.state2 == .paid - } - - if !pendingOrders.isEmpty { - Logger.info("Found \(pendingOrders.count) pending orders to watch: \(pendingOrders.map(\.id))") - - // Notify TransferViewModel to start watching each pending order - for order in pendingOrders { - await transferViewModel.startWatchingOrderFromRestart(order) - } - } - } } diff --git a/Bitkit/ViewModels/ChannelDetailsViewModel.swift b/Bitkit/ViewModels/ChannelDetailsViewModel.swift index 1b4248322..72d6e025f 100644 --- a/Bitkit/ViewModels/ChannelDetailsViewModel.swift +++ b/Bitkit/ViewModels/ChannelDetailsViewModel.swift @@ -199,7 +199,7 @@ class ChannelDetailsViewModel: ObservableObject { } // Create fake channels from pending orders - guard let orders = try? await coreService.blocktank.orders(refresh: true) else { + guard let orders = try? await coreService.blocktank.orders(refresh: false) else { return connections } diff --git a/Bitkit/ViewModels/TransferViewModel.swift b/Bitkit/ViewModels/TransferViewModel.swift index 045feaffd..e2bba1a30 100644 --- a/Bitkit/ViewModels/TransferViewModel.swift +++ b/Bitkit/ViewModels/TransferViewModel.swift @@ -634,18 +634,6 @@ class TransferViewModel: ObservableObject { return trackingSaved } - func startWatchingOrderFromRestart(_ order: IBtOrder) async { - Logger.info("Starting to watch order from restart: \(order.id)") - - uiState.order = order - uiState.clientBalanceSat = order.clientBalanceSat - uiState.lspBalanceSat = order.lspBalanceSat - uiState.feeSat = order.feeSat - uiState.isAdvanced = false - - watchOrder(orderId: order.id) - } - private func watchOrder(orderId: String, frequencyMs: Double = 15000) { stopPolling() diff --git a/Bitkit/Views/Wallets/Send/SendPendingScreen.swift b/Bitkit/Views/Wallets/Send/SendPendingScreen.swift index 5fc848045..507e981f0 100644 --- a/Bitkit/Views/Wallets/Send/SendPendingScreen.swift +++ b/Bitkit/Views/Wallets/Send/SendPendingScreen.swift @@ -356,7 +356,7 @@ struct SendPendingScreen: View { let payer = attempt.recoveryContext?.paymentIdentity else { throw PaykitPaymentRequestError.requestUnavailable } try await proofService.requireRecoveryPayer(payer) } else if let orderId = attempt.orderId { - let orders = try await CoreService.shared.blocktank.orders(orderIds: [orderId], refresh: true) + let orders = try await CoreService.shared.blocktank.orders(orderIds: [orderId], refresh: false) guard let order = orders.first(where: { $0.id == orderId }), order.state2 == .created, order.payment?.onchain?.address == attempt.address, order.clientBalanceSat == attempt.transferContext?.clientBalanceSats, From 8cda3c4a7982086a7876257c8db4b7b7876cbaca Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Sat, 24 Oct 2026 19:23:40 +0200 Subject: [PATCH 11/12] test: get the 10% receive journey's channel without cjit --- .../receive/receive-above-ten-percent-of-channel.xml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/journeys/receive/receive-above-ten-percent-of-channel.xml b/journeys/receive/receive-above-ten-percent-of-channel.xml index a8d5d3ded..ae3ad0011 100644 --- a/journeys/receive/receive-above-ten-percent-of-channel.xml +++ b/journeys/receive/receive-above-ten-percent-of-channel.xml @@ -6,13 +6,14 @@ receiving capacity. From rc.77 a channel opened by an updated app accepts up to its full value. Channels opened before the update keep the 10% limit. - Precondition: an onboarded regtest wallet with no Lightning channel, connected to the LSP - (README.md, Backend preconditions). The LSP node pays the invoices with the lsp helper (borrowed from the sibling Android checkout, see README.md). + Precondition: an onboarded regtest wallet with a new channel from the LSP that has receiving + capacity, opened on LDK Node 0.7.0-rc.77 or later, connected to the LSP (README.md, Backend + preconditions). This build cannot buy a channel from Blocktank (synonymdev/bitkit-ios#1097), so open + it on a build from before that change with an rc.77 node, with Transfer To Spending, and install this + build over it (README.md, Getting a Lightning channel). The LSP node pays the invoices with the lsp + helper (borrowed from the sibling Android checkout, see README.md). - Open Receive, create a Lightning invoice for 50 000 sats (it opens a channel from the LSP on payment) and copy the invoice - Pay it from the LSP node: ../bitkit-android/lsp POST /regtest/lightning/pay '{"invoice":"<bolt11>"}' - Verify the received sheet shows 50 000 sats Open Settings, then the "Advanced" tab, then "Lightning Connections", open the new connection and note its total size and receiving capacity Go back to Receive and create a Lightning invoice for an amount between 15% and 90% of the connection's total size that is also below its receiving capacity (for example 30% of the size) Pay it from the LSP node with the same helper call From 4adaf7f0d3bfd540932ea4a10ef9617766d385d2 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Sat, 24 Oct 2026 19:49:34 +0200 Subject: [PATCH 12/12] fix: point the third blocktank peer at its current address --- Bitkit/Constants/Env.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Bitkit/Constants/Env.swift b/Bitkit/Constants/Env.swift index 871452c10..f9f0b3ef1 100644 --- a/Bitkit/Constants/Env.swift +++ b/Bitkit/Constants/Env.swift @@ -302,7 +302,7 @@ enum Env { return [ .init(nodeId: "039b8b4dd1d88c2c5db374290cda397a8f5d79f312d6ea5d5bfdfc7c6ff363eae3", host: "34.65.111.104", port: 9735), .init(nodeId: "03816141f1dce7782ec32b66a300783b1d436b19777e7c686ed00115bd4b88ff4b", host: "34.65.191.64", port: 9735), - .init(nodeId: "02a371038863605300d0b3fc9de0cf5ccb57728b7f8906535709a831b16e311187", host: "34.65.153.174", port: 9735), + .init(nodeId: "02a371038863605300d0b3fc9de0cf5ccb57728b7f8906535709a831b16e311187", host: "34.65.147.55", port: 9735), ] case .signet: return []