taler-ios

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

GradientBorder.swift (11070B)


      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 @available(iOS 17.7, *)
     28 struct BorderWithHCE<Content: View>: View {
     29     let talerURI: String
     30     let nfcHint: Bool
     31     let size: CGFloat
     32     let scanHints: (String, String)?
     33     var content: () -> Content
     34 
     35     @AppStorage("minimalistic") var minimalistic: Bool = false
     36     @AppStorage("showQRauto16") var showQRauto16: Bool = true
     37     @AppStorage("showQRauto17") var showQRauto17: Bool = false
     38     @StateObject private var tagEmulation = TagEmulation.shared
     39     @State private var showQRcode = false
     40 
     41     var body: some View {
     42 //        let _ = Self._printChanges()
     43         let qrCode = Image(systemName: QRCODE)                                  // 􀖂 "qrcode"
     44         let qrButton = Button(minimalistic ? "\(qrCode)" : "\(qrCode) Show QR code") {
     45             withAnimation { showQRcode = true }
     46         }
     47 
     48         let hint = Group {
     49             if let scanHints {
     50                 Text(scanHints.0)
     51                     .accessibilityLabel(scanHints.1)
     52                     .multilineTextAlignment(.leading)
     53                     .talerFont(.title3)
     54             }
     55         }
     56 
     57         if tagEmulation.canUseHCE {
     58             let nfcLogo = Image(systemName: NFCLOGO)                            // 􀭹 "wave.3.right.circle"
     59             let nfcButton = Button(minimalistic ? "\(nfcLogo)" : "\(nfcLogo) Start NFC") {
     60                 tagEmulation.emulateTag(talerURI)
     61             }.buttonStyle(TalerButtonStyle(type: .prominent))
     62 
     63             VStack {
     64                 if nfcHint {    // QRCodeDetailView
     65                     if showQRcode {
     66                         if !minimalistic {
     67                             Text("\(nfcLogo) Tap for NFC")
     68                                 .talerFont(.subheadline)
     69                                 .padding(.vertical, -4)
     70                         }
     71                         let screenWidth = UIScreen.screenWidth
     72                         GradientBorder(size: size + 20.0,
     73                                       color: .accentColor,
     74                                  background: WalletColors().backgroundColor)
     75                         {
     76                             content()
     77                                 .onTapGesture(count: 1) { tagEmulation.emulateTag(talerURI) }
     78                         }
     79                         hint
     80                     } else {
     81                         nfcButton
     82                         qrButton.buttonStyle(TalerButtonStyle(type: .bordered))
     83                     }
     84                 } else {        // AboutView
     85                     content()
     86                         .onTapGesture(count: 2) { tagEmulation.emulateTag(talerURI) }
     87                 }
     88             }.listRowSeparator(.hidden)
     89             .onDisappear() {
     90                 tagEmulation.killEmulation()
     91             }.task {
     92                 withAnimation {
     93                     if #available(iOS 17.7, *) {
     94                         showQRcode = showQRauto17
     95                     } else {
     96                         showQRcode = showQRauto16
     97                     }
     98                 }
     99             }
    100         } else {
    101             if showQRcode {
    102                 content()
    103                 hint
    104             } else {
    105                 qrButton.buttonStyle(TalerButtonStyle(type: .prominent))
    106             }
    107         }
    108     }
    109 }
    110 // MARK: -
    111 extension FixedWidthInteger {
    112     var data: Data {
    113         let data = withUnsafeBytes(of: self) { Data($0) }
    114         return data
    115     }
    116 }
    117 
    118 struct BorderWithNFC<Content: View>: View {
    119     let totpString: String                    // might be a TOTP code
    120     let nfcHint: Bool
    121     let size: CGFloat
    122     let scanHints: (String, String)?
    123     var content: () -> Content
    124 
    125     @AppStorage("minimalistic") var minimalistic: Bool = false
    126     @AppStorage("showQRauto16") var showQRauto16: Bool = true
    127     @AppStorage("showQRauto17") var showQRauto17: Bool = false
    128     @State private var showTOTP = false
    129     @ObservedObject var nfcWriter = NFCWriter()
    130 
    131     var lockImage: Image {
    132         let name = ICONNAME_LOCKCLOCK
    133         let sysName = SYSTEM_LOCKCLOCK                                          // 􂆉 "lock.badge.clock"
    134         if UIImage(named: name) != nil {
    135             return Image(name)
    136         } else if UIImage(systemName: sysName) != nil {
    137             return Image(systemName: sysName)
    138         } else {
    139             return Image(systemName: FALLBACK_LOCK)
    140         }
    141     }
    142 
    143     private var MAGIC_HEADER: Data {
    144         Data(fromUInt8Array: [
    145             0x42,   //  totp magic header
    146         ])
    147     } // 42
    148 
    149     var totpData: Data {
    150         var data = MAGIC_HEADER                     // 0x42 = "B"
    151         let totpArray = totpString.components(separatedBy: "\n")
    152         for totpCode in totpArray {
    153             if let totpInt = UInt32(totpCode) {
    154                 let intData = totpInt.data
    155 //                print(data, totpInt, intData)
    156                 data.append(intData)
    157             }
    158         }
    159 //        print(data)
    160         return data
    161     }
    162 
    163     var body: some View {
    164 //        let _ = Self._printChanges()
    165         let totpCode = lockImage
    166         let totpButton = Button(minimalistic ? "\(totpCode)" : "\(totpCode) Show TOTP code") {
    167             withAnimation { showTOTP = true }
    168         }
    169 
    170         let hint = Group {
    171             if let scanHints {
    172                 Text(scanHints.0)
    173                     .accessibilityLabel(scanHints.1)
    174                     .multilineTextAlignment(.leading)
    175                     .talerFont(.title3)
    176             }
    177         }
    178 
    179         if true {   // check for write capabilities
    180             let nfcLogo = Image(systemName: NFCLOGO)                            // 􀭹 "wave.3.right.circle"
    181             let nfcButton = Button(minimalistic ? "\(nfcLogo)"
    182                                                 : "\(nfcLogo) Write NFC") {
    183                     nfcWriter.write(totpData)
    184             }.buttonStyle(TalerButtonStyle(type: .prominent))
    185 
    186             VStack {
    187                 if nfcHint {    // QRCodeDetailView
    188                     if showTOTP {
    189                         if !minimalistic {
    190                             Text("\(nfcLogo) Tap for NFC")
    191                                 .talerFont(.subheadline)
    192                                 .padding(.vertical, -4)
    193                         }
    194                         let screenWidth = UIScreen.screenWidth
    195                         GradientBorder(size: size + 20.0,
    196                                        color: .accentColor,
    197                                        background: WalletColors().backgroundColor)
    198                         {
    199                             content()
    200                                 .onTapGesture(count: 1) { nfcWriter.write(totpData) }
    201                         }
    202                         hint
    203                     } else {
    204                         nfcButton
    205                         totpButton.buttonStyle(TalerButtonStyle(type: .bordered))
    206                     }
    207                 }
    208             }.listRowSeparator(.hidden)
    209                 .onDisappear() {
    210 
    211                 }.task {
    212                     withAnimation {
    213                         if #available(iOS 17.7, *) {
    214                             showTOTP = showQRauto17
    215                         } else {
    216                             showTOTP = showQRauto16
    217                         }
    218                     }
    219                 }
    220         } else {
    221             if showTOTP {
    222                 content()
    223                 hint
    224             } else {
    225                 totpButton.buttonStyle(TalerButtonStyle(type: .prominent))
    226             }
    227         }
    228     }
    229 }
    230 // MARK: -
    231 // Use radius: 0 for rect instead of rounded rect
    232 struct GradientBorder<Content: View>: View {
    233     let size: CGFloat
    234     let radius: CGFloat
    235     let lineWidth: CGFloat
    236     let color: Color
    237     let background: Color
    238     var content: () -> Content
    239 
    240     @State var rotation: CGFloat = 0
    241 
    242     var body: some View {
    243         HStack {
    244             Spacer()
    245             ZStack {
    246                 RoundedRectangle(cornerRadius: radius, style: .continuous)
    247                     .frame(width: size, height: size).foregroundStyle(background)
    248                     .shadow(color: background.opacity(0.5), radius: 10, x: 0, y: 10)
    249                 let gradient = Gradient(colors: [
    250                     color.opacity(INVISIBLE),
    251                     color,
    252                     color,
    253                     color.opacity(INVISIBLE)]
    254                 )
    255                 let linearGradient = LinearGradient(gradient: gradient, startPoint: .top, endPoint: .bottom)
    256                 let rotatingRect = Rectangle()
    257                     .frame(width: size*2, height: size/2)
    258                     .foregroundStyle(linearGradient)
    259                     .rotationEffect(.degrees(rotation))
    260                 let border = lineWidth - 0.5
    261                 rotatingRect
    262                     .mask {
    263                         RoundedRectangle(cornerRadius: radius - lineWidth/2, style: .continuous)
    264                             .stroke(lineWidth: lineWidth)
    265                             .frame(width: size - border, height: size - border)
    266                     }
    267                 content()
    268             }
    269             .frame(width: size, height: size)
    270             Spacer()
    271         }
    272         .onAppear {
    273             withAnimation(.linear(duration: 4).repeatForever(autoreverses: false)) {
    274                 rotation = 360
    275             }
    276         }
    277     }
    278 }
    279 extension GradientBorder {
    280     init(size: CGFloat, radius: CGFloat = 20.0, lineWidth: CGFloat = 4.0, color: Color, background: Color, content: @escaping () -> Content) {
    281         self.size = size
    282         self.radius = radius
    283         self.lineWidth = lineWidth
    284         self.color = color
    285         self.background = background
    286         self.content = content
    287     }
    288 }
    289 // MARK: -
    290 struct GradientBorder_Previews: PreviewProvider {
    291     static var previews: some View {
    292 
    293         GradientBorder(size: 260,
    294 //                     radius: 1,
    295 //                  lineWidth: 2,
    296                       color: .blue,
    297                  background: .yellow) {
    298             Text(verbatim: "Preview")
    299         }
    300     }
    301 }