taler-ios

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

GradientBorder.swift (10711B)


      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 size: CGFloat
    130     let scanHints: (String, String)?
    131     var content: () -> Content
    132 
    133     @AppStorage("minimalistic") var minimalistic: Bool = false
    134     @AppStorage("showQRauto16") var showQRauto16: Bool = true   // iOS 16 doesn't have NFC emulation
    135     @AppStorage("showQRauto17") var showQRauto17: Bool = false  // but iOS 17 does
    136     @State private var showTOTP = false
    137     @ObservedObject var nfcWriter = NFCWriter()
    138 
    139     var lockImage: Image {             // 􂆉 "lock.badge.clock"
    140         Image(ICONNAME_LOCKCLOCK, SYSTEM_LOCKCLOCK, fallback: FALLBACK_LOCK)
    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 {   // TODO: check for write capabilities
    180             VStack {
    181                 if nfcHint {    // QRCodeDetailView
    182                     if showTOTP {
    183                         if !minimalistic {
    184                             Text("\(nfcLogo) Tap for NFC")
    185                                 .talerFont(.subheadline)
    186                                 .padding(.vertical, -4)
    187                         }
    188 //                        let screenWidth = UIScreen.screenWidth
    189                         GradientBorder(size: size + 20.0,
    190                                       color: WalletColors().primaryAccent,
    191                                  background: WalletColors().backgroundColor)
    192                         {
    193                             content()
    194                                 .onTapGesture(count: 1) { nfcWriter.write(totpData) }
    195                         }
    196                         hint
    197                     } else {
    198                         NFCbutton(title: String(localized: "Write NFC")) {
    199                             nfcWriter.write(totpData)
    200                         }
    201                         totpButton.buttonStyle(TalerButtonStyle(type: .bordered))
    202                     }
    203                 }
    204             }.listRowSeparator(.hidden)
    205                 .onDisappear() {
    206 
    207                 }.task {
    208                     withAnimation {
    209                         if #available(iOS 17.7, *) {
    210                             showTOTP = showQRauto17
    211                         } else {
    212                             showTOTP = showQRauto16
    213                         }
    214                     }
    215                 }
    216         } else {
    217             if showTOTP {
    218                 content()
    219                 hint
    220             } else {
    221                 totpButton.buttonStyle(TalerButtonStyle(type: .prominent))
    222             }
    223         }
    224     }
    225 }
    226 // MARK: -
    227 // Use radius: 0 for rect instead of rounded rect
    228 struct GradientBorder<Content: View>: View {
    229     let size: CGFloat
    230     let radius: CGFloat
    231     let lineWidth: CGFloat
    232     let color: Color
    233     let background: Color
    234     var content: () -> Content
    235 
    236     @State var rotation: CGFloat = 0
    237 
    238     var body: some View {
    239         HStack {
    240             Spacer()
    241             ZStack {
    242                 RoundedRectangle(cornerRadius: radius, style: .continuous)
    243                     .frame(width: size, height: size).foregroundStyle(background)
    244                     .shadow(color: background.opacity(0.5), radius: 10, x: 0, y: 10)
    245                 let gradient = Gradient(colors: [
    246                     color.opacity(INVISIBLE),
    247                     color,
    248                     color,
    249                     color.opacity(INVISIBLE)]
    250                 )
    251                 let linearGradient = LinearGradient(gradient: gradient, startPoint: .top, endPoint: .bottom)
    252                 let rotatingRect = Rectangle()
    253                     .frame(width: size*2, height: size/2)
    254                     .foregroundStyle(linearGradient)
    255                     .rotationEffect(.degrees(rotation))
    256                 let border = lineWidth - 0.5
    257                 rotatingRect
    258                     .mask {
    259                         RoundedRectangle(cornerRadius: radius - lineWidth/2, style: .continuous)
    260                             .stroke(lineWidth: lineWidth)
    261                             .frame(width: size - border, height: size - border)
    262                     }
    263                 content()
    264             }
    265             .frame(width: size, height: size)
    266             Spacer()
    267         }
    268         .onAppear {
    269             withAnimation(.linear(duration: 8).repeatForever(autoreverses: false)) {
    270                 rotation = 720
    271             }
    272         }
    273     }
    274 }
    275 extension GradientBorder {
    276     init(size: CGFloat, radius: CGFloat = 20.0, lineWidth: CGFloat = 4.0, color: Color, background: Color, content: @escaping () -> Content) {
    277         self.size = size
    278         self.radius = radius
    279         self.lineWidth = lineWidth
    280         self.color = color
    281         self.background = background
    282         self.content = content
    283     }
    284 }
    285 // MARK: -
    286 struct GradientBorder_Previews: PreviewProvider {
    287     static var previews: some View {
    288 
    289         GradientBorder(size: 260,
    290 //                     radius: 1,
    291 //                  lineWidth: 2,
    292                       color: .blue,
    293                  background: .yellow) {
    294             Text(verbatim: "Preview")
    295         }
    296     }
    297 }