taler-ios

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

GradientBorder.swift (11050B)


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