taler-ios

iOS apps for GNU Taler (wallet)
Log | Files | Refs | README | LICENSE

GradientBorder.swift (11079B)


      1 /* MIT License
      2  * Copyright (c) 2024 Sucodee
      3  *
      4  * Permission is hereby granted, free of charge, to any person obtaining a copy
      5  * of this software and associated documentation files (the "Software"), to deal
      6  * in the Software without restriction, including without limitation the rights
      7  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
      8  * copies of the Software, and to permit persons to whom the Software is
      9  * furnished to do so, subject to the following conditions:
     10  *
     11  * The above copyright notice and this permission notice shall be included in all
     12  * copies or substantial portions of the Software.
     13  *
     14  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
     15  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
     16  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
     17  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
     18  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
     19  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
     20  * SOFTWARE.
     21  */
     22 /**
     23  * @author Marc Stibane
     24  */
     25 import SwiftUI
     26 
     27 fileprivate let nfcLogo = Image(systemName: NFCLOGO)        // 􀭹 "wave.3.right.circle"
     28 
     29 struct NFCbutton: View {
     30     let title: String
     31     let action: () -> Void
     32 
     33     @AppStorage("minimalistic") var minimalistic: Bool = false
     34 
     35     var body: some View {
     36         let logo = nfcLogo
     37         Button(minimalistic ? "\(logo)"
     38                             : "\(logo) \(title)") {
     39             action()
     40         }.buttonStyle(TalerButtonStyle(type: .prominent))
     41     }
     42 }
     43 
     44 @available(iOS 17.7, *)
     45 struct BorderWithHCE<Content: View>: View {
     46     let talerURI: String
     47     let nfcHint: Bool
     48     let size: CGFloat
     49     let scanHints: (String, String)?
     50     var content: () -> Content
     51 
     52     @AppStorage("minimalistic") var minimalistic: Bool = false
     53     @AppStorage("showQRauto16") var showQRauto16: Bool = true   // iOS 16 doesn't have NFC emulation
     54     @AppStorage("showQRauto17") var showQRauto17: Bool = false  // but iOS 17 does
     55     @StateObject private var tagEmulation = TagEmulation.shared
     56     @State private var showQRcode = false
     57 
     58     var body: some View {
     59 //        let _ = Self._printChanges()
     60         let qrCode = Image(systemName: QRCODE)                                  // 􀖂 "qrcode"
     61         let qrButton = Button(minimalistic ? "\(qrCode)"
     62                                            : "\(qrCode) Show QR code") {
     63             withAnimation { showQRcode = true }
     64         }
     65 
     66         let hint = Group {
     67             if let scanHints {
     68                 Text(scanHints.0)
     69                     .accessibilityLabel(scanHints.1)
     70                     .multilineTextAlignment(.leading)
     71                     .talerFont(.title3)
     72             }
     73         }
     74 
     75         if tagEmulation.canUseHCE {
     76             VStack {
     77                 if nfcHint {    // QRCodeDetailView
     78                     if showQRcode {
     79                         if !minimalistic {
     80                             Text("\(nfcLogo) Tap for NFC")
     81                                 .talerFont(.subheadline)
     82                                 .padding(.vertical, -4)
     83                         }
     84 //                        let screenWidth = UIScreen.screenWidth
     85                         content()
     86                             .onTapGesture(count: 1) { tagEmulation.emulateTag(talerURI) }
     87                         hint
     88                     } else {
     89                         NFCbutton(title: String(localized: "Start NFC")) {
     90                             tagEmulation.emulateTag(talerURI)
     91                         }
     92                         qrButton.buttonStyle(TalerButtonStyle(type: .bordered))
     93                     }
     94                 } else {        // AboutView
     95                     content()
     96                         .onTapGesture(count: 2) { tagEmulation.emulateTag(talerURI) }
     97                 }
     98             }.listRowSeparator(.hidden)
     99             .onDisappear() {
    100                 tagEmulation.killEmulation()
    101             }.task {
    102                 withAnimation {
    103                     if #available(iOS 17.7, *) {
    104                         showQRcode = showQRauto17
    105                     } else {
    106                         showQRcode = showQRauto16
    107                     }
    108                 }
    109             }
    110         } else if showQRcode {
    111             content()
    112             hint
    113         } else {
    114             qrButton.buttonStyle(TalerButtonStyle(type: .prominent))
    115         }
    116     }
    117 }
    118 // MARK: -
    119 extension FixedWidthInteger {
    120     var data: Data {
    121         let data = withUnsafeBytes(of: self) { Data($0) }
    122         return data
    123     }
    124 }
    125 
    126 struct BorderWithNFC<Content: View>: View {
    127     let totpString: String                    // might be a TOTP code
    128     let nfcHint: Bool
    129     let automaticNfc: Bool?
    130     let size: CGFloat
    131     let scanHints: (String, String)?
    132     var content: () -> Content
    133 
    134     @AppStorage("minimalistic") var minimalistic: Bool = false
    135     @AppStorage("showQRauto16") var showQRauto16: Bool = true   // iOS 16 doesn't have NFC emulation
    136     @AppStorage("showQRauto17") var showQRauto17: Bool = false  // but iOS 17 does
    137     @State private var showTOTP = false
    138     @State private var didWriteTOTP = false
    139     @ObservedObject var nfcWriter = NFCWriter()
    140 
    141     var lockImage: Image {             // 􂆉 "lock.badge.clock"
    142         Image(ICONNAME_LOCKCLOCK, SYSTEM_LOCKCLOCK, fallback: FALLBACK_LOCK)
    143     }
    144 
    145     private var MAGIC_HEADER: Data {
    146         Data(fromUInt8Array: [
    147             0x42,   //  totp magic header
    148         ])
    149     } // 42
    150 
    151     var totpData: Data {
    152         var data = MAGIC_HEADER                     // 0x42 = "B"
    153         let totpArray = totpString.components(separatedBy: LINEFEED)
    154         for totpCode in totpArray {
    155             if let totpInt = UInt32(totpCode) {
    156                 let intData = totpInt.data
    157 //                print(data, totpInt, intData)
    158                 data.append(intData)
    159             }
    160         }
    161 //        print(data)
    162         return data
    163     }
    164 
    165     var body: some View {
    166 //        let _ = Self._printChanges()
    167         let totpCode = lockImage
    168         let totpButton = Button(minimalistic ? "\(totpCode)" : "\(totpCode) Show TOTP code") {
    169             withAnimation { showTOTP = true }
    170         }
    171 
    172         let hint = Group {
    173             if let scanHints {
    174                 Text(scanHints.0)
    175                     .accessibilityLabel(scanHints.1)
    176                     .multilineTextAlignment(.leading)
    177                     .talerFont(.title3)
    178             }
    179         }
    180 
    181         if true {   // We always have NFC write capabilities
    182             VStack {
    183                 if let automaticNfc, automaticNfc, !didWriteTOTP {
    184                     Text("\(nfcLogo) Point your iPhone to the NFC device")
    185                         .tint(WalletColors().primaryAccent)
    186                         .task {
    187                             DispatchQueue.main.asyncAfter(deadline: .now() + 0.9) {
    188                                 nfcWriter.write(totpData)
    189                                 didWriteTOTP = true
    190                             }
    191                         }
    192                 } else if nfcHint {    // QRCodeDetailView
    193                     if showTOTP {
    194                         if !minimalistic {
    195                             Text("\(nfcLogo) Tap for NFC")
    196                                 .talerFont(.subheadline)
    197                                 .padding(.vertical, -4)
    198                         }
    199                         ZStack {
    200                             GradientBorder(size: size + 20.0,
    201                                           color: WalletColors().primaryAccent,
    202                                      background: WalletColors().backgroundColor)
    203                             content()
    204                                 .onTapGesture(count: 1) { nfcWriter.write(totpData) }
    205                         }
    206                         hint
    207                     } else {
    208                         NFCbutton(title: String(localized: "Write NFC")) {
    209                             nfcWriter.write(totpData)
    210                         }
    211                         totpButton.buttonStyle(TalerButtonStyle(type: .bordered))
    212                     }
    213                 }
    214             }.listRowSeparator(.hidden)
    215                 .onDisappear() {
    216 
    217                 }.task {
    218                     withAnimation {
    219                         if #available(iOS 17.7, *) {
    220                             showTOTP = showQRauto17
    221                         } else {
    222                             showTOTP = showQRauto16
    223                         }
    224                     }
    225                 }
    226         } else {
    227             if showTOTP {
    228                 content()
    229                 hint
    230             } else {
    231                 totpButton.buttonStyle(TalerButtonStyle(type: .prominent))
    232             }
    233         }
    234     }
    235 }
    236 // MARK: -
    237 // Use radius: 0 for rect instead of rounded rect
    238 struct GradientBorder: View {
    239     let size: CGFloat
    240     let radius: CGFloat
    241     let lineWidth: CGFloat
    242     let color: Color
    243     let background: Color
    244 
    245     @State private var rotation: CGFloat = 0
    246 
    247     var body: some View {
    248         HStack {
    249             Spacer()
    250             ZStack {
    251                 RoundedRectangle(cornerRadius: radius, style: .continuous)
    252                     .frame(width: size, height: size).foregroundStyle(background)
    253                     .shadow(color: background.opacity(0.5), radius: 10, x: 0, y: 10)
    254                 let gradient = Gradient(colors: [
    255                     color.opacity(INVISIBLE),
    256                     color,
    257                     color,
    258                     color.opacity(INVISIBLE)]
    259                 )
    260                 let linearGradient = LinearGradient(gradient: gradient, startPoint: .top, endPoint: .bottom)
    261                 let rotatingRect = Rectangle()
    262                     .frame(width: size*2, height: size/2)
    263                     .foregroundStyle(linearGradient)
    264                     .rotationEffect(.degrees(rotation))
    265                 let border = lineWidth - 0.5
    266                 rotatingRect
    267                     .mask {
    268                         RoundedRectangle(cornerRadius: radius - lineWidth/2, style: .continuous)
    269                             .stroke(lineWidth: lineWidth)
    270                             .frame(width: size - border, height: size - border)
    271                     }
    272             }
    273             .frame(width: size, height: size)
    274             Spacer()
    275         }
    276         .onAppear {
    277             withAnimation(.linear(duration: 4).repeatForever(autoreverses: false)) {
    278                 rotation = 360
    279             }
    280         }
    281     }
    282 }
    283 extension GradientBorder {
    284     init(size: CGFloat, color: Color, background: Color) {
    285         self.size = size
    286         self.radius = 20.0
    287         self.lineWidth = 4.0
    288         self.color = color
    289         self.background = background
    290     }
    291 }
    292 // MARK: -
    293 struct GradientBorder_Previews: PreviewProvider {
    294     static var previews: some View {
    295 
    296         ZStack {
    297             GradientBorder(size: 260,
    298 //                       radius: 1,
    299 //                    lineWidth: 2,
    300                           color: .blue,
    301                      background: .yellow)
    302             Text(verbatim: "Preview")
    303         }
    304     }
    305 }