taler-ios

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

GradientBorder.swift (11277B)


      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: "\n")
    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 //                        let screenWidth = UIScreen.screenWidth
    200                         GradientBorder(size: size + 20.0,
    201                                       color: WalletColors().primaryAccent,
    202                                  background: WalletColors().backgroundColor)
    203                         {
    204                             content()
    205                                 .onTapGesture(count: 1) { nfcWriter.write(totpData) }
    206                         }
    207                         hint
    208                     } else {
    209                         NFCbutton(title: String(localized: "Write NFC")) {
    210                             nfcWriter.write(totpData)
    211                         }
    212                         totpButton.buttonStyle(TalerButtonStyle(type: .bordered))
    213                     }
    214                 }
    215             }.listRowSeparator(.hidden)
    216                 .onDisappear() {
    217 
    218                 }.task {
    219                     withAnimation {
    220                         if #available(iOS 17.7, *) {
    221                             showTOTP = showQRauto17
    222                         } else {
    223                             showTOTP = showQRauto16
    224                         }
    225                     }
    226                 }
    227         } else {
    228             if showTOTP {
    229                 content()
    230                 hint
    231             } else {
    232                 totpButton.buttonStyle(TalerButtonStyle(type: .prominent))
    233             }
    234         }
    235     }
    236 }
    237 // MARK: -
    238 // Use radius: 0 for rect instead of rounded rect
    239 struct GradientBorder<Content: View>: View {
    240     let size: CGFloat
    241     let radius: CGFloat
    242     let lineWidth: CGFloat
    243     let color: Color
    244     let background: Color
    245     var content: () -> Content
    246 
    247     @State var rotation: CGFloat = 0
    248 
    249     var body: some View {
    250         HStack {
    251             Spacer()
    252             ZStack {
    253                 RoundedRectangle(cornerRadius: radius, style: .continuous)
    254                     .frame(width: size, height: size).foregroundStyle(background)
    255                     .shadow(color: background.opacity(0.5), radius: 10, x: 0, y: 10)
    256                 let gradient = Gradient(colors: [
    257                     color.opacity(INVISIBLE),
    258                     color,
    259                     color,
    260                     color.opacity(INVISIBLE)]
    261                 )
    262                 let linearGradient = LinearGradient(gradient: gradient, startPoint: .top, endPoint: .bottom)
    263                 let rotatingRect = Rectangle()
    264                     .frame(width: size*2, height: size/2)
    265                     .foregroundStyle(linearGradient)
    266                     .rotationEffect(.degrees(rotation))
    267                 let border = lineWidth - 0.5
    268                 rotatingRect
    269                     .mask {
    270                         RoundedRectangle(cornerRadius: radius - lineWidth/2, style: .continuous)
    271                             .stroke(lineWidth: lineWidth)
    272                             .frame(width: size - border, height: size - border)
    273                     }
    274                 content()
    275             }
    276             .frame(width: size, height: size)
    277             Spacer()
    278         }
    279         .onAppear {
    280             withAnimation(.linear(duration: 8).repeatForever(autoreverses: false)) {
    281                 rotation = 720
    282             }
    283         }
    284     }
    285 }
    286 extension GradientBorder {
    287     init(size: CGFloat, radius: CGFloat = 20.0, lineWidth: CGFloat = 4.0, color: Color, background: Color, content: @escaping () -> Content) {
    288         self.size = size
    289         self.radius = radius
    290         self.lineWidth = lineWidth
    291         self.color = color
    292         self.background = background
    293         self.content = content
    294     }
    295 }
    296 // MARK: -
    297 struct GradientBorder_Previews: PreviewProvider {
    298     static var previews: some View {
    299 
    300         GradientBorder(size: 260,
    301 //                     radius: 1,
    302 //                  lineWidth: 2,
    303                       color: .blue,
    304                  background: .yellow) {
    305             Text(verbatim: "Preview")
    306         }
    307     }
    308 }