diff --git a/app/App/Assets.xcassets/Colors/AccentColor2.colorset/Contents.json b/app/App/Assets.xcassets/Colors/AccentColor2.colorset/Contents.json new file mode 100644 index 00000000..76f982ce --- /dev/null +++ b/app/App/Assets.xcassets/Colors/AccentColor2.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xD5", + "green" : "0x26", + "red" : "0x53" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/app/App/Assets.xcassets/Colors/Background2.colorset/Contents.json b/app/App/Assets.xcassets/Colors/Background2.colorset/Contents.json index 54dd1a1a..7a2b0a6f 100644 --- a/app/App/Assets.xcassets/Colors/Background2.colorset/Contents.json +++ b/app/App/Assets.xcassets/Colors/Background2.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0x1D", - "green" : "0x18", - "red" : "0x17" + "blue" : "0x22", + "green" : "0x1D", + "red" : "0x1C" } }, "idiom" : "universal" diff --git a/app/App/Assets.xcassets/Colors/Rainbow/Blue/Contents.json b/app/App/Assets.xcassets/Colors/Rainbow/Blue/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/app/App/Assets.xcassets/Colors/Rainbow/Blue/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/app/App/Assets.xcassets/Colors/Rainbow/Blue/VBlue.colorset/Contents.json b/app/App/Assets.xcassets/Colors/Rainbow/Blue/VBlue.colorset/Contents.json new file mode 100644 index 00000000..f152fc41 --- /dev/null +++ b/app/App/Assets.xcassets/Colors/Rainbow/Blue/VBlue.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xF9", + "green" : "0xBE", + "red" : "0x35" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/app/App/Assets.xcassets/Colors/Rainbow/Blue/VBlueDarker.colorset/Contents.json b/app/App/Assets.xcassets/Colors/Rainbow/Blue/VBlueDarker.colorset/Contents.json new file mode 100644 index 00000000..cfcddbdd --- /dev/null +++ b/app/App/Assets.xcassets/Colors/Rainbow/Blue/VBlueDarker.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xD5", + "green" : "0xA0", + "red" : "0x26" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/app/App/Assets.xcassets/Colors/Rainbow/Contents.json b/app/App/Assets.xcassets/Colors/Rainbow/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/app/App/Assets.xcassets/Colors/Rainbow/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/app/App/Assets.xcassets/Colors/Rainbow/Green/Contents.json b/app/App/Assets.xcassets/Colors/Rainbow/Green/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/app/App/Assets.xcassets/Colors/Rainbow/Green/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/app/App/Assets.xcassets/Colors/Rainbow/Green/VGreen.colorset/Contents.json b/app/App/Assets.xcassets/Colors/Rainbow/Green/VGreen.colorset/Contents.json new file mode 100644 index 00000000..ecc3547b --- /dev/null +++ b/app/App/Assets.xcassets/Colors/Rainbow/Green/VGreen.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x58", + "green" : "0xF5", + "red" : "0xA6" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/app/App/Assets.xcassets/Colors/Rainbow/Green/VGreenDarker.colorset/Contents.json b/app/App/Assets.xcassets/Colors/Rainbow/Green/VGreenDarker.colorset/Contents.json new file mode 100644 index 00000000..46664cc4 --- /dev/null +++ b/app/App/Assets.xcassets/Colors/Rainbow/Green/VGreenDarker.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x26", + "green" : "0xD5", + "red" : "0x7E" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/app/App/Assets.xcassets/Colors/Rainbow/Lime/Contents.json b/app/App/Assets.xcassets/Colors/Rainbow/Lime/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/app/App/Assets.xcassets/Colors/Rainbow/Lime/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/app/App/Assets.xcassets/Colors/Rainbow/Lime/VLime.colorset/Contents.json b/app/App/Assets.xcassets/Colors/Rainbow/Lime/VLime.colorset/Contents.json new file mode 100644 index 00000000..72fc6e51 --- /dev/null +++ b/app/App/Assets.xcassets/Colors/Rainbow/Lime/VLime.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xAA", + "green" : "0xF5", + "red" : "0x58" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/app/App/Assets.xcassets/Colors/Rainbow/Lime/VLimeDarker.colorset/Contents.json b/app/App/Assets.xcassets/Colors/Rainbow/Lime/VLimeDarker.colorset/Contents.json new file mode 100644 index 00000000..082d4cf6 --- /dev/null +++ b/app/App/Assets.xcassets/Colors/Rainbow/Lime/VLimeDarker.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x80", + "green" : "0xD5", + "red" : "0x26" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/app/App/Assets.xcassets/Colors/Rainbow/Orange/Contents.json b/app/App/Assets.xcassets/Colors/Rainbow/Orange/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/app/App/Assets.xcassets/Colors/Rainbow/Orange/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/app/App/Assets.xcassets/Colors/Rainbow/Orange/VOrange.colorset/Contents.json b/app/App/Assets.xcassets/Colors/Rainbow/Orange/VOrange.colorset/Contents.json new file mode 100644 index 00000000..8609bc9c --- /dev/null +++ b/app/App/Assets.xcassets/Colors/Rainbow/Orange/VOrange.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x58", + "green" : "0xC9", + "red" : "0xF5" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/app/App/Assets.xcassets/Colors/Rainbow/Orange/VOrangeDarker.colorset/Contents.json b/app/App/Assets.xcassets/Colors/Rainbow/Orange/VOrangeDarker.colorset/Contents.json new file mode 100644 index 00000000..8c3bd2b2 --- /dev/null +++ b/app/App/Assets.xcassets/Colors/Rainbow/Orange/VOrangeDarker.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x26", + "green" : "0xA3", + "red" : "0xD5" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/app/App/Assets.xcassets/Colors/Rainbow/Pink/Contents.json b/app/App/Assets.xcassets/Colors/Rainbow/Pink/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/app/App/Assets.xcassets/Colors/Rainbow/Pink/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/app/App/Assets.xcassets/Colors/Rainbow/Pink/VPink.colorset/Contents.json b/app/App/Assets.xcassets/Colors/Rainbow/Pink/VPink.colorset/Contents.json new file mode 100644 index 00000000..17f83e9e --- /dev/null +++ b/app/App/Assets.xcassets/Colors/Rainbow/Pink/VPink.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xDC", + "green" : "0x58", + "red" : "0xF5" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/app/App/Assets.xcassets/Colors/Rainbow/Pink/VPinkDarker.colorset/Contents.json b/app/App/Assets.xcassets/Colors/Rainbow/Pink/VPinkDarker.colorset/Contents.json new file mode 100644 index 00000000..6536f7e3 --- /dev/null +++ b/app/App/Assets.xcassets/Colors/Rainbow/Pink/VPinkDarker.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xB8", + "green" : "0x26", + "red" : "0xD5" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/app/App/Assets.xcassets/Colors/Rainbow/Purple/Contents.json b/app/App/Assets.xcassets/Colors/Rainbow/Purple/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/app/App/Assets.xcassets/Colors/Rainbow/Purple/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/app/App/Assets.xcassets/Colors/Rainbow/Purple/VPurple.colorset/Contents.json b/app/App/Assets.xcassets/Colors/Rainbow/Purple/VPurple.colorset/Contents.json new file mode 100644 index 00000000..25341584 --- /dev/null +++ b/app/App/Assets.xcassets/Colors/Rainbow/Purple/VPurple.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xF5", + "green" : "0x58", + "red" : "0x74" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/app/App/Assets.xcassets/Colors/Rainbow/Purple/VPurpleDarker.colorset/Contents.json b/app/App/Assets.xcassets/Colors/Rainbow/Purple/VPurpleDarker.colorset/Contents.json new file mode 100644 index 00000000..76f982ce --- /dev/null +++ b/app/App/Assets.xcassets/Colors/Rainbow/Purple/VPurpleDarker.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xD5", + "green" : "0x26", + "red" : "0x53" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/app/App/Assets.xcassets/SVG/ArrowDown.imageset/Contents.json b/app/App/Assets.xcassets/SVG/ArrowDown.imageset/Contents.json deleted file mode 100644 index 72fdab2e..00000000 --- a/app/App/Assets.xcassets/SVG/ArrowDown.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "filename" : "arrow-down.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "template-rendering-intent" : "template" - } -} diff --git a/app/App/Assets.xcassets/SVG/ArrowDown.imageset/arrow-down.svg b/app/App/Assets.xcassets/SVG/ArrowDown.imageset/arrow-down.svg deleted file mode 100644 index a72f548b..00000000 --- a/app/App/Assets.xcassets/SVG/ArrowDown.imageset/arrow-down.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/App/Assets.xcassets/SVG/ArrowUp.imageset/arrow-up.svg b/app/App/Assets.xcassets/SVG/ArrowUp.imageset/arrow-up.svg deleted file mode 100644 index a3ad395d..00000000 --- a/app/App/Assets.xcassets/SVG/ArrowUp.imageset/arrow-up.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/App/Assets.xcassets/SVG/plus.imageset/Contents.json b/app/App/Assets.xcassets/SVG/Fun.imageset/Contents.json similarity index 86% rename from app/App/Assets.xcassets/SVG/plus.imageset/Contents.json rename to app/App/Assets.xcassets/SVG/Fun.imageset/Contents.json index a150fecd..453696b4 100644 --- a/app/App/Assets.xcassets/SVG/plus.imageset/Contents.json +++ b/app/App/Assets.xcassets/SVG/Fun.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "plus.svg", + "filename" : "Fun.svg", "idiom" : "universal" } ], diff --git a/app/App/Assets.xcassets/SVG/Fun.imageset/Fun.svg b/app/App/Assets.xcassets/SVG/Fun.imageset/Fun.svg new file mode 100644 index 00000000..a7718671 --- /dev/null +++ b/app/App/Assets.xcassets/SVG/Fun.imageset/Fun.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/App/Assets.xcassets/SVG/ArrowUp.imageset/Contents.json b/app/App/Assets.xcassets/SVG/Gear.imageset/Contents.json similarity index 84% rename from app/App/Assets.xcassets/SVG/ArrowUp.imageset/Contents.json rename to app/App/Assets.xcassets/SVG/Gear.imageset/Contents.json index 2847f54d..8353f045 100644 --- a/app/App/Assets.xcassets/SVG/ArrowUp.imageset/Contents.json +++ b/app/App/Assets.xcassets/SVG/Gear.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "arrow-up.svg", + "filename" : "gear.svg", "idiom" : "universal" } ], diff --git a/app/App/Assets.xcassets/SVG/Gear.imageset/gear.svg b/app/App/Assets.xcassets/SVG/Gear.imageset/gear.svg new file mode 100644 index 00000000..b0c5b302 --- /dev/null +++ b/app/App/Assets.xcassets/SVG/Gear.imageset/gear.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/App/Assets.xcassets/SVG/wallet.imageset/Contents.json b/app/App/Assets.xcassets/SVG/Wallet.imageset/Contents.json similarity index 100% rename from app/App/Assets.xcassets/SVG/wallet.imageset/Contents.json rename to app/App/Assets.xcassets/SVG/Wallet.imageset/Contents.json diff --git a/app/App/Assets.xcassets/SVG/wallet.imageset/wallet.svg b/app/App/Assets.xcassets/SVG/Wallet.imageset/wallet.svg similarity index 100% rename from app/App/Assets.xcassets/SVG/wallet.imageset/wallet.svg rename to app/App/Assets.xcassets/SVG/Wallet.imageset/wallet.svg diff --git a/app/App/Assets.xcassets/SVG/plus.imageset/plus.svg b/app/App/Assets.xcassets/SVG/plus.imageset/plus.svg deleted file mode 100644 index f3a79a1b..00000000 --- a/app/App/Assets.xcassets/SVG/plus.imageset/plus.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/App/Assets.xcassets/SVG/transfer.imageset/Contents.json b/app/App/Assets.xcassets/SVG/transfer.imageset/Contents.json deleted file mode 100644 index 2a2d9f28..00000000 --- a/app/App/Assets.xcassets/SVG/transfer.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "filename" : "transfer.svg", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "template-rendering-intent" : "template" - } -} diff --git a/app/App/Assets.xcassets/SVG/transfer.imageset/transfer.svg b/app/App/Assets.xcassets/SVG/transfer.imageset/transfer.svg deleted file mode 100644 index 0cdedf8f..00000000 --- a/app/App/Assets.xcassets/SVG/transfer.imageset/transfer.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/App/Components/ActivityView.swift b/app/App/Components/ActivityView.swift new file mode 100644 index 00000000..b8de153b --- /dev/null +++ b/app/App/Components/ActivityView.swift @@ -0,0 +1,46 @@ +// +// ActivityView.swift +// Vault +// +// Created by Charles Lanier on 25/06/2024. +// + +import SwiftUI + +struct ActivityView: UIViewControllerRepresentable { + + var activityItems: [Any] + var applicationActivities: [UIActivity]? = nil + + @Binding var isPresented: Bool + + func makeUIViewController(context: Context) -> UIActivityViewController { + let controller = UIActivityViewController( + activityItems: self.activityItems, + applicationActivities: self.applicationActivities + ) + + // close view on completion + controller.completionWithItemsHandler = { _, _, _, _ in + self.isPresented = false + } + + return controller + } + + func updateUIViewController(_ uiViewController: UIActivityViewController, context: Context) { + // No update action needed + } + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + class Coordinator: NSObject { + var parent: ActivityView + + init(_ parent: ActivityView) { + self.parent = parent + } + } +} diff --git a/app/App/Components/AmountInput.swift b/app/App/Components/AmountInput.swift deleted file mode 100644 index eea1f73e..00000000 --- a/app/App/Components/AmountInput.swift +++ /dev/null @@ -1,79 +0,0 @@ -// -// AmountInput.swift -// Vault -// -// Created by Charles Lanier on 03/05/2024. -// - -import SwiftUI - -struct AmountInput: View { - - @Binding private var amount: String - @FocusState private var focused: Bool - - private let regex = try! NSRegularExpression(pattern: "^\\d*,?\\d{0,2}$", options: []) - - init(amount: Binding) { - self._amount = amount - } - - var body: some View { - TextField("", text: $amount) - .focused($focused) - .onChange(of: self.amount, initial: false) { (oldValue, newValue) in - - // format input - if let amount = self.formattedAmount(newValue) { - self.amount = amount - } else { - self.amount = oldValue - } - } - .keyboardType(.decimalPad) - .frame(width: 0, height: 0) - .onAppear { - // Automatically focus the TextField when the view appears - DispatchQueue.main.async { - self.focused = true - } - } - - Text("$\(self.amount.isEmpty ? "0" : self.amount)").textTheme(.hero) - } - - private func formattedAmount(_ amount: String) -> String? { - if amount == "," { - return "0," - } else if regex.firstMatch( - in: amount, - options: [], - range: NSRange(location: 0, length: amount.utf8.count) - ) != nil { - // remove useless 0 if needed - return amount.first == "0" && amount.count > 1 && amount[amount.index(amount.startIndex, offsetBy: 1)].isNumber - ? String(amount.dropFirst()) - : amount - } - - return nil - } -} - -#if DEBUG -struct AmountInputPreviews : PreviewProvider { - - @State static var amount: String = "100" - - static var previews: some View { - NavigationStack { - ZStack { - Color.background1.edgesIgnoringSafeArea(.all) - VStack(alignment: .leading, spacing: 16) { - AmountInput(amount: $amount) - } - } - }.preferredColorScheme(.dark) - } -} -#endif diff --git a/app/App/Components/Avatar/Avatar.swift b/app/App/Components/Avatar/Avatar.swift new file mode 100644 index 00000000..b5a96797 --- /dev/null +++ b/app/App/Components/Avatar/Avatar.swift @@ -0,0 +1,82 @@ +// +// Avatar.swift +// Vault +// +// Created by Charles Lanier on 04/06/2024. +// + +import SwiftUI + +struct Avatar: View { + + private var imageData: Data? = nil + private var imageURL: URL? = nil + + private let name: String + private let size: CGFloat + private let salt: String? + + static let defaultName = "?" + static let defaultSize: CGFloat = 42 + + init( + salt: String? = nil, + name: String? = nil, + size: CGFloat = Self.defaultSize + ) { + self.name = name ?? Self.defaultName + self.size = size + self.salt = salt + } + + init( + salt: String? = nil, + name: String? = nil, + size: CGFloat = Self.defaultSize, + url: String? = nil + ) { + self.init(salt: salt, name: name, size: size) + self.imageURL = if let url = url { URL(string: url) } else { nil } + } + + init( + salt: String? = nil, + name: String? = nil, + size: CGFloat = Self.defaultSize, + data: Data? = nil + ) { + self.init(salt: salt, name: name, size: size) + self.imageData = data + } + + var body: some View { + if let imageURL = self.imageURL { + AsyncImage( + url: imageURL, + content: { image in + image + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: self.size, height: self.size) + .scaledToFit() + }, + placeholder: { + ProgressView() + } + ) + .clipShape(Circle()) + } else if + let imageData = self.imageData, + let uiImage = UIImage(data: imageData) + { + Image(uiImage: uiImage) + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: self.size, height: self.size) + .scaledToFit() + .clipShape(Circle()) + } else { + NoAvatar(salt: self.salt, name: self.name, size: self.size) + } + } +} diff --git a/app/App/Components/Avatar/NoAvatar.swift b/app/App/Components/Avatar/NoAvatar.swift new file mode 100644 index 00000000..1674e560 --- /dev/null +++ b/app/App/Components/Avatar/NoAvatar.swift @@ -0,0 +1,86 @@ +// +// NoAvatar.swift +// Vault +// +// Created by Charles Lanier on 22/05/2024. +// + +import SwiftUI +import Starknet + +enum NoAvatarSalt: Int, CaseIterable { + case s1 = 0 + case s2 = 1 + case s3 = 2 + case s4 = 3 + case s5 = 4 + case s6 = 5 + + var gradient: Gradient { + switch self { + case .s1: + return Gradient(colors: [.vPurple, .vPurpleDarker]) + + case .s2: + return Gradient(colors: [.vGreen, .vGreenDarker]) + + case .s3: + return Gradient(colors: [.vLime, .vLimeDarker]) + + case .s4: + return Gradient(colors: [.vPink, .vPinkDarker]) + + case .s5: + return Gradient(colors: [.vOrange, .vOrangeDarker]) + + case .s6: + return Gradient(colors: [.vBlue, .vBlueDarker]) + } + } + + var fillColor: Color { .accentColor2 } +} + +struct NoAvatar: View { + let salt: NoAvatarSalt + let name: String + let size: CGFloat + + init(salt: String?, name: String, size: CGFloat = Avatar.defaultSize) { + let saltInt = Int(salt?.bytes.last ?? 0) % NoAvatarSalt.allCases.count + self.salt = NoAvatarSalt(rawValue: saltInt) ?? .s1 + self.name = name + self.size = size + } + + var body: some View { + let linearGradient = LinearGradient( + gradient: self.salt.gradient, + startPoint: .top, + endPoint: .bottom + ) + + Capsule() + .fill(linearGradient) + .frame(width: self.size, height: self.size) + .overlay() { + Text(name.initials.uppercased()) + .font(.system(size: 18)) + .fontWeight(.medium) + .foregroundStyle(.neutral1) + } + } +} + +#Preview { + VStack { + NoAvatar(salt: "1", name: "Kenny McCormick") + NoAvatar(salt: "2", name: "Kenny McCormick") + NoAvatar(salt: "3", name: "Kenny McCormick") + NoAvatar(salt: "4", name: "Kenny McCormick") + NoAvatar(salt: "5", name: "Kenny McCormick") + NoAvatar(salt: "6", name: "Kenny McCormick") + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .defaultBackground() +} diff --git a/app/App/Components/Buttons.swift b/app/App/Components/Buttons.swift index bd7cf033..9e3f239e 100644 --- a/app/App/Components/Buttons.swift +++ b/app/App/Components/Buttons.swift @@ -14,6 +14,7 @@ struct PrimaryButtonStyle: ButtonStyle { configuration.label .background(.accent) .clipShape(RoundedRectangle(cornerRadius: 10)) + .shadow(color: .accent.opacity(0.2), radius: 15, y: 5) .opacity(configuration.isPressed ? 0.7 : 1) .animation(.easeOut(duration: 0.1), value: configuration.isPressed) } @@ -24,7 +25,7 @@ struct PrimaryButton: View { let text: String let disabled: Bool - let action: (() -> Void) /// use closure for callback + let action: () -> Void init(_ text: String, disabled: Bool = false, action: @escaping () -> Void) { self.text = text @@ -33,9 +34,10 @@ struct PrimaryButton: View { } var body: some View { - Button(action: action) { /// call the closure here + Button(action: action) { Text(text) .textTheme(.button) + .padding(.top, 2) .frame(maxWidth: .infinity, minHeight: height) } .buttonStyle(PrimaryButtonStyle()) @@ -60,7 +62,7 @@ struct SecondaryButton: View { let text: String let disabled: Bool - let action: (() -> Void) /// use closure for callback + let action: () -> Void init(_ text: String, disabled: Bool = false, action: @escaping () -> Void) { self.text = text @@ -69,7 +71,7 @@ struct SecondaryButton: View { } var body: some View { - Button(action: action) { /// call the closure here + Button(action: action) { Text(text) .foregroundStyle(.accent) .textTheme(.button) @@ -82,46 +84,112 @@ struct SecondaryButton: View { } } -// MARK: Capsule button +// MARK: Icon button + +enum IconButtonSize { + case medium + case large + case custom(CGFloat, CGFloat) + + var buttonSize: CGFloat { + switch self { + case .medium: + return 36 + + case .large: + return 54 + + case .custom(let buttonSize, _): + return buttonSize + } + } + + var iconSize: CGFloat { + switch self { + case .medium: + return 12 + + case .large: + return 16 + + case .custom(_, let iconSize): + return iconSize + } + } +} + +enum IconButtonPriority { + case primary + case secondary + + var background: Color { + switch self { + case .primary: + return .accent + + case .secondary: + return .background2 + } + } +} struct IconButtonStyle: ButtonStyle { + let priority: IconButtonPriority + func makeBody(configuration: Configuration) -> some View { configuration.label - .background(.background2) + .background(self.priority.background) .clipShape(Capsule()) + .shadow(color: self.priority.background.opacity(0.2), radius: 8, y: 4) .opacity(configuration.isPressed ? 0.7 : 1) .animation(.easeOut(duration: 0.1), value: configuration.isPressed) } } -struct IconButton: View { - let size: CGFloat = 52 - let iconSize: CGFloat = 16 - - let text: String - let icon: ImageResource - let action: (() -> Void) /// use closure for callback - - init(_ text: String, iconName: String, action: @escaping () -> Void) { - self.text = text - self.icon = ImageResource(name: iconName, bundle: Bundle.main) +struct IconButton: View where Icon : View { + let size: IconButtonSize + let priority: IconButtonPriority + let icon: Icon + let action: () -> Void + + init( + size: IconButtonSize = .medium, + priority: IconButtonPriority = .secondary, + action: @escaping () -> Void, + @ViewBuilder icon: () -> Icon + ) { + self.icon = icon() + self.size = size self.action = action + self.priority = priority } var body: some View { VStack(spacing: 10) { - Button(action: action) { /// call the closure here + Button(action: action) { HStack { - Image(self.icon) - .renderingMode(.template) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: self.iconSize, height: self.iconSize) + self.icon + .frame(width: self.size.iconSize, height: self.size.iconSize) .foregroundStyle(.neutral1) } - .frame(width: self.size, height: self.size) + .frame(width: self.size.buttonSize, height: self.size.buttonSize) } - .buttonStyle(IconButtonStyle()) + .buttonStyle(IconButtonStyle(priority: self.priority)) + } + } + + @ViewBuilder func withText(_ text: String) -> some View { + self.modifier(IconButtonWithTextModifier(text: text)) + } +} + +struct IconButtonWithTextModifier: ViewModifier { + var text: String + + func body(content: Content) -> some View { + VStack(spacing: 10) { + content + .preferredColorScheme(.dark) Text(self.text).textTheme(.buttonIcon) } @@ -140,26 +208,6 @@ struct TabItemButtonStyle: ButtonStyle { } } -// MARK: Gradient - -struct GradientButtonStyle: ButtonStyle { - func makeBody(configuration: Configuration) -> some View { - configuration.label - .foregroundStyle(.neutral1) - .background( - LinearGradient( - gradient: configuration.isPressed - ? Gradient(colors: [.gradient1B]) - : Constants.gradient1, - startPoint: .topLeading, - endPoint: .bottomTrailing - ) - .animation(.easeInOut(duration: 0.2), value: configuration.isPressed) - ) - .clipShape(RoundedRectangle(cornerRadius: 16)) - } -} - // MARK: Noop struct NoopButtonStyle: ButtonStyle { @@ -185,18 +233,32 @@ struct NoopButtonStyle: ButtonStyle { Spacer() - Button() {} label: { - Text("Enabled") - .textTheme(.button) - .padding(16) + HStack { + IconButton(size: .large) {} icon: { + Image("Gear").iconify() + } + .withText("Settings") + + IconButton(size: .large, priority: .primary) {} icon: { + Image("FaceID").iconify() + } + .withText("Face ID") } - .buttonStyle(GradientButtonStyle()) Spacer() HStack { - IconButton("Send", iconName: "ArrowUp") {} - IconButton("Add", iconName: "Plus") {} + IconButton {} icon: { + Image(systemName: "xmark") + .iconify() + .fontWeight(.bold) + } + IconButton {} icon: { + Image(systemName: "chevron.down") + .iconify() + .fontWeight(.bold) + .padding(.top, 4) + } } }.padding(16) } diff --git a/app/App/Components/CountryPickerView.swift b/app/App/Components/CountryPickerView.swift index 3c3840a4..e3039d30 100644 --- a/app/App/Components/CountryPickerView.swift +++ b/app/App/Components/CountryPickerView.swift @@ -10,33 +10,33 @@ import PhoneNumberKit struct CountryPickerView: View { - @Environment(\.presentationMode) var presentationMode - - @EnvironmentObject private var phoneNumberModel: PhoneNumberModel + @Environment(\.dismiss) var dismiss + + @EnvironmentObject private var model: Model var body: some View { NavigationView { ZStack { VStack { HStack(spacing: 8) { - SearchBar(search: $phoneNumberModel.searchedCountry) + SearchBar(search: $model.searchedCountry) Button { - self.presentationMode.wrappedValue.dismiss() + self.dismiss() } label: { Text("Cancel").textTheme(.buttonSmall) } } .padding(16) - List(self.phoneNumberModel.filteredCountries.indexed(), id: \.element) { index, countryData in + List(self.model.filteredCountries.indexed(), id: \.element) { index, countryData in let flagRessource = ImageResource(name: countryData.regionCode.lowercased(), bundle: Bundle.main) let isFirst = index == 0; - let isLast = index == self.phoneNumberModel.filteredCountries.count - 1 - let isSelected = self.phoneNumberModel.isSelected(countryData.regionCode) + let isLast = index == self.model.filteredCountries.count - 1 + let isSelected = self.model.isSelected(countryData.regionCode) Button { - self.phoneNumberModel.selectedRegionCode = countryData.regionCode - self.presentationMode.wrappedValue.dismiss() + self.model.selectedRegionCode = countryData.regionCode + self.dismiss() } label: { HStack(spacing: 16) { Image(flagRessource) @@ -88,7 +88,7 @@ struct CountryPickerView: View { #if DEBUG struct CountryPickerViewPreviews : PreviewProvider { - @StateObject static var phoneNumberModel = PhoneNumberModel() + @StateObject static var model = Model() @State static var isPresented = true @State static var selectedRegionCode = Locale.current.regionOrFrance.identifier @@ -101,12 +101,10 @@ struct CountryPickerViewPreviews : PreviewProvider { } .sheet(isPresented: $isPresented) { CountryPickerView() - .preferredColorScheme(.dark) - .environmentObject(self.phoneNumberModel) + .environmentObject(self.model) } } - }.preferredColorScheme(.dark) + } } - } #endif diff --git a/app/App/Components/FancyAmount.swift b/app/App/Components/FancyAmount.swift new file mode 100644 index 00000000..06d1d425 --- /dev/null +++ b/app/App/Components/FancyAmount.swift @@ -0,0 +1,42 @@ +// +// FancyAmount.swift +// Vault +// +// Created by Charles Lanier on 17/05/2024. +// + +import SwiftUI + +struct FancyAmount: View { + @Binding var amount: String + + var body: some View { + let splittedAmount = amount.components(separatedBy: ",") + let shouldDisplayComma = splittedAmount.count > 1 + + HStack(spacing: 4) { + Text("$") + .font(.custom("Sofia Pro", size: 32)) + .textTheme(.hero) + + HStack(alignment: .bottom, spacing: 0) { + Text("\(splittedAmount.first!)\(shouldDisplayComma ? "," : "")") + .font(.custom("Sofia Pro", size: 64)) + .textTheme(.hero) + + Text("\(shouldDisplayComma ? splittedAmount.last! : "")") + .foregroundStyle(.neutral2) + .font(.custom("Sofia Pro", size: 64)) + .textTheme(.hero) + } + } + } +} + +#Preview { + VStack { + FancyAmount(amount: .constant("123,45")) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .defaultBackground() +} diff --git a/app/App/Components/Input.swift b/app/App/Components/Input/Input.swift similarity index 100% rename from app/App/Components/Input.swift rename to app/App/Components/Input/Input.swift diff --git a/app/App/Components/OTPInput.swift b/app/App/Components/Input/OTPInput.swift similarity index 93% rename from app/App/Components/OTPInput.swift rename to app/App/Components/Input/OTPInput.swift index 5b45109d..2e414145 100644 --- a/app/App/Components/OTPInput.swift +++ b/app/App/Components/Input/OTPInput.swift @@ -27,10 +27,10 @@ struct OTPInput: View { ZStack { TextField("", text: $otp) .focused($focused) - .onChange(of: self.otp, initial: false) { (_, newValue) in + .onChange(of: self.otp) { newValue in // remove non digit chars self.otp = String( - self.otp.filter { $0.isWholeNumber }.prefix(self.numberOfFields) + newValue.filter { $0.isWholeNumber }.prefix(self.numberOfFields) ) } .keyboardType(.numberPad) @@ -88,7 +88,7 @@ struct OTPInputPreviews : PreviewProvider { OTPInput(otp: $otp, numberOfFields: 6) } } - }.preferredColorScheme(.dark) + } } } #endif diff --git a/app/App/Components/PhoneInput.swift b/app/App/Components/Input/PhoneInput.swift similarity index 78% rename from app/App/Components/PhoneInput.swift rename to app/App/Components/Input/PhoneInput.swift index 94aae961..ff16e6c5 100644 --- a/app/App/Components/PhoneInput.swift +++ b/app/App/Components/Input/PhoneInput.swift @@ -10,11 +10,11 @@ import PhoneNumberKit struct PhoneInput: View { - @StateObject private var phoneNumberModel = PhoneNumberModel() + @EnvironmentObject private var model: Model @Binding var phoneNumber: String { didSet { - self.parsedPhoneNumber = self.phoneNumberModel.parse(phoneNumber: self.phoneNumber) + self.parsedPhoneNumber = self.model.parse(phoneNumber: self.phoneNumber) } } @Binding var parsedPhoneNumber: PhoneNumber? @@ -38,7 +38,7 @@ struct PhoneInput: View { showingPicker = true } label: { let flagRessource = ImageResource( - name: self.phoneNumberModel.selectedCountryData.regionCode.lowercased(), + name: self.model.selectedCountryData.regionCode.lowercased(), bundle: Bundle.main ) @@ -49,7 +49,7 @@ struct PhoneInput: View { .scaledToFill() .clipShape(Capsule()) - Text("\(self.phoneNumberModel.selectedCountryData.phoneCode)") + Text("\(self.model.selectedCountryData.phoneCode)") .foregroundStyle(.neutral2) .fontWeight(.medium) } @@ -61,10 +61,8 @@ struct PhoneInput: View { .buttonStyle(NoopButtonStyle()) .sheet(isPresented: $showingPicker) { CountryPickerView() - .environmentObject(self.phoneNumberModel) - .preferredColorScheme(.dark) .onAppear { - self.phoneNumberModel.searchedCountry = "" + self.model.searchedCountry = "" } } @@ -77,11 +75,11 @@ struct PhoneInput: View { } }) .keyboardType(.numberPad) - .onChange(of: self.phoneNumber, initial: false) { (_, newValue) in - self.phoneNumber = self.phoneNumberModel.format(phoneNumber: newValue) + .onChange(of: self.phoneNumber) { newValue in + self.phoneNumber = self.model.format(phoneNumber: newValue) } } - .onReceive(self.phoneNumberModel.$selectedRegionCode) { _ in + .onReceive(self.model.$selectedRegionCode) { _ in self.phoneNumber = "" } } @@ -101,7 +99,7 @@ struct PhoneInputPreviews : PreviewProvider { PhoneInput(phoneNumber: $text, parsedPhoneNumber: $phoneNumber) } } - }.preferredColorScheme(.dark) + } } } #endif diff --git a/app/App/Components/Line.swift b/app/App/Components/Line.swift new file mode 100644 index 00000000..246a9ae8 --- /dev/null +++ b/app/App/Components/Line.swift @@ -0,0 +1,17 @@ +// +// Line.swift +// Vault +// +// Created by Charles Lanier on 03/07/2024. +// + +import SwiftUI + +struct Line: Shape { + func path(in rect: CGRect) -> Path { + var path = Path() + path.move(to: CGPoint(x: 0, y: 0)) + path.addLine(to: CGPoint(x: rect.width, y: 0)) + return path + } +} diff --git a/app/App/Components/Modifiers/Placeholder.swift b/app/App/Components/Modifiers/Placeholder.swift new file mode 100644 index 00000000..c9b26191 --- /dev/null +++ b/app/App/Components/Modifiers/Placeholder.swift @@ -0,0 +1,60 @@ +// +// Placeholder.swift +// Vault +// +// Created by Charles Lanier on 03/07/2024. +// + +import SwiftUI + +struct AnimatePlaceholderModifier: AnimatableModifier { + @Binding var isLoading: Bool + + @State private var isAnim: Bool = false + private var center = (UIScreen.main.bounds.width / 2) + 110 + private let animation: Animation = .linear(duration: 1.5) + + init(isLoading: Binding) { + self._isLoading = isLoading + } + + func body(content: Content) -> some View { + content + .background(self.isLoading ? .background3 : .clear) + .clipShape(RoundedRectangle(cornerRadius: self.isLoading ? 4 : 0)) + .overlay(animView) + } + + var animView: some View { + ZStack { + Color.black.opacity(isLoading ? 0.09 : 0.0) + Color.background3.mask( + Rectangle() + .fill( + LinearGradient( + gradient: .init(colors: [.clear, .white.opacity(0.48), .clear]), + startPoint: .top , + endPoint: .bottom + ) + ) + .scaleEffect(1.5) + .rotationEffect(.init(degrees: 70.0)) + .offset(x: isAnim ? center : -center) + ) + } + .animation(isLoading ? animation.repeatForever(autoreverses: false) : nil, value: isAnim) + .onAppear { + guard isLoading else { return } + isAnim.toggle() + } + .onChange(of: isLoading) { + isAnim.toggle() + } + } +} + +extension View { + func animatePlaceholder(isLoading: Binding) -> some View { + self.modifier(AnimatePlaceholderModifier(isLoading: isLoading)) + } +} diff --git a/app/App/Components/Modifiers/Popover.swift b/app/App/Components/Modifiers/Popover.swift new file mode 100644 index 00000000..b1ca13cb --- /dev/null +++ b/app/App/Components/Modifiers/Popover.swift @@ -0,0 +1,88 @@ +// +// Popover.swift +// Vault +// +// Created by Charles Lanier on 04/06/2024. +// + +import SwiftUI + +struct InnerHeightPreferenceKey: PreferenceKey { + + static let defaultValue: CGFloat = .zero + + static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { + value = nextValue() + } +} + +struct PopoverModifier: ViewModifier where PopoverContent : View { + + @State private var sheetHeight: CGFloat = .zero + + @Binding var isPresented: Bool + + var popoverContent: () -> PopoverContent + + func body(content: Content) -> some View { + content + .sheet(isPresented: self.$isPresented) { + VStack { + VStack { + self.popoverContent() + } + .padding(EdgeInsets(top: 44, leading: 20, bottom: 32, trailing: 20)) + .frame(maxWidth: .infinity) + .background(.background2) + .clipShape(RoundedRectangle(cornerRadius: 32)) + } + .padding(EdgeInsets(top: 0, leading: 16, bottom: 16, trailing: 16)) + .overlay { + GeometryReader { geometry in + Color.clear.preference(key: InnerHeightPreferenceKey.self, value: geometry.size.height) + } + } + .onPreferenceChange(InnerHeightPreferenceKey.self) { newHeight in + self.sheetHeight = newHeight + } + .presentationDetents([.height(self.sheetHeight)]) + .presentationDragIndicator(.visible) + .presentationBackground(.clear) + } + } +} + +extension View { + public func sheetPopover( + isPresented: Binding, + @ViewBuilder content: @escaping () -> Content + ) -> some View where Content : View { + self.modifier( + PopoverModifier( + isPresented: isPresented, + popoverContent: content + ) + ) + } +} + +#if DEBUG +struct PopoverViewPreviews : PreviewProvider { + + @State static var isPresented = true + + static var previews: some View { + VStack { + Button("Open popover") { + self.isPresented = true + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .defaultBackground() + .sheetPopover(isPresented: self.$isPresented) { + Text("Holà, I'm the popover") + .textTheme(.headlineMedium) + } + } +} +#endif diff --git a/app/App/Components/Modifiers/Sending/AddSendingConfirmation.swift b/app/App/Components/Modifiers/Sending/AddSendingConfirmation.swift new file mode 100644 index 00000000..2db7bda5 --- /dev/null +++ b/app/App/Components/Modifiers/Sending/AddSendingConfirmation.swift @@ -0,0 +1,65 @@ +// +// AddSendingConfirmation.swift +// Vault +// +// Created by Charles Lanier on 29/06/2024. +// + +import SwiftUI + +struct AddSendingConfirmationModifier: ViewModifier { + + @EnvironmentObject var model: Model + + @Binding var isPresented: Bool + + @State var isConfirming = false + + var onDismiss: () -> Void + + func body(content: Content) -> some View { + content + .sheet(isPresented: self.$isPresented) { + if self.model.sendingStatus == .signed { + self.isConfirming = true + + Task { + await self.model.executeTransfer() + } + } + } content: { + SendingConfirmationView() + } + .sheetPopover(isPresented: .constant((self.model.sendingStatus == .loading || self.model.sendingStatus == .success) && self.isConfirming)) { + + Text("Executing your transfer").textTheme(.headlineSmall) + + Spacer().frame(height: 32) + + SpinnerView(isComplete: .constant(self.model.sendingStatus == .success)) + } + .onChange(of: self.model.sendingStatus) { newValue in + if !self.isPresented && !self.isConfirming { return } + + // close confirmation sheet on signing + if newValue == .signed { + self.isPresented = false + } else if newValue == .success { + Task { + try await Task.sleep(for: .seconds(1)) + + self.isConfirming = false + self.onDismiss() + } + } + } + } +} + +extension View { + public func addSendingConfirmation(isPresented: Binding, onDismiss: @escaping () -> Void) -> some View { + return self.modifier( + AddSendingConfirmationModifier(isPresented: isPresented, onDismiss: onDismiss) + ) + } +} diff --git a/app/App/Components/Modifiers/Sending/SendingConfirmationView.swift b/app/App/Components/Modifiers/Sending/SendingConfirmationView.swift new file mode 100644 index 00000000..1d7b9af5 --- /dev/null +++ b/app/App/Components/Modifiers/Sending/SendingConfirmationView.swift @@ -0,0 +1,138 @@ +// +// SendingConfirmationView.swift +// Vault +// +// Created by Charles Lanier on 04/06/2024. +// + +import SwiftUI + +struct SendingConfirmationView: View { + + @EnvironmentObject var model: Model + + @AppStorage("surname") var surname: String = "" + + var body: some View { + if let usdcAmount = Amount.usdc(from: self.model.parsedAmount) { + VStack { + Text("Finalize your transfer") + .textTheme(.headlineSmall) + .padding(.vertical, 32) + + Spacer().frame(height: 16) + + HStack { + VStack(spacing: 8) { + Avatar(salt: self.model.address, name: self.surname) + + Text("You") + .foregroundStyle(.neutral1) + .fontWeight(.semibold) + .textTheme(.subtitle) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 16) + .padding(.horizontal, 8) + .background(.background3.opacity(0.5)) + .clipShape(RoundedRectangle(cornerRadius: 10)) + + Spacer(minLength: 24) + + VStack { + Image(systemName: "arrow.right") + .font(.system(size: 12)) + .foregroundStyle(.neutral1) + .fontWeight(.bold) + } + .padding(.horizontal, 6) + .padding(.vertical, 8) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(.background3, lineWidth: 1) + ) + + Spacer(minLength: 24) + + VStack(spacing: 8) { + if let recipient = self.model.recipient { + Avatar( + salt: recipient.phoneNumber ?? recipient.address?.toHex(), + name: recipient.name, + data: recipient.imageData + ) + + Text(recipient.name) + .foregroundStyle(.neutral1) + .fontWeight(.semibold) + .textTheme(.subtitle) + .lineLimit(1) + } else { + // TODO: placeholder + EmptyView() + } + } + .frame(maxWidth: .infinity) + .padding(.vertical, 16) + .padding(.horizontal, 8) + .background(.background3.opacity(0.5)) + .clipShape(RoundedRectangle(cornerRadius: 10)) + } + + Spacer().frame(height: 16) + + HStack { + Text("Amount") + .textTheme(.bodySecondary) + + Spacer() + + Text("$\(usdcAmount.toFixed())") + .textTheme(.headlineSmall) + .padding(.top, 2) + } + .padding(.horizontal, 16) + .padding(.vertical, 16) + .background(.background3.opacity(0.5)) + .clipShape(RoundedRectangle(cornerRadius: 10)) + + Spacer() + + PrimaryButton("Send") { + Task { + await self.model.signTransfer() + } + } + } + .padding() + .presentationDetents([.medium, .large]) + } + } +} + +#Preview { + struct ConfirmationViewPreviews : View { + + @StateObject var model = { + let model = Model() + + model.setRecipient( + Recipient(name: "Very Long Bobby Name", phoneNumber: "+33612345678") + ) + + return model + }() + + var body: some View { + VStack {} + .frame(maxWidth: .infinity, maxHeight: .infinity) + .defaultBackground() + .sheet(isPresented: .constant(true)) { + SendingConfirmationView() + .environmentObject(model) + } + } + } + + return ConfirmationViewPreviews() +} diff --git a/app/App/Components/ThemedText.swift b/app/App/Components/Modifiers/ThemedText.swift similarity index 91% rename from app/App/Components/ThemedText.swift rename to app/App/Components/Modifiers/ThemedText.swift index 26a4f71c..43eb1b41 100644 --- a/app/App/Components/ThemedText.swift +++ b/app/App/Components/Modifiers/ThemedText.swift @@ -32,7 +32,7 @@ struct ThemedTextModifier: ViewModifier { .font(.custom("Sofia Pro", size: 46)) .fontWeight(.medium) .foregroundStyle(.neutral1) - .tracking(1.2) + .tracking(2) case .headlineLarge: content @@ -46,14 +46,14 @@ struct ThemedTextModifier: ViewModifier { .font(.custom("Sofia Pro", size: 20)) .fontWeight(.medium) .foregroundStyle(.neutral1) - .tracking(1.2) + .tracking(0.8) case .headlineSmall: content .font(.custom("Sofia Pro", size: 18)) .fontWeight(.medium) .foregroundStyle(.neutral1) - .tracking(1.2) + .tracking(0.6) case .headlineSubtitle: content @@ -126,13 +126,13 @@ extension View { Spacer() - Text("Hedaline Large").textTheme(.headlineLarge) - Text("Hedaline Medium").textTheme(.headlineMedium) - Text("Hedaline Small").textTheme(.headlineSmall) + Text("Headline Large").textTheme(.headlineLarge) + Text("Headline Medium").textTheme(.headlineMedium) + Text("Headline Small").textTheme(.headlineSmall) Spacer() - Text("Hedaline Subtitle").textTheme(.headlineSubtitle) + Text("Headline Subtitle").textTheme(.headlineSubtitle) Spacer() diff --git a/app/App/Components/NumPad.swift b/app/App/Components/NumPad.swift new file mode 100644 index 00000000..b95b1c80 --- /dev/null +++ b/app/App/Components/NumPad.swift @@ -0,0 +1,131 @@ +// +// NumPad.swift +// Vault +// +// Created by Charles Lanier on 17/05/2024. +// + +import SwiftUI + +enum PadTouch: Hashable, Identifiable { + var id: Self { + return self + } + + case char(String) + case backspace + + var label: some View { + Group { + switch self { + case .char(let symbol): + Text(symbol) + .textTheme(.headlineLarge) + + case .backspace: + Image(systemName: "delete.backward") + .font(.system(size: 20)) + .fontWeight(.medium) + .foregroundStyle(.neutral1) + } + } + .frame(maxWidth: .infinity, maxHeight: 60) + } +} + +struct NumPad: View { + + @Binding var amount: String + + private let pad = [ + Container( + [PadTouch.char("1"), PadTouch.char("2"), PadTouch.char("3")] + ), + Container( + [PadTouch.char("4"), PadTouch.char("5"), PadTouch.char("6")] + ), + Container( + [PadTouch.char("7"), PadTouch.char("8"), PadTouch.char("9")] + ), + Container( + [PadTouch.char(","), PadTouch.char("0"), PadTouch.backspace] + ), + ] + + private let regex = try! NSRegularExpression(pattern: "^\\d*,?\\d{0,2}$", options: []) + + var body: some View { + VStack(spacing: 0) { + ForEach(self.pad, id: \.id) { row in + HStack(spacing: 0) { + ForEach(row.elements, id: \.id) { touch in + Button { + let oldValue = self.amount + + switch touch { + case .char(let symbol): + self.amount += symbol + + case .backspace: + self.amount = String(self.amount.dropLast()) + } + + // format input + if let amount = self.formattedAmount(self.amount) { + self.amount = amount + } else { + self.amount = oldValue + } + } label: { + touch.label + } + } + } + } + } + } + + private func formattedAmount(_ amount: String) -> String? { + if amount == "" { + return "0" + } else if regex.firstMatch( + in: amount, + options: [], + range: NSRange(location: 0, length: amount.utf8.count) + ) != nil { + // remove useless 0 if needed + return amount.first == "0" && amount.count > 1 && amount[amount.index(amount.startIndex, offsetBy: 1)].isNumber + ? String(amount.dropFirst()) + : amount + } + + return nil + } +} + +#if DEBUG +struct NumPadPreviews : PreviewProvider { + + struct NumPadContainer: View { + @State private var amount: String = "100" + + var body: some View { + VStack { + Spacer() + + FancyAmount(amount: $amount) + + Spacer() + + NumPad(amount: $amount) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .defaultBackground() + } + } + + static var previews: some View { + NumPadContainer() + } +} +#endif diff --git a/app/App/Components/SpinnerView.swift b/app/App/Components/SpinnerView.swift index 0c541b36..71fd98e6 100644 --- a/app/App/Components/SpinnerView.swift +++ b/app/App/Components/SpinnerView.swift @@ -7,30 +7,77 @@ import SwiftUI +struct CheckmarkShape: Shape { + func path(in rect: CGRect) -> Path { + var path = Path() + + path.move(to: CGPoint(x: rect.minX, y: rect.maxY * 0.5)) + path.addLine(to: CGPoint(x: rect.maxX * 0.4, y: rect.maxY)) + path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY)) + + return path + } +} + struct SpinnerView: View { - @State private var spinnerLength = 0.6 - @State private var degree:Int = 270 + @State private var isSpinning = false + @State private var isTrimming = false + + @Binding var isComplete: Bool { + didSet { + self.isTrimming = false + } + } var body: some View { - Circle() - .trim(from: 0.0,to: spinnerLength) - .stroke(.accent, style: StrokeStyle(lineWidth: 5.0,lineCap: .round,lineJoin:.round)) - .rotationEffect(Angle(degrees: Double(degree))) - .frame(width: 48,height: 48) - .onAppear { - withAnimation(Animation.easeIn(duration: 1.5).repeatForever(autoreverses: true)) { - spinnerLength = 0.05 - } - withAnimation(Animation.linear(duration: 1).repeatForever(autoreverses: false)) { - degree = 270 + 360 - } + ZStack { + VStack { + Circle() + .trim(from: 0.0,to: self.isComplete ? 1 : self.isTrimming ? 0.05 : 0.6) + .stroke(.accent, style: StrokeStyle(lineWidth: 5.0,lineCap: .round,lineJoin:.round)) + .frame(width: 48, height: 48) + .animation(.easeIn(duration: 1.5).repeatForever(autoreverses: true), value: self.isTrimming) + .animation(.linear(duration: 0.3), value: self.isComplete) } + .rotationEffect(Angle(degrees: isSpinning ? 360 : 0)) + .animation(.linear(duration: 1.0).repeatForever(autoreverses: false), value: isSpinning) + + CheckmarkShape() + .trim(from: 0, to: isComplete ? 1 : 0) + .stroke(style: StrokeStyle(lineWidth: 5, lineCap: .round, lineJoin: .round)) + .frame(width: 20, height: 20) + .offset(y: 1) + .foregroundColor(.accent) + .animation(.easeIn(duration: 0.2).delay(0.3), value: isComplete) + + } + .onAppear { + // small delay because SwiftUI animation are broken + Task { @MainActor in + try await Task.sleep(for: .seconds(0.1)) + + self.isTrimming = true + self.isSpinning = true + } + } } } #Preview { - ZStack { - SpinnerView() + struct Preview: View { + + @State var isComplete = false + + var body: some View { + SpinnerView(isComplete: $isComplete) + .background(.background1) + .onTapGesture { + // Simulate the completion of the task + self.isComplete = true + } + } } + + return Preview() } diff --git a/app/App/Components/WebView.swift b/app/App/Components/WebView.swift index 2e0c011e..d6215599 100644 --- a/app/App/Components/WebView.swift +++ b/app/App/Components/WebView.swift @@ -17,7 +17,33 @@ struct WebView: UIViewRepresentable { } func updateUIView(_ webView: WKWebView, context: Context) { + webView.uiDelegate = context.coordinator let request = URLRequest(url: url) webView.load(request) } + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } +} + +class Coordinator: NSObject, WKUIDelegate { + var parent: WebView + + init(_ parent: WebView) { + self.parent = parent + } + + // Delegate methods go here + + @available(iOS 15, *) + func webView( + _ webView: WKWebView, + requestMediaCapturePermissionFor origin: WKSecurityOrigin, + initiatedByFrame frame: WKFrameInfo, + type: WKMediaCaptureType, + decisionHandler: @escaping (WKPermissionDecision) -> Void + ) { + decisionHandler(.grant) + } } diff --git a/app/App/Constants/Configuration.swift b/app/App/Constants/Configuration.swift new file mode 100644 index 00000000..eacd1783 --- /dev/null +++ b/app/App/Constants/Configuration.swift @@ -0,0 +1,51 @@ +// +// Configuration.swift +// Vault +// +// Created by Charles Lanier on 18/06/2024. +// + +import Foundation +import Starknet + +enum AppConfiguration { + enum Error: Swift.Error { + case missingKey, invalidValue + } + + static func value(for key: String) throws -> T where T: LosslessStringConvertible { + guard let object = Bundle.main.object(forInfoDictionaryKey: key) else { + throw Error.missingKey + } + + switch object { + case let value as T: + return value + case let string as String: + guard let value = T(string) else { fallthrough } + return value + default: + throw Error.invalidValue + } + } + + enum API { + static var baseURL: URL { + let base: String = try! AppConfiguration.value(for: "API_BASE_URL") + return URL(string: "https://" + base)! + } + } + + enum Misc { + static var privateKeyLabel: String { + try! AppConfiguration.value(for: "PRIVATE_KEY_LABEL") + } + } + + enum Starknet { + static var starknetChainId: StarknetChainId { + let networkName: String = try! AppConfiguration.value(for: "SN_NETWORK") + return StarknetChainId(fromNetworkName: networkName) + } + } +} diff --git a/app/App/Constants/Constants.swift b/app/App/Constants/Constants.swift index f4adfd1d..60877725 100644 --- a/app/App/Constants/Constants.swift +++ b/app/App/Constants/Constants.swift @@ -7,11 +7,11 @@ import Foundation import SwiftUI +import Starknet struct Constants { - static let usdcDecimals = 6 - static let usdcDecimalPlaces: Double = pow(10, Double(usdcDecimals)) + static let usdcDecimals: UInt8 = 6 static let gradient1 = Gradient(colors: [.gradient1A, .gradient1B]) static let linearGradient1 = LinearGradient( @@ -22,13 +22,36 @@ struct Constants { static let registrationCodeDigitsCount = 6 - // MARK: ENV + // MARK: ICONS - static let vaultBaseURL: URL = { - guard let urlString = ProcessInfo.processInfo.environment["VAULT_API_BASE_URL"], - let url = URL(string: urlString) else { - fatalError("Vault API Base URL not configured properly.") + struct Icons { + static let arrowUp = Self.renderIcon("ArrowUp") + static let arrowDown = Self.renderIcon("ArrowDown") + static let plus = Self.renderIcon("Plus") + + static private func renderIcon(_ name: String) -> any View { + return Image(name) + .renderingMode(.template) + .resizable() + .aspectRatio(contentMode: .fit) } - return url - }() + } + + // MARK: STARKNET + + struct Starknet { + static private let vaultFactoryAddresses = [ + StarknetChainId.main.value: Felt("0x410da9af28e654fa93354430841ce7c5f0c2c17cc92971fb23d3d4f826d9834"), + StarknetChainId.sepolia.value: Felt("0x33498f0d9e6ebef71b3d8dfa56501388cfe5ce96cba81503cd8572be92bd77c"), + ] + static let vaultFactoryAddress: Felt = Self.vaultFactoryAddresses[AppConfiguration.Starknet.starknetChainId.value]! + + static private let usdcAddresses = [ + StarknetChainId.main.value: Felt("0x053c91253bc9682c04929ca02ed00b3e423f6710d2ee7e0d5ebb06f3ecf368a8"), + StarknetChainId.sepolia.value: Felt("0x07ab0b8855a61f480b4423c46c32fa7c553f0aac3531bbddaa282d86244f7a23"), + ] + static let usdcAddress: Felt = Self.usdcAddresses[AppConfiguration.Starknet.starknetChainId.value]! + + static let blankAccountClassHash = Felt("0x1fa186ff7ea06307ded0baa1eb7648afc43618b92084da1110a9c0bd2b6bf56"); + } } diff --git a/app/App/Managers/SecureEnclaveManager.swift b/app/App/Managers/SecureEnclaveManager.swift index 60f40f9f..53d02b71 100644 --- a/app/App/Managers/SecureEnclaveManager.swift +++ b/app/App/Managers/SecureEnclaveManager.swift @@ -6,34 +6,36 @@ // import Foundation +import Starknet -struct PublicKey { - var x: Data - var y: Data - - var debugDescription: String { - return "{ x: \(self.x.toHex()), y: \(self.y.toHex()) }" - } -} - -struct Signature { - var r: Data - var s: Data - - var debugDescription: String { - return "{ r: \(self.r.toHex()), s: \(self.s.toHex()) }" - } +struct P256Signature { + var r: Uint256 + var s: Uint256 } class SecureEnclaveManager { static let shared = SecureEnclaveManager() - static let privateKeyLabel = "com.vault.keys.privateKey" - // MARK: Public - public func generateKeyPair() throws -> PublicKey? { + public func getOrGenerateKeyPair() throws -> P256PublicKey? { + do { + let privateKey = try self.getPrivateKey(); + + // get public key from private key + guard let publicKey = SecKeyCopyPublicKey(privateKey) else { + throw "Error obtaining public key from private key." + } + + // extract public key data + return self.parse(publicKey: publicKey) + } catch { + return try! self.generateKeyPair() + } + } + + public func generateKeyPair() throws -> P256PublicKey? { // compute private key access rights let access = SecAccessControlCreateWithFlags( kCFAllocatorDefault, @@ -50,7 +52,7 @@ class SecureEnclaveManager { kSecPrivateKeyAttrs: [ kSecAttrIsPermanent: true, kSecAttrAccessControl: access, - kSecAttrLabel: Self.privateKeyLabel, + kSecAttrLabel: AppConfiguration.Misc.privateKeyLabel, ], ] @@ -72,38 +74,23 @@ class SecureEnclaveManager { return parsedPublicKey } - public func sign(message: Data) throws -> Signature { + public func sign(hash: Felt) throws -> P256Signature { let privateKey = try self.getPrivateKey() + let hashData = hash.value.serialize() - guard let publicKey = SecKeyCopyPublicKey(privateKey) else { - throw "Error obtaining public key from private key." - } - - // extract public key data - guard let parsedPublicKey = self.parse(publicKey: publicKey) else { - throw "Error parsing public key." - } - - print("Public key: \(publicKey)\n") - print("Public key: \(parsedPublicKey.debugDescription)\n") - - // signature - var hash = "04c659dac4479d23f29a8b7c44e30c87e6f0d662a40b25007eebfaeaa1cb086c" - guard let signature = self.sign(hash: Data(hex: hash)!, with: privateKey) else { + // Signature + guard let signature = self.sign(hash: hashData, with: privateKey) else { throw "Error signing hash." } - print("Hash: 0x\(hash)") - print("Signature: \(signature.debugDescription)") - - // signature - hash = "0601d3d2e265c10ff645e1554c435e72ce6721f0ba5fc96f0c650bfc6231191a" - guard let signature = self.sign(hash: Data(hex: hash)!, with: privateKey) else { - throw "Error signing hash." - } + #if DEBUG + print("Hash: \(hash.toHex())") - print("Hash: 0x\(hash)") - print("Signature: \(signature.debugDescription)") + print("Signature: \(signature)\n") + print("Signature:") + print("R: \(signature.r.low) - \(signature.r.high)\n") + print("S: \(signature.s.low) - \(signature.s.high)\n") + #endif return signature } @@ -114,18 +101,19 @@ class SecureEnclaveManager { let query: [String: Any] = [ kSecClass as String: kSecClassKey, kSecAttrKeyClass as String: kSecAttrKeyClassPrivate, - kSecAttrLabel as String: Self.privateKeyLabel, + kSecAttrLabel as String: AppConfiguration.Misc.privateKeyLabel, + kSecMatchLimit as String: kSecMatchLimitAll, kSecReturnRef as String: true, ] - var item: CFTypeRef? - let status = SecItemCopyMatching(query as CFDictionary, &item) + var items: CFTypeRef? + let status = SecItemCopyMatching(query as CFDictionary, &items) guard status == errSecSuccess else { throw "Failed to retrieve private key" } - return item as! SecKey + return items!.lastObject as! SecKey } - private func sign(hash: Data, with privateKey: SecKey) -> Signature? { + private func sign(hash: Data, with privateKey: SecKey) -> P256Signature? { var error: Unmanaged? guard let signature = SecKeyCreateSignature( @@ -144,13 +132,13 @@ class SecureEnclaveManager { // MARK: Parsing - private func parse(publicKey: SecKey) -> PublicKey? { + private func parse(publicKey: SecKey) -> P256PublicKey? { var error: Unmanaged? if let publicKeyData = SecKeyCopyExternalRepresentation(publicKey, &error) as Data? { - return PublicKey( - x: publicKeyData[1..<33], - y: publicKeyData[33..<65] + return P256PublicKey( + x: Uint256(fromHex: publicKeyData[1..<33].toHex())!, + y: Uint256(fromHex: publicKeyData[33..<65].toHex())! ) } else { if let error = error { @@ -160,15 +148,15 @@ class SecureEnclaveManager { } } - private func parse(signature signatureData: Data) -> Signature { - let rLength = Int(signatureData[3]) - 1 - let sLength = Int(signatureData[6 + rLength]) - 1 - let rOffset = 5 - let sOffset = 8 + rLength + private func parse(signature signatureData: Data) -> P256Signature { + let rLength = Int(signatureData[3]) + let sLength = Int(signatureData[5 + rLength]) + let rOffset = 4 + let sOffset = 6 + rLength - return Signature( - r: signatureData[rOffset..<(rOffset + rLength)], - s: signatureData[sOffset..<(sOffset + sLength)] + return P256Signature( + r: Uint256(fromHex: signatureData[rOffset..<(rOffset + rLength)].toHex())!, + s: Uint256(fromHex: signatureData[sOffset..<(sOffset + sLength)].toHex())! ) } } diff --git a/app/App/Model/PhoneNumberModel.swift b/app/App/Model/PhoneNumberModel.swift deleted file mode 100644 index def7fe31..00000000 --- a/app/App/Model/PhoneNumberModel.swift +++ /dev/null @@ -1,84 +0,0 @@ -// -// PhoneNumberModel.swift -// Vault -// -// Created by Charles Lanier on 11/04/2024. -// - -import Foundation -import PhoneNumberKit - -struct CountryData: Hashable { - let regionCode: String - let phoneCode: String - let name: String -} - -class PhoneNumberModel: ObservableObject { - - @Published var selectedRegionCode = Locale.current.regionOrFrance.identifier - @Published var searchedCountry = "" - - var selectedCountryData: CountryData { - return self.countryData(forRegionCode: self.selectedRegionCode) - } - - private let phoneNumberKit = PhoneNumberKit() - - private lazy var countries: [CountryData] = { - phoneNumberKit.allCountries() - .map { regionCode in - return self.countryData(forRegionCode: regionCode) - } - .filter { $0.name != "world" } - .sorted { $0.name < $1.name } - }() - - var filteredCountries: [CountryData] { - if self.searchedCountry.isEmpty { - return self.countries - } else { - return self.countries.filter { $0.name.lowercased().contains(self.searchedCountry.lowercased()) } - } - } - - private var partialFormatter: PartialFormatter { - PartialFormatter( - phoneNumberKit: self.phoneNumberKit, - defaultRegion: self.selectedRegionCode, - withPrefix: false, - ignoreIntlNumbers: true - ) - } - - // MARK: Functions - - func isSelected(_ regionCode: String) -> Bool { - return self.selectedRegionCode == regionCode - } - - func format(phoneNumber: String) -> String { - return self.partialFormatter.formatPartial(phoneNumber) - } - - func parse(phoneNumber: String) -> PhoneNumber? { - do { - return try phoneNumberKit.parse(phoneNumber, withRegion: self.selectedRegionCode, ignoreType: true) - } catch { - return nil - } - } - - // MARK: Internal - - private func countryData(forRegionCode regionCode: String) -> CountryData { - let countryName = Locale.current.localizedString(forRegionCode: regionCode) ?? "world" - let phoneCode = phoneNumberKit.countryCode(for: regionCode) ?? 0 - - return CountryData( - regionCode: regionCode, - phoneCode: "+\(phoneCode)", - name: countryName - ) - } -} diff --git a/app/App/Model/RegistrationModel.swift b/app/App/Model/RegistrationModel.swift deleted file mode 100644 index 25ca9cef..00000000 --- a/app/App/Model/RegistrationModel.swift +++ /dev/null @@ -1,53 +0,0 @@ -// -// RegistrationModel.swift -// Vault -// -// Created by Charles Lanier on 21/04/2024. -// - -import Foundation -import PhoneNumberKit - -class RegistrationModel: ObservableObject { - - @Published var isLoading = false - - private var vaultService: VaultService - - init(vaultService: VaultService) { - self.vaultService = vaultService - } - - func startRegistration(phoneNumber: PhoneNumber, completion: @escaping (Result) -> Void) { - self.isLoading = true - -// DispatchQueue.main.asyncAfter(deadline: .now() + 2) { -// self.isLoading = false -// completion(.success(Void())) -// } - - vaultService.getOTP(phoneNumber: phoneNumber.rawString()) { result in - self.isLoading = false - completion(result) - } - } - - func confirmRegistration( - phoneNumber: PhoneNumber, - otp: String, - publicKey: PublicKey, - completion: @escaping (Result - ) -> Void) { - self.isLoading = true - -// DispatchQueue.main.asyncAfter(deadline: .now() + 2) { -// self.isLoading = false -// completion(.success("0xdead")) -// } - - vaultService.verifyOTP(phoneNumber: phoneNumber.rawString(), otp: otp, publicKey: publicKey) { result in - self.isLoading = false - completion(result) - } - } -} diff --git a/app/App/Model/SettingsModel.swift b/app/App/Model/SettingsModel.swift deleted file mode 100644 index ef5511d1..00000000 --- a/app/App/Model/SettingsModel.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// SettingsModel.swift -// Vault -// -// Created by Charles Lanier on 26/03/2024. -// - -import Foundation - -// UserDefaults Keys -enum UserDefaultsKeys: String { - case surname - case isOnboarded -} - -class SettingsModel: ObservableObject { - - @Published var surname: String { - didSet { - UserDefaults.standard.set(surname, forKey: UserDefaultsKeys.surname.rawValue) - } - } - @Published var isOnboarded: Bool { - didSet { - UserDefaults.standard.set(isOnboarded, forKey: UserDefaultsKeys.isOnboarded.rawValue) - } - } - - init() { - self.surname = UserDefaults.standard.string(forKey: UserDefaultsKeys.surname.rawValue) ?? "" - self.isOnboarded = UserDefaults.standard.bool(forKey: UserDefaultsKeys.isOnboarded.rawValue) - } -} diff --git a/app/App/Models/Contact.swift b/app/App/Models/Contact.swift new file mode 100644 index 00000000..3007e5ef --- /dev/null +++ b/app/App/Models/Contact.swift @@ -0,0 +1,77 @@ +// +// Recipient.swift +// Vault +// +// Created by Charles Lanier on 22/05/2024. +// + +import Foundation +import Contacts +import UIKit +import Starknet +import BigInt + +enum AddressOrPhoneNumber { + case address(String) + case phoneNumber(String) +} + +struct Recipient: Identifiable { + var id = UUID() + var name: String + var addressOrPhone: AddressOrPhoneNumber + var imageData: Data? + + init(name: String, address: String, imageData: Data? = nil) { + self.name = name + self.addressOrPhone = .address(address) + self.imageData = imageData + } + + init(name: String, phoneNumber: String, imageData: Data? = nil) { + self.name = name + self.addressOrPhone = .phoneNumber(phoneNumber) + self.imageData = imageData + } + + var phoneNumber: String? { + switch self.addressOrPhone { + case .address: + return nil + + case .phoneNumber(let phoneNumber): + return phoneNumber + } + } + + // addr utils + + var address: Felt? { + switch self.addressOrPhone { + case .address(let address): + return Felt(fromHex: address) + + case .phoneNumber(let phoneNumber): + guard let phoneNumberFelt = Felt.fromShortString(phoneNumber) else { + return nil + } + + // TODO: remove this extra step after nonce support + guard let phoneNumberHex = Felt.fromShortString(phoneNumberFelt.toHex())?.toHex().dropFirst(2) else { + return nil + } + + guard let phoneNumberBytes = BigUInt(phoneNumberHex, radix: 16)?.serialize().bytes else { + return nil + } + + return StarknetContractAddressCalculator.calculateFrom( + classHash: Constants.Starknet.blankAccountClassHash, + calldata: [], + salt: starknetKeccak(on: phoneNumberBytes), + deployerAddress: Constants.Starknet.vaultFactoryAddress + ) + } + + } +} diff --git a/app/App/Models/CountryData.swift b/app/App/Models/CountryData.swift new file mode 100644 index 00000000..4844a989 --- /dev/null +++ b/app/App/Models/CountryData.swift @@ -0,0 +1,14 @@ +// +// CountryData.swift +// Vault +// +// Created by Charles Lanier on 03/06/2024. +// + +import Foundation + +struct CountryData: Hashable { + let regionCode: String + let phoneCode: String + let name: String +} diff --git a/app/App/Models/History.swift b/app/App/Models/History.swift new file mode 100644 index 00000000..4a164d14 --- /dev/null +++ b/app/App/Models/History.swift @@ -0,0 +1,75 @@ +// +// History.swift +// Vault +// +// Created by Charles Lanier on 19/06/2024. +// + +import Foundation + +struct User: Hashable { + let nickname: String? + let avatarUrl: String? = nil + let address: String? + let phoneNumber: String? + + init(transactionUser: RawTransactionUser) { + self.nickname = transactionUser.nickname ?? transactionUser.phone_number + self.address = transactionUser.contract_address + self.phoneNumber = transactionUser.phone_number + } +} + +struct Transaction: Identifiable, Hashable { + + typealias ID = String + + let from: User + let to: User + let amount: Amount + let date: Date + let isSending: Bool + let balance: Amount + let id: ID + + static func == (lhs: Transaction, rhs: Transaction) -> Bool { + return lhs.id == rhs.id + } + + static let dateFormatter = { + let dateFormatter = DateFormatter() + + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + dateFormatter.locale = Locale(identifier: "en_US_POSIX") + dateFormatter.timeZone = TimeZone(secondsFromGMT: 0) + + return dateFormatter + }() + + init(address: String, transaction: RawTransaction) { + self.from = User(transactionUser: transaction.from) + self.to = User(transactionUser: transaction.to) + self.amount = Amount.usdc(from: transaction.amount)! + self.date = Self.dateFormatter.date(from: transaction.transaction_timestamp)! + self.isSending = transaction.from.contract_address == address + self.balance = Amount.usdc(from: self.isSending ? transaction.from.balance : transaction.to.balance)! + self.id = transaction.transferId + } + + init( + address: String, + from: RawTransactionUser, + to: RawTransactionUser, + amount: Amount, + date: Date, + transferId: ID + ) { + self.from = User(transactionUser: from) + self.to = User(transactionUser: to) + self.amount = amount + self.date = date + self.isSending = from.contract_address == address + self.balance = Amount.usdc(from: 0)! + self.id = transferId + } +} diff --git a/app/App/Models/Model.swift b/app/App/Models/Model.swift new file mode 100644 index 00000000..15e1e294 --- /dev/null +++ b/app/App/Models/Model.swift @@ -0,0 +1,614 @@ +// +// Model.swift +// Vault +// +// Created by Charles Lanier on 11/04/2024. +// + +import Foundation +import PhoneNumberKit +import Contacts +import SwiftUI +import Starknet +import BigInt + +enum Status: Equatable { + case none // TODO: find a better name + case loading + case signing + case signed + case success + case error(String) +} + +// Aggregate Model +@MainActor +class Model: ObservableObject { + + @AppStorage("starknetMainAddress") var address: String = "" + @AppStorage("isOnboarded") var isOnboarded: Bool = false + @AppStorage("surname") var surname: String = "" + + // App + @Published var isLoading = false + @Published var showMessage = false + @Published var amount: String = "" + + // Onramp + @Published var showOnrampView = false { + didSet { + if self.showOnrampView { + self.initiateOnramp() + } + } + } + @Published var onRampQuoteId: String? + @Published var onRampTotalUsd: String? + @Published var estSubtotalUsd: Double = 0 + @Published var paymentTokenAmount: Double = 0 + @Published var stripeRedirectUrl: URL? = nil + + // Sending USDC + @Published var recipient: Recipient? + @Published var sendingStatus: Status = .none + @Published var showSendingView = false { + didSet { + if self.showSendingView { + self.initiateTransfer() + } else { + self.dismissTransfer() + } + } + } + @Published var showSendingConfirmation = false + @Published var pendingTransaction: Transaction? = nil + + // Requesting USDC + @Published var showRequestingView = false { + didSet { + if self.showRequestingView { + self.initiateRequest() + } + } + } + @Published var showRequestingConfirmation = false + + // Starknet + @Published var outsideExecution: OutsideExecution? + @Published var outsideExecutionSignature: StarknetSignature? + + // Country picker + @Published var selectedRegionCode = Locale.current.regionOrFrance.identifier + @Published var searchedCountry = "" + + // Contacts + @Published var contacts: [Recipient] = [] + @Published var contactsMapping: [String: [Recipient]] = [:] + @Published var contactsAuthorizationStatus: CNAuthorizationStatus = .notDetermined + + var parsedAmount: Double { + // Replace the comma with a dot + let amount = self.amount.replacingOccurrences(of: ",", with: ".") + + // Check if the string ends with a dot and append a zero if true + // Convert the final string to a Float + return Double(amount.hasSuffix(".") ? "\(amount)0" : amount) ?? 0 + } + + private var contactStore = CNContactStore() + + private let phoneNumberKit = PhoneNumberKit() + private lazy var countries: [CountryData] = { + phoneNumberKit.allCountries() + .map { regionCode in + return self.countryData(forRegionCode: regionCode) + } + .filter { $0.name != "world" } + .sorted { $0.name < $1.name } + }() + + private lazy var signer = P256Signer() + + private var currentTask: Task? { + willSet { + if let task = currentTask { + if task.isCancelled { return } + task.cancel() + // Setting a new task cancelling the current task + } + } + } + + init() { + // Contacts + self.checkContactsAuthorizationStatus() + } +} + +// MARK: - Vault API + +extension Model { + + func startRegistration(phoneNumber: PhoneNumber, onSuccess: @escaping () -> Void) { + self.isLoading = true + + VaultService.shared.send(GetOTP(phoneNumber: phoneNumber, nickname: self.surname)) { result in + DispatchQueue.main.async { + self.isLoading = false + + switch result { + case .success: + onSuccess() + + case .failure(let error): + // TODO: Handle error +#if DEBUG + print(error) +#endif + } + } + } + } + + func confirmRegistration( + phoneNumber: PhoneNumber, + otp: String, + onSuccess: @escaping () -> Void + ) { + do { + guard let publicKey = try SecureEnclaveManager.shared.getOrGenerateKeyPair() else { + throw "Failed to generate public key." + } + + self.isLoading = true + + VaultService.shared.send(VerifyOTP(phoneNumber: phoneNumber, sentOTP: otp, publicKey: publicKey)) { result in + DispatchQueue.main.async { + self.isLoading = false + + switch result { + case .success(let response): + self.address = response.contract_address + onSuccess() + + case .failure(let error): + // TODO: Handle error +#if DEBUG + print(error) +#endif + } + } + } + } catch { + // TODO: Handle error + #if DEBUG + print(error) + #endif + } + } +} + +// MARK: - Country picker + +extension Model { + + var selectedCountryData: CountryData { + return self.countryData(forRegionCode: self.selectedRegionCode) + } + + var filteredCountries: [CountryData] { + if self.searchedCountry.isEmpty { + return self.countries + } else { + return self.countries.filter { $0.name.lowercased().contains(self.searchedCountry.lowercased()) } + } + } + + private var partialFormatter: PartialFormatter { + PartialFormatter( + phoneNumberKit: self.phoneNumberKit, + defaultRegion: self.selectedRegionCode, + withPrefix: false, + ignoreIntlNumbers: true + ) + } + + func isSelected(_ regionCode: String) -> Bool { + return self.selectedRegionCode == regionCode + } + + func format(phoneNumber: String) -> String { + return self.partialFormatter.formatPartial(phoneNumber) + } + + func parse(phoneNumber: String) -> PhoneNumber? { + do { + return try phoneNumberKit.parse(phoneNumber, withRegion: self.selectedRegionCode, ignoreType: true) + } catch { + return nil + } + } +} + +// MARK: - Starknet + +extension Model { + + // sign + + func signOutsideExecution(outsideExecution: OutsideExecution) async throws -> StarknetSignature { + let feltAddress = Felt(fromHex: self.address)! + + print("MessageHash: \(self.outsideExecution!.getMessageHash(forSigner: feltAddress))") + return try self.signer.sign(transactionHash: self.outsideExecution!.getMessageHash(forSigner: feltAddress)) + } +} + +// MARK: - Contacts + +extension Model { + + public func checkContactsAuthorizationStatus() { + self.contactsAuthorizationStatus = CNContactStore.authorizationStatus(for: .contacts) + + switch self.contactsAuthorizationStatus { + case .notDetermined, .denied, .restricted: + break + + case .authorized: + self.fetchContacts() + + @unknown default: + fatalError("Unknown authorization status") + } + } + + public func requestContactsAccess() { + if self.contactsAuthorizationStatus == .denied { + self.openSettings() + } else if self.contactsAuthorizationStatus == .notDetermined { + contactStore.requestAccess(for: .contacts) { [weak self] (granted, error) in + DispatchQueue.main.async { + if granted { + self?.contactsAuthorizationStatus = .authorized + self?.fetchContacts() + } else { + self?.contactsAuthorizationStatus = .denied + // TODO: Handle the case where permission is denied + print("Permission denied") + } + } + } + } + } + + func addContact(name: String, phoneNumber: String, completionHandler: @escaping (Recipient) -> Void) { + DispatchQueue.global(qos: .userInitiated).async { [weak self] in + let newContact = CNMutableContact() + let nameComponents = name.split(separator: " ") + + if let firstName = nameComponents.first { + newContact.givenName = String(firstName) + } + + if nameComponents.count > 1 { + newContact.familyName = nameComponents.dropFirst().joined(separator: " ") + } + + let phoneValue = CNLabeledValue(label: CNLabelPhoneNumberMain, value: CNPhoneNumber(stringValue: phoneNumber)) + newContact.phoneNumbers = [phoneValue] + + let saveRequest = CNSaveRequest() + saveRequest.add(newContact, toContainerWithIdentifier: nil) + + do { + try self?.contactStore.execute(saveRequest) + + DispatchQueue.main.async { + self?.fetchContacts() // Refresh the contacts list + completionHandler(Recipient(name: name, phoneNumber: phoneNumber)) + } + } catch { + print("Failed to save contact: \(error)") + // TODO: Handle this case + } + } + } +} + +// MARK: - Sending Logic + +extension Model { + + func setRecipient(_ recipient: Recipient) { + self.recipient = recipient + } + + func executeTransfer() async { + guard + let outsideExecution = self.outsideExecution, + let outsideExecutionSignature = self.outsideExecutionSignature + else { + self.sendingStatus = .error("Invalid request.") + return + } + + self.sendingStatus = .loading + + VaultService.shared.send( + ExecuteFromOutside( + address: self.address, + outsideExecution: outsideExecution, + signature: outsideExecutionSignature + ) + ) { result in + DispatchQueue.main.async { + switch result { + case .success(let response): + let recipient = self.recipient! + let amount = Amount.usdc(from: self.parsedAmount)! + + self.pendingTransaction = Transaction( + address: self.address, + from: RawTransactionUser(nickname: self.surname, contract_address: self.address, phone_number: nil, balance: ""), + to: RawTransactionUser(nickname: recipient.name, contract_address: nil, phone_number: recipient.phoneNumber, balance: ""), + amount: amount, + date: Date(), + transferId: "\(response.transaction_hash)_0" + ) + self.sendingStatus = .success +#if DEBUG + print("tx: \(response.transaction_hash)") +#endif + + case .failure(let error): + self.sendingStatus = .success + // self.sendingStatus = .error("An error has occured during the transaction.") + +#if DEBUG + print(error) +#endif + } + } + } + } + + func signTransfer() async { + guard + let recipient = self.recipient, + let recipientAddress = recipient.address, + let amount = Amount.usdc(from: self.parsedAmount) + else { + self.sendingStatus = .error("Invalid request.") + return + } + + let call = StarknetCall( + contractAddress: Constants.Starknet.usdcAddress, + entrypoint: starknetSelector(from: "transfer"), + calldata: [ + recipientAddress, + amount.value.low, + amount.value.high, + ] + ) + self.outsideExecution = OutsideExecution(calls: [call]) + + self.sendingStatus = .signing + + do { + self.outsideExecutionSignature = try await self.signOutsideExecution(outsideExecution: self.outsideExecution!) + + self.sendingStatus = .signed + } catch let error { + self.sendingStatus = .none + +#if DEBUG + print(error) +#endif + } + } +} + +// MARK: - Deeplinks + +extension Model { + public func handleDeepLink(_ url: URL) { + guard url.scheme == "vltfinance" else { + return + } + + guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true) else { + print("Invalid URL") + return + } + + guard let action = components.host, action == "request" else { + print("Unknown URL, we can't handle this one!") + return + } + + guard + let amount = components.queryItems?.first(where: { $0.name == "amount" })?.value, + let parsedAmount = Amount.usdc(fromHex: amount), + let recipientAddress = components.queryItems?.first(where: { $0.name == "recipientAddress" })?.value + else { + print("Invalid payment request") + return + } + + VaultService.shared.send(GetUser(address: recipientAddress)) { result in + DispatchQueue.main.async { + self.isLoading = false + + switch result { + case .success(let response): + self.recipient = Recipient(name: response.user, address: recipientAddress) + + case .failure(let error): + self.recipient = Recipient(name: "UNKNOWN", address: recipientAddress) + + // TODO: Handle error +#if DEBUG + print(error) +#endif + } + } + } + + self.amount = parsedAmount.toFixed(2) + self.showRequestingConfirmation = true + } +} + +// MARK: - Onramp + +extension Model { + + public func getOnrampQuote() { + self.onRampQuoteId = nil + self.onRampTotalUsd = nil + + if self.parsedAmount <= 0 { return } + + self.isLoading = true + + self.currentTask = Task { + let response = try! await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + VaultService.shared.send(GetFunkitStripeCheckoutQuote(address: self.address, amount: String(self.parsedAmount))) { result in + DispatchQueue.main.async { + switch result { + case .success(let response): + continuation.resume(returning: response) + + case .failure(let error): + // TODO: Handle error + continuation.resume(throwing: error) + } + } + } + } + + // task has been cancelled + if Task.isCancelled { return } + + self.isLoading = false + + self.onRampQuoteId = response.quoteId + self.onRampTotalUsd = response.totalUsd + self.estSubtotalUsd = response.estSubtotalUsd + self.paymentTokenAmount = response.paymentTokenAmount + } + } + + public func createOnrampCheckout() { + guard let quoteId = self.onRampQuoteId else { return } + + VaultService.shared.send( + CreateFunkitStripeCheckout( + quoteId: quoteId, + paymentTokenAmount: self.paymentTokenAmount, + estSubtotalUsd: self.estSubtotalUsd + ) + ) { result in + DispatchQueue.main.async { + switch result { + case .success(let response): + self.stripeRedirectUrl = URL(string: response.stripeRedirectUrl) + + case .failure(let error): + // TODO: Handle error + print(error) + } + } + } + } +} + +// MARK: - Private Logic + +extension Model { + + private func countryData(forRegionCode regionCode: String) -> CountryData { + let countryName = Locale.current.localizedString(forRegionCode: regionCode) ?? "world" + let phoneCode = phoneNumberKit.countryCode(for: regionCode) ?? 0 + + return CountryData( + regionCode: regionCode, + phoneCode: "+\(phoneCode)", + name: countryName + ) + } + + private func openSettings() { + guard let settingsUrl = URL(string: UIApplication.openSettingsURLString) else { + return + } + + if UIApplication.shared.canOpenURL(settingsUrl) { + UIApplication.shared.open(settingsUrl) + } + } + + private func fetchContacts() { + DispatchQueue.global(qos: .userInitiated).async { [weak self] in + let request = CNContactFetchRequest( + keysToFetch: [ + CNContactGivenNameKey, + CNContactFamilyNameKey, + CNContactPhoneNumbersKey, + CNContactImageDataKey, + ] as [CNKeyDescriptor] + ) + request.sortOrder = CNContactSortOrder.givenName + + var contacts: [Recipient] = [] + + do { + try self?.contactStore.enumerateContacts(with: request) { (cnContact, stop) in + let name = "\(cnContact.givenName) \(cnContact.familyName)".trimmingCharacters(in: .whitespaces) + let imageData = cnContact.imageData + + // Phone + let phoneNumber = cnContact.phoneNumbers.first?.value.stringValue ?? "" + + // Only add contacts with a name AND a phone number + if !name.isEmpty && phoneNumber.hasPrefix("+") { + let contact = Recipient(name: name, phoneNumber: phoneNumber, imageData: imageData) + contacts.append(contact) + } + } + + DispatchQueue.main.async { + self?.contacts = contacts + self?.contactsMapping = Dictionary(grouping: contacts, by: { $0.phoneNumber! }) + } + } catch { + print("Failed to fetch contacts: \(error)") + } + } + } + + private func initiateTransfer() { + self.recipient = nil + self.amount = "0" + self.outsideExecution = nil + self.outsideExecutionSignature = nil + } + + private func dismissTransfer() { + self.sendingStatus = .none + } + + private func initiateRequest() { + self.amount = "0" + } + + private func initiateOnramp() { + self.amount = "0" + self.onRampQuoteId = nil + self.onRampTotalUsd = nil + self.estSubtotalUsd = 0 + self.stripeRedirectUrl = nil + } +} diff --git a/app/App/Model/NavigationModel.swift b/app/App/Models/Navigation.swift similarity index 79% rename from app/App/Model/NavigationModel.swift rename to app/App/Models/Navigation.swift index ecfe7b2d..10035d63 100644 --- a/app/App/Model/NavigationModel.swift +++ b/app/App/Models/Navigation.swift @@ -35,11 +35,3 @@ enum Tab: Int, CaseIterable{ } } } - -class NavigationModel: ObservableObject { - @Published var selectedTab: Tab = Tab.payments - - func openTab(_ tab: Tab) { - self.selectedTab = tab - } -} diff --git a/app/App/Models/OutsideExecution.swift b/app/App/Models/OutsideExecution.swift new file mode 100644 index 00000000..e10f5997 --- /dev/null +++ b/app/App/Models/OutsideExecution.swift @@ -0,0 +1,61 @@ +// +// OutsideExecution.swift +// Vault +// +// Created by Charles Lanier on 14/06/2024. +// + +import Foundation +import Starknet + +public class OutsideExecution { + + static let OUTSIDE_EXECUTION_TYPE_SELECTOR = Felt(fromHex: "0x312b56c05a7965066ddbda31c016d8d05afc305071c0ca3cdc2192c3c2f1f0f")! + static let CALL_TYPE_SELECTOR = Felt(fromHex: "0x3635c7f2a7ba93844c0d064e18e487f35ab90f7c39d00f186a781fc3f0c2ca9")! + static let STARKNET_MESSAGE = Felt.fromShortString("StarkNet Message")! + static let SN_MAIN_DOMAIN_HASH = Felt(fromHex: "0x9951e1d5316915dc4436acce0a48afd57bb4b6c501687c151e869ab878a2ac")! + static let SN_SEPOLIA_DOMAIN_HASH = Felt(fromHex: "0x2534050c42890f9cad3cf470a8b54a39c4d283a246dfceb486a8755e44a91df")! + static let ANY_CALLER = Felt.fromShortString("ANY_CALLER")! + static let EXECUTE_AFTER = Felt.zero + static let EXECUTE_BEFORE = Felt(clamping: 999_999_999_999) + + var nonce: Felt + var calls: [StarknetCall] + + var calldata: [Felt] { + [Self.ANY_CALLER, self.nonce, Self.EXECUTE_AFTER, Self.EXECUTE_BEFORE, Felt(clamping: self.calls.count)] + + self.calls.flatMap { [$0.contractAddress, $0.entrypoint, Felt(clamping: $0.calldata.count)] + $0.calldata } + } + + init(calls: [StarknetCall]) { + self.calls = calls + self.nonce = Felt(clamping: Int64(Date().timeIntervalSince1970)) + } + + func getMessageHash(forSigner address: Felt) -> Felt { + let callsHash = StarknetPoseidon.poseidonHash(calls.map { call -> Felt in + return StarknetPoseidon.poseidonHash([ + Self.CALL_TYPE_SELECTOR, + call.contractAddress, + call.entrypoint, + StarknetPoseidon.poseidonHash(call.calldata), + ]) + }) + + let outsideExecutionHash = StarknetPoseidon.poseidonHash([ + Self.OUTSIDE_EXECUTION_TYPE_SELECTOR, + Self.ANY_CALLER, + self.nonce, + Self.EXECUTE_AFTER, + Self.EXECUTE_BEFORE, + callsHash, + ]) + + return StarknetPoseidon.poseidonHash([ + Self.STARKNET_MESSAGE, + Self.SN_SEPOLIA_DOMAIN_HASH, + address, + outsideExecutionHash, + ]) + } +} diff --git a/app/App/Models/Pagination/PaginationModel.swift b/app/App/Models/Pagination/PaginationModel.swift new file mode 100644 index 00000000..5f73d6bd --- /dev/null +++ b/app/App/Models/Pagination/PaginationModel.swift @@ -0,0 +1,210 @@ +// +// PaginationModel.swift +// Vault +// +// Created by Charles Lanier on 20/06/2024. +// + +import Foundation + +public enum PaginationState: Equatable { + case loading + case loaded + case error(error: Error) + + public static func == (lhs: PaginationState, rhs: PaginationState) -> Bool { + switch (lhs, rhs) { + case (.loading, .loading): + return true + case (.loaded, .loaded): + return true + case (.error, .error): + return true // we don't need associated value equality to be verified here + default: + return false + } + } +} + +public final class PaginationModel: ObservableObject { + + public typealias Item = TPageable.TPage.Item + + @Published private(set) var source: TPageable? + + private let pageSize: Int + private let threshold: Int + private var state: PaginationState = .loaded + + private var firstPageInfo: PageInfo = .default + private var lastPageInfo: PageInfo? = nil + + // polling + private var pollingTimer: Timer? + private var pollingAction: (() async -> Void)? + + init(threshold: Int, pageSize: Int) { + self.threshold = threshold + self.pageSize = pageSize + self.source = source + } + + deinit { + self.stopPolling() + } + + private var currentTask: Task? { + willSet { + if let task = currentTask { + if task.isCancelled { return } + task.cancel() + // Setting a new task cancelling the current task + } + } + } + + private var currentPollingTask: Task? { + willSet { + if let task = currentPollingTask { + if task.isCancelled { return } + task.cancel() + // Setting a new task cancelling the current task + } + } + } + + private var canLoadMorePages: Bool { lastPageInfo?.hasNext ?? false } + + public func start(withSource source: TPageable) { + // prevent double start + if self.source != nil { return } + + self.source = source + + // start polling + self.pollingAction = { + guard let source = self.source else { return } + + do { + let isFirstFetch = self.lastPageInfo == nil + let previousPage = try await source.loadPreviousPage(pageInfo: self.firstPageInfo, pageSize: nil) + + // task has been cancelled + if Task.isCancelled { return } + + // do nothing if no new items have been found + if previousPage.items.count == 0 { return } + + self.firstPageInfo = previousPage.info + + // set this page as the last one if it's the first time data is fetched + if isFirstFetch { + self.lastPageInfo = previousPage.info + } + + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + // no need to reverse data if we've fetched for the first time + self.source?.addPreviousItems(items: isFirstFetch ? previousPage.items : previousPage.items.reversed()) + } + } catch { + // Publish our error to SwiftUI + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + self.state = .error(error: error) + } + } + } + + // first execution + self.currentPollingTask = Task { + await self.pollingAction?() + } + + self.pollingTimer = Timer.scheduledTimer( + withTimeInterval: 3.0, // 3s + repeats: true + ) { _ in + self.currentPollingTask = Task { + await self.pollingAction?() + } + } + } + + public func loadNext() { + self.state = .loading + self.currentTask = Task { + await loadMoreItems() + } + } + + public func onItemAppear(_ item: Item) { + guard let source = self.source else { return } + + // (1) appeared: No more pages + if !self.canLoadMorePages { + return + } + + // (2) appeared: Already loading + if self.state == .loading { + return + } + + // (3) No index + guard let index = source.items.firstIndex(where: { $0.id == item.id }) else { + return + } + + // (4) appeared: Threshold not reached + let thresholdIndex = source.items.index(source.items.endIndex, offsetBy: -threshold) + if index != thresholdIndex { + return + } + + // (5) appeared: Load next page + self.loadNext() + } + + func loadMoreItems() async { + guard + let source = self.source, + let lastPageInfo = self.lastPageInfo + else { return } + + do { + // (1) Ask the source for a page + let nextPage = try await source.loadNextPage(pageInfo: lastPageInfo, pageSize: self.pageSize) + + // (2) Task has been cancelled + if Task.isCancelled { return } + + // (3) Append the items to the existing set of items + self.lastPageInfo = nextPage.info + + // (4) Publish our changes to SwiftUI by setting our items and state + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + self.source?.addItems(items: nextPage.items) + self.state = .loaded + } + } catch { + + // (5) Publish our error to SwiftUI + DispatchQueue.main.async { [weak self] in + guard let self = self else { return } + self.state = .error(error: error) + } + } + } + + private func stopPolling() { + pollingTimer?.invalidate() + pollingTimer = nil + pollingAction = nil + } + + func pushOptimisticUpdate(items: [Item]) { + self.source?.addPendingItems(items: items) + } +} diff --git a/app/App/Models/Pagination/Sources/Transactions.swift b/app/App/Models/Pagination/Sources/Transactions.swift new file mode 100644 index 00000000..684c7644 --- /dev/null +++ b/app/App/Models/Pagination/Sources/Transactions.swift @@ -0,0 +1,120 @@ +// +// Transactions.swift +// Vault +// +// Created by Charles Lanier on 21/06/2024. +// + +import Foundation + +class TransactionsPage: Page { + + typealias Item = Transaction + + var info: PageInfo + var items: [Item] + + init(info: PageInfo, items: [Item]) { + self.info = info + self.items = items + } +} + +struct TransactionHistory: PageableSource { + + typealias Item = Transaction + + var transactions: [Item] = [] + var groupedTransactions: [Date: [Item]] = [:] + var groupedPendingTransactions: [Date: [Item]] = [:] + var days: [Date] = [] + + var items: [Item] { transactions } + + private let address: String + + init(address: String) { + self.address = address + } + + public func loadNextPage(pageInfo: PageInfo, pageSize: Int?) async throws -> TransactionsPage { + return try await self.loadPage(request: GetTransactionsHistory(address: self.address, first: pageSize, after: pageInfo.endCursor)) + } + + public func loadPreviousPage(pageInfo: PageInfo, pageSize: Int?) async throws -> TransactionsPage { + return try await self.loadPage(request: GetTransactionsHistory(address: self.address, first: pageSize, before: pageInfo.startCursor)) + } + + private func loadPage(request: GetTransactionsHistory) async throws -> TransactionsPage { + return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + VaultService.shared.send(request) { result in + switch result { + case .success(let response): + let pageInfo = PageInfo(hasNext: response.hasNext, startCursor: response.startCursor, endCursor: response.endCursor) + let transactions = response.items.map { Item(address: self.address, transaction: $0) } + + continuation.resume(returning: TransactionsPage(info: pageInfo, items: transactions)) + + case .failure(let error): + // TODO: Handle error + continuation.resume(throwing: error) + } + } + } + } + + public mutating func addItems(items: [Transaction]) { + self.transactions += items + + items.forEach { item in + let day = Calendar.current.startOfDay(for: item.date) + + if self.groupedTransactions[day] == nil { + self.groupedTransactions[day] = [item] + } else { + self.groupedTransactions[day]?.append(item) + } + } + self.updateDays() + } + + public mutating func addPreviousItems(items: [Transaction]) { + self.transactions = items + self.transactions + + items.reversed().forEach { item in + let day = Calendar.current.startOfDay(for: item.date) + + // remove from pending if needed + self.groupedPendingTransactions[day]?.removeAll { pendingItem in + return pendingItem.id == item.id + } + + if self.groupedTransactions[day] == nil { + self.groupedTransactions[day] = [item] + } else { + self.groupedTransactions[day]?.insert(item, at: 0) + } + } + self.updateDays() + } + + public mutating func addPendingItems(items: [Transaction]) { + items.reversed().forEach { item in + let day = Calendar.current.startOfDay(for: item.date) + + if self.groupedPendingTransactions[day] == nil { + self.groupedPendingTransactions[day] = [item] + } else { + self.groupedPendingTransactions[day]?.insert(item, at: 0) + } + } + self.updateDays() + } + + public mutating func updateDays() { + let confirmedDays = self.groupedTransactions.keys + let pendingDays = self.groupedPendingTransactions.keys + + self.days = Array(Set(confirmedDays).union(pendingDays)).sorted(by: >) + } +} diff --git a/app/App/Models/Pagination/Utils/Page.swift b/app/App/Models/Pagination/Utils/Page.swift new file mode 100644 index 00000000..c97d4e61 --- /dev/null +++ b/app/App/Models/Pagination/Utils/Page.swift @@ -0,0 +1,16 @@ +// +// Page.swift +// Vault +// +// Created by Charles Lanier on 21/06/2024. +// + +import Foundation + +public protocol Page { + + associatedtype Item: Identifiable + + var info: PageInfo { get set } + var items: [Item] { get set } +} diff --git a/app/App/Models/Pagination/Utils/PageInfo.swift b/app/App/Models/Pagination/Utils/PageInfo.swift new file mode 100644 index 00000000..449a551f --- /dev/null +++ b/app/App/Models/Pagination/Utils/PageInfo.swift @@ -0,0 +1,16 @@ +// +// PageInfo.swift +// Vault +// +// Created by Charles Lanier on 20/06/2024. +// + +import Foundation + +public struct PageInfo { + let hasNext: Bool + let startCursor: String? + let endCursor: String? + + public static let `default`: Self = Self(hasNext: true, startCursor: nil, endCursor: nil) +} diff --git a/app/App/Models/Pagination/Utils/PageableSource.swift b/app/App/Models/Pagination/Utils/PageableSource.swift new file mode 100644 index 00000000..fc787513 --- /dev/null +++ b/app/App/Models/Pagination/Utils/PageableSource.swift @@ -0,0 +1,22 @@ +// +// PageableSource.swift +// Vault +// +// Created by Charles Lanier on 21/06/2024. +// + +import Foundation + +public protocol PageableSource { + + associatedtype TPage: Page + + var items: [TPage.Item] { get } + + func loadNextPage(pageInfo: PageInfo, pageSize: Int?) async throws -> TPage + func loadPreviousPage(pageInfo: PageInfo, pageSize: Int?) async throws -> TPage + + mutating func addItems(items: [TPage.Item]) + mutating func addPreviousItems(items: [TPage.Item]) + mutating func addPendingItems(items: [TPage.Item]) +} diff --git a/app/App/Navigation/ContentView.swift b/app/App/Navigation/ContentView.swift index 2860f02d..b695d084 100644 --- a/app/App/Navigation/ContentView.swift +++ b/app/App/Navigation/ContentView.swift @@ -9,9 +9,9 @@ import SwiftUI struct ContentView: View { - @StateObject private var registrationModel: RegistrationModel - @StateObject private var settingsModel = SettingsModel() - @StateObject private var navigationModel = NavigationModel() + @EnvironmentObject var model: Model + + @State private var selectedTab: Tab = Tab.payments init() { let navBarAppearance = UINavigationBarAppearance() @@ -29,52 +29,59 @@ struct ContentView: View { tabBarAppearance.stackedLayoutAppearance.selected.titleTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor(Color.accentColor)] UITabBar.appearance().standardAppearance = tabBarAppearance - - // init vault API models - - let vaultService = VaultService() - - self._registrationModel = StateObject(wrappedValue: RegistrationModel(vaultService: vaultService)) } var body: some View { - if settingsModel.isOnboarded { + if self.model.isOnboarded { ZStack(alignment: .bottom) { - TabView(selection: $navigationModel.selectedTab) { + TabView(selection: $selectedTab) { NavigationStack { - HomeView().edgesIgnoringSafeArea(.bottom) + HomeView() } .tag(Tab.payments) - .toolbarBackground(.hidden, for: .tabBar) NavigationStack { - BudgetView().edgesIgnoringSafeArea(.bottom) + BudgetView() } .tag(Tab.budget) NavigationStack { - EarnView().edgesIgnoringSafeArea(.bottom) + EarnView() } .tag(Tab.earn) } - .toolbarBackground(.hidden, for: .navigationBar) - .environmentObject(settingsModel) - .preferredColorScheme(.dark) + .edgesIgnoringSafeArea(.bottom) + + CustomTabbar(selectedTab: $selectedTab) + } + .onOpenURL { incomingURL in + #if DEBUG + print("App was opened via URL: \(incomingURL)") + #endif - CustomTabbar(selectedTab: $navigationModel.selectedTab) + self.model.handleDeepLink(incomingURL) + } + .addSendingConfirmation(isPresented: self.$model.showRequestingConfirmation) { + self.model.sendingStatus = .none } - .ignoresSafeArea(.keyboard, edges: .bottom) } else { NavigationStack { OnboardingView() } - .environmentObject(settingsModel) - .environmentObject(registrationModel) - .preferredColorScheme(.dark) } } } #Preview { - ContentView() + struct ContentViewPreviews: View { + + @StateObject var model = Model() + + var body: some View { + ContentView() + .environmentObject(self.model) + } + } + + return ContentViewPreviews() } diff --git a/app/App/Navigation/Core/BudgetView.swift b/app/App/Navigation/Core/Budget/BudgetView.swift similarity index 71% rename from app/App/Navigation/Core/BudgetView.swift rename to app/App/Navigation/Core/Budget/BudgetView.swift index 658ae463..b925d095 100644 --- a/app/App/Navigation/Core/BudgetView.swift +++ b/app/App/Navigation/Core/Budget/BudgetView.swift @@ -37,16 +37,28 @@ struct BudgetView: View { .shadow(radius: 10) VStack(alignment: .leading, spacing: 8) { - Text("Set up your budget").textTheme(.headlineMedium) - Text("Gain peace of mind by organizing your spending.").textTheme(.bodySecondary) + Text("Set up your budget") + .textTheme(.headlineMedium) + + Text("Gain peace of mind by organizing your spending.") + .textTheme(.bodySecondary) + .multilineTextAlignment(.leading) } Spacer(minLength: 0) } - .frame(minWidth: 0, maxWidth: .infinity) + .foregroundStyle(.neutral1) .padding(16) + .background( + LinearGradient( + gradient: Constants.gradient1, + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + .clipShape(RoundedRectangle(cornerRadius: 16)) } - .buttonStyle(GradientButtonStyle()) + .buttonStyle(PlainButtonStyle()) Spacer() } diff --git a/app/App/Navigation/Core/Cards/HistoricalGraph.swift b/app/App/Navigation/Core/Budget/HistoricalGraph.swift similarity index 100% rename from app/App/Navigation/Core/Cards/HistoricalGraph.swift rename to app/App/Navigation/Core/Budget/HistoricalGraph.swift diff --git a/app/App/Navigation/Core/Cards/TransferRow.swift b/app/App/Navigation/Core/Cards/TransferRow.swift deleted file mode 100644 index 1c01ba90..00000000 --- a/app/App/Navigation/Core/Cards/TransferRow.swift +++ /dev/null @@ -1,81 +0,0 @@ -// -// Home.swift -// Vault -// -// Created by Charles Lanier on 01/04/2024. -// - -import SwiftUI - -struct TransferRow: View { - @State private var transfer: Transfer - @State private var me: User - - init(transfer: Transfer, me: User) { - self.transfer = transfer - self.me = me - } - - var body: some View { - let isSpending = transfer.from.address == self.me.address - let displayedUser = isSpending ? transfer.to : transfer.from - - let dateFormatter = DateFormatter() - let _ = dateFormatter.dateFormat = "HH:mm" - let _ = dateFormatter.timeZone = TimeZone.current // Use the user's current timezone - let formattedDate = dateFormatter.string(from: transfer.date) - - HStack(spacing: 12) { - if let avatarUrl = displayedUser.avatarUrl { - AsyncImage( - url: URL(string: avatarUrl), - content: { image in - image - .resizable() - .aspectRatio(contentMode: .fill) - .frame(width: 42, height: 42).scaledToFit() - }, - placeholder: { - ProgressView() - } - ) - .clipShape(RoundedRectangle(cornerRadius: 99)) - } else { - Capsule() - .fill(.accent.opacity(0.5)) - .strokeBorder(.accent, lineWidth: 1) - .frame(width: 42, height: 42) - .overlay() { - Text(displayedUser.username.first?.description.uppercased() ?? "") - .font(.system(size: 18)) - .fontWeight(.semibold) - .foregroundStyle(.accent) - } - } - - VStack(alignment: .leading) { - Text(displayedUser.username).textTheme(.bodyPrimary) - - Spacer() - - Text("\(formattedDate)").textTheme(.subtitle) - } - .padding(EdgeInsets(top: 6, leading: 0, bottom: 6, trailing: 0)) - - Spacer() - - Text("\(isSpending ? "-" : "")$\(transfer.amount.toFixed())") - .if(!isSpending) { view in - view - .fontWeight(.semibold) - } - .textTheme(.bodySecondary) - .padding(EdgeInsets(top: 2, leading: 6, bottom: 2, trailing: 6)) - .if(!isSpending) { view in - view - .background(.accent) - .clipShape(RoundedRectangle(cornerRadius: 4)) - } - }.fixedSize(horizontal: false, vertical: true) - } -} diff --git a/app/App/Navigation/Core/EarnView.swift b/app/App/Navigation/Core/EarnView.swift index ade26906..cac8f7a7 100644 --- a/app/App/Navigation/Core/EarnView.swift +++ b/app/App/Navigation/Core/EarnView.swift @@ -13,13 +13,11 @@ struct EarnView: View { Text("Start Earning yield !").textTheme(.headlineLarge) Text("Coming soon").textTheme(.subtitle) } + .frame(maxWidth: .infinity, maxHeight: .infinity) .defaultBackground() } } #Preview { - ZStack { - Color.background1.ignoresSafeArea() - EarnView() - } + EarnView() } diff --git a/app/App/Navigation/Core/Cards/BalanceView.swift b/app/App/Navigation/Core/Home/BalanceView.swift similarity index 58% rename from app/App/Navigation/Core/Cards/BalanceView.swift rename to app/App/Navigation/Core/Home/BalanceView.swift index 7f7bc33a..3f9318a3 100644 --- a/app/App/Navigation/Core/Cards/BalanceView.swift +++ b/app/App/Navigation/Core/Home/BalanceView.swift @@ -8,18 +8,26 @@ import SwiftUI struct BalanceView: View { + + @Binding var balance: Amount? + var body: some View { + let fixedBalance = self.balance?.toFixed() ?? "0.00" + let splittedBalance = fixedBalance.components(separatedBy: ".") + let integerPart = splittedBalance[0] + let decimalPart = splittedBalance[1] + HStack(spacing: 4) { Text("$") .font(.custom("Sofia Pro", size: 32)) - .textTheme(.bodyPrimary) + .textTheme(.hero) HStack(alignment: .bottom, spacing: 0) { - Text("456.") + Text("\(integerPart).") .font(.custom("Sofia Pro", size: 64)) .textTheme(.hero) - Text("18") + Text("\(decimalPart)") .font(.custom("Sofia Pro", size: 36)) .foregroundStyle(.neutral2) .textTheme(.hero) @@ -30,5 +38,5 @@ struct BalanceView: View { } #Preview { - BalanceView() + BalanceView(balance: .constant(Amount.usdc(from: 456.18))).defaultBackground() } diff --git a/app/App/Navigation/Core/Home/HomeView.swift b/app/App/Navigation/Core/Home/HomeView.swift new file mode 100644 index 00000000..6434ea56 --- /dev/null +++ b/app/App/Navigation/Core/Home/HomeView.swift @@ -0,0 +1,215 @@ +// +// Home.swift +// Vault +// +// Created by Charles Lanier on 01/04/2024. +// + +import SwiftUI + +// PreferenceKey to store the scroll offset +struct ScrollOffsetKey: PreferenceKey { + typealias Value = CGFloat + static var defaultValue: CGFloat = 0 + + static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { + value += nextValue() + } +} + +struct HomeView: View { + + @EnvironmentObject private var model: Model + @EnvironmentObject private var txHistoryModel: PaginationModel + + @State private var scrollOffset: CGFloat = 0 + + var body: some View { + ScrollView { + + // MARK: Balance + + VStack(spacing: 48) { + HStack { + Spacer() + + VStack(spacing: 12) { + Text("Account Balance") + .foregroundStyle(.neutral2) + .textTheme(.bodyPrimary) + + BalanceView(balance: .constant(self.txHistoryModel.source?.items.first?.balance)) + } + + Spacer() + } + .padding(.top, 180) + .padding(.bottom, 58) + + ActionsView() + + HistoryView() + } + } + .onAppear { + self.txHistoryModel.start(withSource: TransactionHistory(address: self.model.address)) + } + .defaultBackground() + .navigationBarItems( + trailing: IconButton(size: .custom(IconButtonSize.medium.buttonSize, 20)) {} icon: { + Image("Gear") + .iconify() + } + ) + .removeNavigationBarBorder() + .fullScreenCover(isPresented: self.$model.showSendingView) { + SendingView() + } + .fullScreenCover(isPresented: self.$model.showRequestingView) { + RequestingView() + } + .fullScreenCover(isPresented: self.$model.showOnrampView) { + OnrampView() + } + .edgesIgnoringSafeArea(.all) + } + + private func formatSectionHeader(for date: Date) -> String { + let calendar = Calendar.current + + if calendar.isDateInToday(date) { + return "Today" + } else if calendar.isDateInYesterday(date) { + return "Yesterday" + } else { + let formatter = DateFormatter() + formatter.dateFormat = "EEEE d MMMM" // Day DD Month + return formatter.string(from: date) + } + } + + // MARK: - Actions View + + @ViewBuilder + func ActionsView() -> some View { + HStack { + Spacer(minLength: 24) + + IconButton(size: .large, priority: .primary) { + self.model.showSendingView = true + } icon: { + Image(systemName: "arrow.up") + .iconify() + .fontWeight(.semibold) + } + .withText("Send") + .frame(maxWidth: .infinity) + + Spacer() + + IconButton(size: .large) { + self.model.showRequestingView = true + } icon: { + Image(systemName: "arrow.down") + .iconify() + .fontWeight(.semibold) + } + .withText("Request") + .frame(maxWidth: .infinity) + + Spacer() + + IconButton(size: .large) { + self.model.showOnrampView = true + } icon: { + Image(systemName: "plus") + .iconify() + .fontWeight(.medium) + } + .withText("Add funds") + .frame(maxWidth: .infinity) + + Spacer(minLength: 24) + } + } + + // MARK: - History View + + @ViewBuilder + func HistoryView() -> some View { + LazyVStack(spacing: 48) { + if let txHistory = self.txHistoryModel.source { + ForEach(txHistory.days, id: \.self) { day in + VStack(alignment: .leading, spacing: 12) { + Text(self.formatSectionHeader(for: day).uppercased()) + .textTheme(.headlineSmall) + + // PENDING + + let pendingTransfers = txHistory.groupedPendingTransactions[day] ?? [] + + if !pendingTransfers.isEmpty { + VStack(spacing: 32) { + ForEach(pendingTransfers, id: \.self.id) { transfer in + + TransferRow(transfer: transfer) + } + .padding(16) + .background(.accent.opacity(0.2)) + .animatePlaceholder(isLoading: .constant(true)) + } + .background(.background2) + .clipShape(RoundedRectangle(cornerRadius: 16)) + } + + // CONFIRMED + + let confirmedTransfers = txHistory.groupedTransactions[day] ?? [] + + if !confirmedTransfers.isEmpty { + VStack(spacing: 32) { + ForEach(confirmedTransfers, id: \.self.id) { transfer in + + TransferRow(transfer: transfer) + .onAppear { + self.txHistoryModel.onItemAppear(transfer) + } + } + } + .padding(16) + .background(.background2) + .clipShape(RoundedRectangle(cornerRadius: 16)) + } + } + .padding(.horizontal, 16) + } + } + } + .padding(.top, 32) + .padding(.bottom, 120) + .background(.background1) + .onChange(of: self.model.pendingTransaction) { newValue in + if let pendingTransaction = newValue { + self.txHistoryModel.pushOptimisticUpdate(items: [pendingTransaction]) + } + } + } +} + +#Preview { + struct HomeViewPreviews: View { + + @StateObject var model = Model() + @StateObject private var txHistoryModel: PaginationModel = PaginationModel(threshold: 7, pageSize: 15) + + var body: some View { + NavigationStack { + HomeView() + .environmentObject(self.model) + .environmentObject(self.txHistoryModel) + } + } + } + + return HomeViewPreviews() +} diff --git a/app/App/Navigation/Core/Home/TransferRow.swift b/app/App/Navigation/Core/Home/TransferRow.swift new file mode 100644 index 00000000..65ee49e1 --- /dev/null +++ b/app/App/Navigation/Core/Home/TransferRow.swift @@ -0,0 +1,50 @@ +// +// Home.swift +// Vault +// +// Created by Charles Lanier on 01/04/2024. +// + +import SwiftUI + +struct TransferRow: View { + + @EnvironmentObject var model: Model + + @State private var transfer: Transaction + + init(transfer: Transaction) { + self.transfer = transfer + + print(transfer.to) + } + + var body: some View { + let displayedUser = self.transfer.isSending ? transfer.to : transfer.from + let displayedContact = displayedUser.phoneNumber == nil ? nil : self.model.contactsMapping[displayedUser.phoneNumber!]?.first + + let dateFormatter = DateFormatter() + let _ = dateFormatter.dateFormat = "HH:mm" + let _ = dateFormatter.timeZone = TimeZone.current // Use the user's current timezone + let formattedDate = dateFormatter.string(from: transfer.date) + + HStack(alignment: .center, spacing: 12) { + Avatar(salt: displayedUser.address, name: displayedUser.nickname, size: 46, data: displayedContact?.imageData) + + VStack(alignment: .leading, spacing: 4) { + Text(displayedUser.nickname ?? "UNKNOWN").textTheme(.bodyPrimary) + + Text("\(formattedDate)").textTheme(.subtitle) + } + + Spacer() + + Text("\(self.transfer.isSending ? "-" : "")$\(self.transfer.amount.toFixed())") + .fontWeight(self.transfer.isSending ? .regular : .semibold) + .textTheme(.bodyPrimary) + .padding(EdgeInsets(top: 2, leading: 6, bottom: 2, trailing: 6)) + .background(self.transfer.isSending ? .transparent : .accent) + .clipShape(RoundedRectangle(cornerRadius: 4)) + }.fixedSize(horizontal: false, vertical: true) + } +} diff --git a/app/App/Navigation/Core/HomeView.swift b/app/App/Navigation/Core/HomeView.swift deleted file mode 100644 index f4deadc4..00000000 --- a/app/App/Navigation/Core/HomeView.swift +++ /dev/null @@ -1,250 +0,0 @@ -// -// Home.swift -// Vault -// -// Created by Charles Lanier on 01/04/2024. -// - -import SwiftUI - -class User { - let address: String - let username: String - let avatarUrl: String? - - init(address: String, username: String, avatarUrl: String? = nil) { - self.address = address - self.username = username - self.avatarUrl = avatarUrl - } -} - -class Transfer: Identifiable { - let from: User - let to: User - let amount: USDCAmount - let date: Date - - init(from: User, to: User, amount: USDCAmount, timestamp: Double) { - self.from = from - self.to = to - self.amount = amount - self.date = Date(timeIntervalSince1970: timestamp) - } -} - -class History { - let transfers: [Transfer] - - var groupedTransfers: [Date: [Transfer]] { - get { - return Dictionary(grouping: self.transfers) { (transfer) -> Date in - Calendar.current.startOfDay(for: transfer.date) - } - } - } - - init(transfers: [Transfer]) { - self.transfers = transfers - } -} - -let users: [String: User] = [ - "me": User( - address: "0xdead", - username: "Bobby" - ), - "sbf": User( - address: "0x1", - username: "SBF", - avatarUrl: "https://fortune.com/img-assets/wp-content/uploads/2022/11/SBF-1.jpg" - ), - "apple": User( - address: "0x2", - username: "Apple", - avatarUrl: "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcRIHoznvT47BiebsgSlaiey1FKjGR8xZru6gROHvntwI3QSA2I7T08Ys7g1by9_iBw-ekI&usqp=CAU" - ), - "vitalik": User( - address: "0x3", - username: "Vitalik", - avatarUrl: "https://images.moneycontrol.com/static-mcnews/2021/05/vitalik-Buterin-ethereum.jpg?impolicy=website&width=1600&height=900" - ), - "satoshi": User(address: "0x4", username: "Satoshi N"), - "alex": User( - address: "0x5", - username: "Alex", - avatarUrl: "https://www.cryptotimes.io/wp-content/uploads/2024/02/Matter_Labs_co-founder_and_CEO_Alex_Gluchowski_proposed_an_Ethereum_court_system.jpg.webp" - ), - "abdel": User( - address: "0x6", - username: "Abdel.stark", - avatarUrl: "https://miro.medium.com/v2/resize:fit:1400/1*BTiOG6PF5d9ToTAZqlIjuw.jpeg" - ), -] - -struct HomeView: View { - - @State private var showingAddFundsWebView = false - - private var me: User { - get { - return users["me"]! - } - } - - let history: History - - init() { - self.history = History(transfers: [ - Transfer(from: users["me"]!, to: users["sbf"]!, amount: USDCAmount(1_604_568_230_000), timestamp: 1712199068), - Transfer(from: users["me"]!, to: users["apple"]!, amount: USDCAmount(4_249_990_000), timestamp: 1711924459), - Transfer(from: users["vitalik"]!, to: users["me"]!, amount: USDCAmount(70_000_000_000), timestamp: 1711878225), - - Transfer(from: users["alex"]!, to: users["me"]!, amount: USDCAmount(1_000_000), timestamp: 1711847328), - Transfer(from: users["me"]!, to: users["satoshi"]!, amount: USDCAmount(32_570_000), timestamp: 1712000648), - - Transfer(from: users["abdel"]!, to: users["me"]!, amount: USDCAmount(10_000), timestamp: 1711828026), - ]) - } - - var body: some View { - VStack(alignment: .center) { - - // MARK: Header - - HStack { - Spacer() - - Capsule() - .fill(.accent.opacity(0.5)) - .strokeBorder(.accent, lineWidth: 1) - .frame(width: 42, height: 42) - .overlay() { - Text("C") - .font(.system(size: 18)) - .fontWeight(.semibold) - .foregroundStyle(.accent) - } - } - - List { - - VStack { - - // MARK: Balance - - VStack(spacing: 12) { - Text("Account Balance") - .foregroundStyle(.neutral2) - .textTheme(.bodyPrimary) - - BalanceView() - } - .padding(EdgeInsets(top: 32, leading: 0, bottom: 42, trailing: 0)) - - // MARK: Transfers - - HStack { - Spacer(minLength: 16) - - IconButton("Send", iconName: "ArrowUp") { - // TODO: Handle sending - } - .frame(maxWidth: .infinity) - - Spacer(minLength: 8) - - IconButton("Request", iconName: "ArrowDown") { - // TODO: Handle sending - } - .frame(maxWidth: .infinity) - - Spacer(minLength: 8) - - IconButton("Add funds", iconName: "Plus") { - self.showingAddFundsWebView = true - } - .frame(maxWidth: .infinity) - .fullScreenCover(isPresented: $showingAddFundsWebView) { - WebView(url: URL(string: "https://app.fun.xyz")!) - } - - Spacer(minLength: 16) - } - } - .padding(.bottom, 24) - .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0)) - .listRowBackground(EmptyView()) - .listRowSeparator(.hidden) - - // MARK: History - - ForEach(self.history.groupedTransfers.keys.sorted(by: >), id: \.self) { day in - Section { - ForEach(0.. String { - let calendar = Calendar.current - - if calendar.isDateInToday(date) { - return "Today" - } else if calendar.isDateInYesterday(date) { - return "Yesterday" - } else { - let formatter = DateFormatter() - formatter.dateFormat = "EEEE d MMMM" // Day DD Month - return formatter.string(from: date) - } - } -} - -#Preview { - HomeView() -} diff --git a/app/App/Navigation/Core/Onramp/OnrampAmountView.swift b/app/App/Navigation/Core/Onramp/OnrampAmountView.swift new file mode 100644 index 00000000..6160081c --- /dev/null +++ b/app/App/Navigation/Core/Onramp/OnrampAmountView.swift @@ -0,0 +1,87 @@ +// +// OnrampAmountView.swift +// Vault +// +// Created by Charles Lanier on 01/07/2024. +// + +import SwiftUI + +struct OnrampAmountView: View { + + @Environment(\.dismiss) var dismiss + + @EnvironmentObject private var model: Model + + @State private var presentingNextView = false + + private var hexAmount: String { + Amount.usdc(from: self.model.parsedAmount)?.value.toHex() ?? "0x0" + } + + var body: some View { + VStack { + Spacer() + + VStack(spacing: 8) { + FancyAmount(amount: self.$model.amount) + + VStack { + if + let totalUsd = self.model.onRampTotalUsd, + let totalUsdDouble = Double(totalUsd) + { + Text("$\(String(format: "%.2f", totalUsdDouble - self.model.parsedAmount)) Fees").textTheme(.headlineSubtitle) + } + } + .frame(minWidth: 100, minHeight: 24) + .redacted(reason: self.model.isLoading ? .placeholder : []) + .animatePlaceholder(isLoading: self.$model.isLoading) + } + + Spacer() + + VStack(spacing: 32) { + PrimaryButton("Next", disabled: self.model.onRampQuoteId == nil) { + self.presentingNextView = true + } + + NumPad(amount: self.$model.amount) + } + } + .padding(EdgeInsets(top: 0, leading: 16, bottom: 8, trailing: 16)) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .defaultBackground() + .navigationBarItems( + leading: IconButton { + self.dismiss() + } icon: { + Image(systemName: "xmark") + .iconify() + .fontWeight(.bold) + } + ) + .removeNavigationBarBorder() + .navigationBarBackButtonHidden(true) + .navigationBarTitleDisplayMode(.inline) + .navigationTitle("Select Amount") + .navigationDestination(isPresented: $presentingNextView) { + OnrampStripeView() + } + .onChange(of: self.model.amount) { + self.model.getOnrampQuote() + } + } +} + +#if DEBUG +struct OnrampAmountViewPreviews : PreviewProvider { + + @StateObject static var model = Model() + + static var previews: some View { + OnrampAmountView() + .environmentObject(self.model) + } +} +#endif diff --git a/app/App/Navigation/Core/Onramp/OnrampStripeView.swift b/app/App/Navigation/Core/Onramp/OnrampStripeView.swift new file mode 100644 index 00000000..cee59952 --- /dev/null +++ b/app/App/Navigation/Core/Onramp/OnrampStripeView.swift @@ -0,0 +1,130 @@ +// +// OnrampStripeView.swift +// Vault +// +// Created by Charles Lanier on 01/07/2024. +// + +import SwiftUI + +struct OnrampStripeView: View { + + @Environment(\.dismiss) var dismiss + + @EnvironmentObject private var model: Model + + @State private var presentingNextView = false + + var body: some View { + ZStack { + if let stripeRedirectUrl = self.model.stripeRedirectUrl { + WebView(url: stripeRedirectUrl) + } else { + VStack { + Spacer() + + Spacer().frame(height: 32) + + HStack { + Spacer(minLength: 24) + + HStack { + Image(.logo) + .iconify() + .frame(width: 64) + .foregroundStyle(.background1) + .padding(.top, 8) + } + .frame(width: 100, height: 100) + .background(.neutral1) + .clipShape(Circle()) + + Line() + .stroke(style: StrokeStyle(lineWidth: 1, dash: [5])) + .frame(height: 1) + + HStack { + Image(.fun) + .iconify() + .frame(width: 64) + .foregroundStyle(.background1) + .padding(.bottom, 8) + } + .frame(width: 100, height: 100) + .background(.neutral1) + .clipShape(Circle()) + + Spacer(minLength: 24) + } + + Spacer().frame(height: 64) + + VStack(spacing: 16) { + Text("Vault x Fun").textTheme(.headlineLarge) + + Text("Please wait a few moments on this screen") + .textTheme(.headlineSubtitle) + .multilineTextAlignment(.center) + } + + Spacer() + + HStack(alignment: .center, spacing: 4) { + ZStack { + Image(systemName: "shield.fill").foregroundStyle(.accent) + .font(.system(size: 22)) + + Image(systemName: "lock.fill") + .padding(.bottom, 2) + .font(.system(size: 12)) + .fontWeight(.bold) + .foregroundStyle(.neutral1) + } + + Text("Payment secured by").textTheme(.bodyPrimary) + + Text("Stripe") + .textTheme(.headlineMedium) + .padding(.top, 4) + + Spacer().frame(width: 12) + } + } + .padding(EdgeInsets(top: 0, leading: 16, bottom: 8, trailing: 16)) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .onAppear { + Task { + try await Task.sleep(for: .seconds(1)) + self.model.createOnrampCheckout() + } + } + .navigationBarItems( + leading: IconButton { + self.dismiss() + } icon: { + Image(systemName: "chevron.left") + .iconify() + .fontWeight(.bold) + } + ) + .defaultBackground() + .removeNavigationBarBorder() + .navigationBarBackButtonHidden(true) + .navigationBarTitleDisplayMode(.inline) + .navigationTitle("Checkout") + } +} + +#if DEBUG +struct OnrampStripeViewPreviews : PreviewProvider { + + @StateObject static var model = Model() + + static var previews: some View { + OnrampStripeView() + .environmentObject(self.model) + } +} +#endif diff --git a/app/App/Navigation/Core/Onramp/OnrampView.swift b/app/App/Navigation/Core/Onramp/OnrampView.swift new file mode 100644 index 00000000..e36756d8 --- /dev/null +++ b/app/App/Navigation/Core/Onramp/OnrampView.swift @@ -0,0 +1,17 @@ +// +// OnrampView.swift +// Vault +// +// Created by Charles Lanier on 01/07/2024. +// + +import SwiftUI + +struct OnrampView: View { + + var body: some View { + NavigationStack { + OnrampAmountView() + } + } +} diff --git a/app/App/Navigation/Core/Requesting/RequestingAmountView.swift b/app/App/Navigation/Core/Requesting/RequestingAmountView.swift new file mode 100644 index 00000000..80b1edb5 --- /dev/null +++ b/app/App/Navigation/Core/Requesting/RequestingAmountView.swift @@ -0,0 +1,78 @@ +// +// RequestingAmountView.swift +// Vault +// +// Created by Charles Lanier on 25/06/2024. +// + +import SwiftUI + +struct RequestingAmountView: View { + + @Environment(\.dismiss) var dismiss + + @EnvironmentObject private var model: Model + + @State var isShareSheetPresented = false + + private var hexAmount: String { + Amount.usdc(from: self.model.parsedAmount)?.value.toHex() ?? "0x0" + } + + var body: some View { + VStack { + Spacer() + + FancyAmount(amount: self.$model.amount) + + Spacer() + + VStack(spacing: 32) { + PrimaryButton("Request", disabled: self.model.parsedAmount <= 0) { + self.isShareSheetPresented = true + } + + NumPad(amount: self.$model.amount) + } + } + .padding(EdgeInsets(top: 0, leading: 16, bottom: 8, trailing: 16)) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .defaultBackground() + .navigationBarItems( + leading: IconButton { + self.dismiss() + } icon: { + Image(systemName: "xmark") + .iconify() + .fontWeight(.bold) + } + ) + .removeNavigationBarBorder() + .navigationBarBackButtonHidden(true) + .navigationBarTitleDisplayMode(.inline) + .navigationTitle("Select Amount") + .sheet(isPresented: self.$isShareSheetPresented) { + ActivityView( + activityItems: [ + "Hello ! Please send me $\(self.model.amount) via vltfinance://request?amount=\(self.hexAmount)&recipientAddress=\(self.model.address)" + ], + isPresented: self.$isShareSheetPresented + ) + .ignoresSafeArea() + .presentationDragIndicator(.hidden) + .presentationDetents([.medium, .large]) + } + } +} + +#if DEBUG +struct RequestingAmountViewPreviews : PreviewProvider { + + @StateObject static var model = Model() + + static var previews: some View { + RequestingAmountView() + .environmentObject(self.model) + } +} +#endif diff --git a/app/App/Navigation/Core/Requesting/RequestingView.swift b/app/App/Navigation/Core/Requesting/RequestingView.swift new file mode 100644 index 00000000..54f3fd9f --- /dev/null +++ b/app/App/Navigation/Core/Requesting/RequestingView.swift @@ -0,0 +1,17 @@ +// +// RequestingView.swift +// Vault +// +// Created by Charles Lanier on 21/06/2024. +// + +import SwiftUI + +struct RequestingView: View { + + var body: some View { + NavigationStack { + RequestingAmountView() + } + } +} diff --git a/app/App/Navigation/Core/Sending/ContactRow.swift b/app/App/Navigation/Core/Sending/ContactRow.swift new file mode 100644 index 00000000..bdecffcd --- /dev/null +++ b/app/App/Navigation/Core/Sending/ContactRow.swift @@ -0,0 +1,42 @@ +// +// ContactRow.swift +// Vault +// +// Created by Charles Lanier on 01/04/2024. +// + +import SwiftUI + +struct ContactRow: View { + + let contact: Recipient + + var body: some View { + HStack(spacing: 12) { + Avatar(salt: self.contact.phoneNumber, name: self.contact.name, data: self.contact.imageData) + + VStack(alignment: .leading) { + Text(self.contact.name) + .textTheme(.bodyPrimary) + .lineLimit(1) + + Spacer() + + Text(self.contact.phoneNumber ?? "").textTheme(.subtitle) + } + .padding(.vertical, 6) + + Spacer() + } + .fixedSize(horizontal: false, vertical: true) + } +} + +#Preview { + VStack { + ContactRow(contact: Recipient(name: "Kenny McCormick", phoneNumber: "+33612345678")) + ContactRow(contact: Recipient(name: "Kenny McCormick But with a very long name", phoneNumber: "+33612345678")) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .defaultBackground() +} diff --git a/app/App/Navigation/Core/Sending/NewRecipientView.swift b/app/App/Navigation/Core/Sending/NewRecipientView.swift new file mode 100644 index 00000000..6d09bedd --- /dev/null +++ b/app/App/Navigation/Core/Sending/NewRecipientView.swift @@ -0,0 +1,80 @@ +// +// NewRecipientView.swift +// Vault +// +// Created by Charles Lanier on 21/05/2024. +// + +import SwiftUI +import PhoneNumberKit + +struct NewRecipientView: View { + + @EnvironmentObject private var model: Model + + @Environment(\.dismiss) var dismiss + + @State private var name = "" + @State private var phoneNumber = "" + @State private var parsedPhoneNumber: PhoneNumber? + + private var parsedName: String { + self.name.trimmingCharacters(in: .whitespacesAndNewlines) + } + + var body: some View { + VStack(alignment: .center, spacing: 32) { + VStack(spacing: 16) { + TextInput("Name", text: $name, shouldFocusOnAppear: true) + + PhoneInput(phoneNumber: $phoneNumber, parsedPhoneNumber: $parsedPhoneNumber) + } + + Spacer() + + PrimaryButton("Add", disabled: self.parsedPhoneNumber == nil || self.parsedName.isEmpty) { + guard let parsedPhoneNumber = self.parsedPhoneNumber else { + fatalError("Should be disabled") + } + + self.model.addContact( + name: self.parsedName, + phoneNumber: parsedPhoneNumber.rawString() + ) { contact in + // TODO: handle this new contact + print(contact) + self.dismiss() + } + } + } + .padding(EdgeInsets(top: 32, leading: 16, bottom: 16, trailing: 16)) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .defaultBackground() + .navigationBarBackButtonHidden(true) + .navigationTitle("New Recipient") + .navigationBarItems( + leading: IconButton { + self.dismiss() + } icon: { + Image(systemName: "chevron.left") + .iconify() + .fontWeight(.bold) + .padding(.trailing, 2) + } + ) + } +} + +#if DEBUG +struct NewRecipientViewPreviews : PreviewProvider { + + @StateObject static var model = Model() + + static var previews: some View { + NavigationStack { + NewRecipientView() + .environmentObject(self.model) + } + } +} +#endif diff --git a/app/App/Navigation/Core/Sending/SendingAmountView.swift b/app/App/Navigation/Core/Sending/SendingAmountView.swift new file mode 100644 index 00000000..bbf5f8fd --- /dev/null +++ b/app/App/Navigation/Core/Sending/SendingAmountView.swift @@ -0,0 +1,72 @@ +// +// SendingAmountView.swift +// Vault +// +// Created by Charles Lanier on 17/05/2024. +// + +import SwiftUI + +@MainActor +struct SendingAmountView: View { + + @Environment(\.dismiss) var dismiss + + @EnvironmentObject private var model: Model + + var body: some View { + VStack { + Spacer() + + FancyAmount(amount: self.$model.amount) + + Spacer() + + VStack(spacing: 32) { + PrimaryButton("Send", disabled: self.model.parsedAmount <= 0) { + self.model.showSendingConfirmation = true + } + + NumPad(amount: self.$model.amount) + } + } + .padding(EdgeInsets(top: 0, leading: 16, bottom: 8, trailing: 16)) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .defaultBackground() + .navigationBarItems( + leading: IconButton { + self.dismiss() + } icon: { + Image(systemName: "chevron.left") + .iconify() + .fontWeight(.bold) + .padding(.trailing, 2) + } + ) + .removeNavigationBarBorder() + .navigationBarBackButtonHidden(true) + .navigationTitle("Select Amount") + .addSendingConfirmation(isPresented: self.$model.showSendingConfirmation) { + self.model.showSendingView = false + } + } +} + +#if DEBUG +struct SendingAmountViewPreviews : PreviewProvider { + + @StateObject static var model = { + let model = Model() + + model.setRecipient(Recipient(name: "Very Long Bobby Name", phoneNumber: "+33612345678")) + model.sendingStatus = .none + + return model + }() + + static var previews: some View { + SendingAmountView() + .environmentObject(self.model) + } +} +#endif diff --git a/app/App/Navigation/Core/Sending/SendingRecipientView.swift b/app/App/Navigation/Core/Sending/SendingRecipientView.swift new file mode 100644 index 00000000..c6ea20b2 --- /dev/null +++ b/app/App/Navigation/Core/Sending/SendingRecipientView.swift @@ -0,0 +1,196 @@ +// +// SendingRecipientView.swift +// Vault +// +// Created by Charles Lanier on 20/05/2024. +// + +import SwiftUI + +struct SendingRecipientView: View { + + @Environment(\.dismiss) var dismiss + + @EnvironmentObject private var model: Model + + @State private var presentingNewRecipientView = false + @State private var presentingSendingAmountView = false + + var body: some View { + List() { + + switch self.model.contactsAuthorizationStatus { + case .notDetermined, .denied: + Section { + Button { + model.requestContactsAccess() + } label: { + HStack(alignment: .top, spacing: 16) { + Image(systemName: "person.fill") + .font(.system(size: 22)) + .fontWeight(.semibold) + .foregroundStyle(.accent) + .padding(10) + .background( + Rectangle() + .fill(.accent.opacity(0.25)) + .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous)) + ) + .shadow(radius: 10) + + VStack(alignment: .leading, spacing: 8) { + Text("Authorize contacts access") + .textTheme(.button) + .padding(.top, 2) + + Text("Send money instantly and easily to your friends on Vault") + .multilineTextAlignment(.leading) + .textTheme(.subtitle) + .padding(.top, 2) + } + + Spacer() + } + } + .padding(16) + .background(.background2) + .buttonStyle(PlainButtonStyle()) + .clipShape(RoundedRectangle(cornerRadius: 16)) + } + .listRowInsets(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)) + .listRowBackground(EmptyView()) + .listRowSeparator(.hidden) + + default: + Section { + Button { + self.presentingNewRecipientView = true + } label: { + HStack(spacing: 16) { + Image(.logo) + .resizable() + .scaledToFit() + .frame(width: 32) + .foregroundStyle(.accent) + + Text("Vault Recipient") + .textTheme(.button) + .padding(.top, 2) + + Spacer() + } + .padding(.vertical, 24) + .padding(.horizontal, 16) + .background(.background2) + } + .buttonStyle(PlainButtonStyle()) + .clipShape(RoundedRectangle(cornerRadius: 16)) + + } header: { + Text("Add new") + .textTheme(.headlineMedium) + .listRowInsets(EdgeInsets(top: 32, leading: 24, bottom: 12, trailing: 0)) + } + .textCase(nil) + .listRowInsets(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)) + .listRowBackground(EmptyView()) + .listRowSeparator(.hidden) + } + + if !self.model.contacts.isEmpty { + Section { + ForEach( + Array(self.model.contacts.enumerated()), + id: \.offset + ) { index, contact in + let isFirst = index == 0 + let isLast = index == self.model.contacts.count - 1 + + Button { + self.model.setRecipient(contact) + self.presentingSendingAmountView = true + } label: { + ContactRow(contact: contact) + .padding(16) + .background(.background2) + .clipShape( + .rect( + topLeadingRadius: isFirst ? 16 : 0, + bottomLeadingRadius: isLast ? 16 : 0, + bottomTrailingRadius: isLast ? 16 : 0, + topTrailingRadius: isFirst ? 16 : 0 + ) + ) + } + .buttonStyle(PlainButtonStyle()) + .listRowInsets(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16)) + .listRowSeparator(.hidden) + .listRowBackground(EmptyView()) + } + } header: { + Text("Contacts").textTheme(.headlineMedium) + .listRowInsets(EdgeInsets(top: 16, leading: 24, bottom: 12, trailing: 0)) + } + .textCase(nil) + } + } + + // List + + .scrollClipDisabled() + .scrollContentBackground(.hidden) + .listStyle(.grouped) + + // Layout + + .frame(maxWidth: .infinity, maxHeight: .infinity) + .defaultBackground() + .safeAreaInset(edge: .bottom) { + EmptyView().frame(height: 32) + } + .safeAreaInset(edge: .top) { + EmptyView().frame(height: 16) + } + + // Nagivation + + .navigationBarBackButtonHidden(true) + .navigationTitle("Select recipient") + .navigationBarTitleDisplayMode(.inline) + .navigationBarItems( + leading: IconButton { + self.dismiss() + } icon: { + Image(systemName: "xmark") + .iconify() + .fontWeight(.bold) + } + ) + .removeNavigationBarBorder() + + // Navigation destination + + .navigationDestination(isPresented: self.$presentingNewRecipientView) { + NewRecipientView() + } + .navigationDestination(isPresented: self.$presentingSendingAmountView) { + SendingAmountView() + } + } +} + +#Preview { + struct SendingRecipientViewPreviews: View { + + @StateObject var model = Model() + + var body: some View { + NavigationStack { + SendingRecipientView() + .environmentObject(self.model) + } + } + } + + return SendingRecipientViewPreviews() +} diff --git a/app/App/Navigation/Core/Sending/SendingView.swift b/app/App/Navigation/Core/Sending/SendingView.swift new file mode 100644 index 00000000..ea594c87 --- /dev/null +++ b/app/App/Navigation/Core/Sending/SendingView.swift @@ -0,0 +1,17 @@ +// +// SendingView.swift +// Vault +// +// Created by Charles Lanier on 21/05/2024. +// + +import SwiftUI + +struct SendingView: View { + + var body: some View { + NavigationStack { + SendingRecipientView() + } + } +} diff --git a/app/App/Navigation/Core/TransferView.swift b/app/App/Navigation/Core/TransferView.swift deleted file mode 100644 index dc5330fb..00000000 --- a/app/App/Navigation/Core/TransferView.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// TransferView.swift -// Vault -// -// Created by Charles Lanier on 02/05/2024. -// - -import SwiftUI - -struct TransferView: View { - - @State var amount: String = "" - - @State var rawPhoneNumber: String = "" - - var body: some View { - VStack { - Spacer() - - VStack(alignment: .center, spacing: 64) { - AmountInput(amount: $amount) - - TextInput("phone number", text: $rawPhoneNumber) - .keyboardType(.phonePad) - } - .frame(maxWidth: 250) - - Spacer() - - HStack { - PrimaryButton("Request") {} - PrimaryButton("Send") {} - } - } - .padding(16) - .defaultBackground() - } -} - -#Preview { - ZStack { - Color.background1.edgesIgnoringSafeArea(.all) - TransferView() - } -} diff --git a/app/App/Navigation/CustomTabBar.swift b/app/App/Navigation/CustomTabBar.swift index a02c4fc8..599e3a13 100644 --- a/app/App/Navigation/CustomTabBar.swift +++ b/app/App/Navigation/CustomTabBar.swift @@ -53,7 +53,6 @@ struct CustomTabbar: View { .tag(2) } .toolbarBackground(.hidden, for: .navigationBar) - .preferredColorScheme(.dark) CustomTabbar(selectedTab: .constant(.payments)) } diff --git a/app/App/Navigation/ErrorView.swift b/app/App/Navigation/ErrorView.swift new file mode 100644 index 00000000..27087a37 --- /dev/null +++ b/app/App/Navigation/ErrorView.swift @@ -0,0 +1,23 @@ +// +// ErrorView.swift +// Vault +// +// Created by Charles Lanier on 18/06/2024. +// + +import SwiftUI + +struct ErrorView: View { + var body: some View { + VStack { + Text("An error has occurred").textTheme(.headlineLarge) + Text("Please contact our support.").textTheme(.subtitle) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .defaultBackground() + } +} + +#Preview { + ErrorView() +} diff --git a/app/App/Navigation/Onboarding/AccessCodeView.swift b/app/App/Navigation/Onboarding/AccessCodeView.swift deleted file mode 100644 index 71b8b881..00000000 --- a/app/App/Navigation/Onboarding/AccessCodeView.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// AskSurnameView.swift -// Vault -// -// Created by Charles Lanier on 21/03/2024. -// - -import SwiftUI - -struct AccessCodeView: View { - @State private var presentingNextView = false - @State private var accessCode = "" - - var body: some View { - OnboardingPage { - VStack(alignment: .center, spacing: 64) { - VStack(alignment: .center, spacing: 24) { - Text("Early User Access").textTheme(.headlineLarge) - - Text("To join our exclusive early users, please enter your access code.") - .textTheme(.headlineSubtitle) - .multilineTextAlignment(.center) - } - - VStack(alignment: .center, spacing: 32) { - TextInput("Access Code", text: $accessCode, shouldFocusOnAppear: true) - - PrimaryButton("Start", disabled: accessCode.isEmpty) { - // TODO: Verify access code - presentingNextView = true - } - } - } - - Spacer() - } - .navigationDestination(isPresented: $presentingNextView) { - AskSurnameView() - } - } -} - -#Preview { - NavigationStack { - AccessCodeView() - } -} diff --git a/app/App/Navigation/Onboarding/AskSurnameView.swift b/app/App/Navigation/Onboarding/AskSurnameView.swift index f1b0f3ce..b0d60505 100644 --- a/app/App/Navigation/Onboarding/AskSurnameView.swift +++ b/app/App/Navigation/Onboarding/AskSurnameView.swift @@ -8,7 +8,8 @@ import SwiftUI struct AskSurnameView: View { - @EnvironmentObject private var settingsModel: SettingsModel + + @AppStorage("surname") var surname: String = "" @State private var presentingNextView = false @@ -21,13 +22,14 @@ struct AskSurnameView: View { Text("Introduce yourself with your surname. Change it anytime.") .textTheme(.headlineSubtitle) .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) } VStack(alignment: .center, spacing: 32) { - TextInput("Surname", text: $settingsModel.surname, shouldFocusOnAppear: true) + TextInput("Surname", text: self.$surname, shouldFocusOnAppear: true) - PrimaryButton("Next", disabled: settingsModel.surname.isEmpty) { - presentingNextView = true + PrimaryButton("Next", disabled: surname.isEmpty) { + self.presentingNextView = true } } } diff --git a/app/App/Navigation/Onboarding/CelebrationView.swift b/app/App/Navigation/Onboarding/CelebrationView.swift index cc9d3cfa..3f7d4ea4 100644 --- a/app/App/Navigation/Onboarding/CelebrationView.swift +++ b/app/App/Navigation/Onboarding/CelebrationView.swift @@ -9,7 +9,8 @@ import SwiftUI import ConfettiSwiftUI struct CelebrationView: View { - @EnvironmentObject private var settingsModel: SettingsModel + + @EnvironmentObject var model: Model @State private var presentingNextView = false @State private var confetti = 0 @@ -35,14 +36,16 @@ struct CelebrationView: View { Text("That’s it ! You are all set") .textTheme(.headlineLarge) .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) Text("You're now part of a 100% mobile, flexible banking revolution. Enjoy the instant transactions, sub-accounts for easier saving, and much more.") .textTheme(.headlineSubtitle) .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) } PrimaryButton("Start exploring") { - settingsModel.isOnboarded = true + self.model.isOnboarded = true } } } diff --git a/app/App/Navigation/Onboarding/FaceIDView.swift b/app/App/Navigation/Onboarding/FaceIDView.swift index 0e00717a..73ac4de5 100644 --- a/app/App/Navigation/Onboarding/FaceIDView.swift +++ b/app/App/Navigation/Onboarding/FaceIDView.swift @@ -26,10 +26,12 @@ struct FaceIDView: View { Text("Better experience with Face ID") .textTheme(.headlineLarge) .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) Text("Enable Face ID to make your transactions smooth and secure.") .textTheme(.headlineSubtitle) .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) } PrimaryButton("Set up Face ID") { diff --git a/app/App/Navigation/Onboarding/NotificationsView.swift b/app/App/Navigation/Onboarding/NotificationsView.swift index aea92956..b4bb59ed 100644 --- a/app/App/Navigation/Onboarding/NotificationsView.swift +++ b/app/App/Navigation/Onboarding/NotificationsView.swift @@ -25,10 +25,12 @@ struct NotificationView: View { Text("Stay Updated Instantly") .textTheme(.headlineLarge) .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) Text("Notifications to keep track of your account activity effortlessly.") .textTheme(.headlineSubtitle) .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) } VStack(alignment: .center, spacing: 16) { @@ -55,3 +57,14 @@ struct NotificationView: View { NotificationView() } } + +extension UINavigationController: UIGestureRecognizerDelegate { + override open func viewDidLoad() { + super.viewDidLoad() + interactivePopGestureRecognizer?.delegate = self + } + + public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + return viewControllers.count > 1 + } +} diff --git a/app/App/Navigation/Onboarding/PhoneRequestView.swift b/app/App/Navigation/Onboarding/PhoneRequestView.swift index 48a57b73..d790d6d6 100644 --- a/app/App/Navigation/Onboarding/PhoneRequestView.swift +++ b/app/App/Navigation/Onboarding/PhoneRequestView.swift @@ -10,14 +10,14 @@ import PhoneNumberKit struct PhoneRequestView: View { - @EnvironmentObject private var registrationModel: RegistrationModel + @EnvironmentObject private var model: Model @State private var presentingNextView = false @State private var phoneNumber = "" @State private var parsedPhoneNumber: PhoneNumber? var body: some View { - OnboardingPage(isLoading: $registrationModel.isLoading) { + OnboardingPage(isLoading: $model.isLoading) { VStack(alignment: .center, spacing: 64) { VStack(alignment: .center, spacing: 24) { Text("A Personalized Touch").textTheme(.headlineLarge) @@ -25,6 +25,7 @@ struct PhoneRequestView: View { Text("Enter your phone number. We will send you a confirmation code.") .textTheme(.headlineSubtitle) .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) } VStack(alignment: .center, spacing: 32) { @@ -33,15 +34,9 @@ struct PhoneRequestView: View { VStack(alignment: .center, spacing: 16) { // TODO: implement login PrimaryButton("Sign up", disabled: self.parsedPhoneNumber == nil) { - registrationModel.startRegistration(phoneNumber: self.parsedPhoneNumber!) { result in - switch result { - case .success(): - presentingNextView = true - - case .failure(let error): - print(error) - // TODO: handle error - } + self.model.startRegistration(phoneNumber: self.parsedPhoneNumber!) { + // next view + presentingNextView = true } } } @@ -59,12 +54,12 @@ struct PhoneRequestView: View { #if DEBUG struct PhoneRequestViewPreviews : PreviewProvider { - @StateObject static var registrationModel = RegistrationModel(vaultService: VaultService()) + @StateObject static var model = Model() static var previews: some View { NavigationStack { PhoneRequestView() - .environmentObject(self.registrationModel) + .environmentObject(self.model) } } } diff --git a/app/App/Navigation/Onboarding/PhoneValidationView.swift b/app/App/Navigation/Onboarding/PhoneValidationView.swift index b3a00faa..b3e54050 100644 --- a/app/App/Navigation/Onboarding/PhoneValidationView.swift +++ b/app/App/Navigation/Onboarding/PhoneValidationView.swift @@ -10,7 +10,9 @@ import PhoneNumberKit struct PhoneValidationView: View { - @EnvironmentObject private var registrationModel: RegistrationModel + @EnvironmentObject private var model: Model + + @AppStorage("starknetMainAddress") private var address: String = "0xdead" @State private var presentingNextView = false @State private var otp = "" { @@ -22,7 +24,7 @@ struct PhoneValidationView: View { let phoneNumber: PhoneNumber! var body: some View { - OnboardingPage(isLoading: $registrationModel.isLoading) { + OnboardingPage(isLoading: $model.isLoading) { VStack(alignment: .center, spacing: 64) { VStack(alignment: .center, spacing: 24) { Text("6-digits code").textTheme(.headlineLarge) @@ -30,30 +32,16 @@ struct PhoneValidationView: View { Text("A code has been sent to +\(phoneNumber.countryCode)\(phoneNumber.numberString.filter { !$0.isWhitespace })") .textTheme(.headlineSubtitle) .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) } VStack(alignment: .leading, spacing: 32) { OTPInput(otp: $otp, numberOfFields: Constants.registrationCodeDigitsCount) - .onChange(of: otp, initial: false) { (_, newValue) in + .onChange(of: self.otp) { newValue in if newValue.count == Constants.registrationCodeDigitsCount { - do { - guard let publicKey = try SecureEnclaveManager.shared.generateKeyPair() else { - throw "Failed to generate public key" - } - - registrationModel.confirmRegistration(phoneNumber: self.phoneNumber, otp: newValue, publicKey: publicKey) { result in - switch result { - case .success(let address): - print(address) - presentingNextView = true - - case .failure(let error): - print(error) - // TODO: handle error - } - } - } catch { - // TODO: Handle errors + self.model.confirmRegistration(phoneNumber: self.phoneNumber, otp: newValue) { + // next view + presentingNextView = true } } } @@ -75,7 +63,7 @@ struct PhoneValidationView: View { #if DEBUG struct PhoneValidationViewPreviews : PreviewProvider { - @StateObject static var registrationModel = RegistrationModel(vaultService: VaultService()) + @StateObject static var model = Model() static let phoneNumberKit = PhoneNumberKit() @@ -90,7 +78,7 @@ struct PhoneValidationViewPreviews : PreviewProvider { static var previews: some View { NavigationStack { PhoneValidationView(phoneNumber: self.phoneNumber) - .environmentObject(self.registrationModel) + .environmentObject(self.model) } } } diff --git a/app/App/Navigation/Onboarding/Shared.swift b/app/App/Navigation/Onboarding/Shared.swift index 86207a09..f6a0671d 100644 --- a/app/App/Navigation/Onboarding/Shared.swift +++ b/app/App/Navigation/Onboarding/Shared.swift @@ -11,22 +11,22 @@ struct OnboardingPage: View where Content : View { @Binding var loading: Bool - let content: () -> Content + let content: Content - init(isLoading: Binding, @ViewBuilder content: @escaping () -> Content) { - self.content = content + init(isLoading: Binding, @ViewBuilder content: () -> Content) { + self.content = content() self._loading = isLoading } - init(@ViewBuilder content: @escaping () -> Content) { - self.content = content + init(@ViewBuilder content: () -> Content) { + self.content = content() self._loading = .constant(false) } var body: some View { ZStack { VStack { - content() + content } .toolbar(.hidden) .padding(EdgeInsets(top: 64, leading: 16, bottom: 32, trailing: 16)) @@ -38,7 +38,7 @@ struct OnboardingPage: View where Content : View { .opacity(0.5) .ignoresSafeArea() - SpinnerView() + SpinnerView(isComplete: .constant(false)) } } } diff --git a/app/App/Navigation/Onboarding/WelcomeView.swift b/app/App/Navigation/Onboarding/WelcomeView.swift index 7eaefcdf..172395ee 100644 --- a/app/App/Navigation/Onboarding/WelcomeView.swift +++ b/app/App/Navigation/Onboarding/WelcomeView.swift @@ -33,6 +33,7 @@ struct WelcomeView: View { Text("Empower Your Assets\nRedefine Control") .textTheme(.headlineSubtitle) .multilineTextAlignment(.center) + .fixedSize(horizontal: false, vertical: true) } PrimaryButton("Get Started") { presentingNextView = true @@ -40,7 +41,7 @@ struct WelcomeView: View { } } .navigationDestination(isPresented: $presentingNextView) { - AccessCodeView() + AskSurnameView() } } } diff --git a/app/App/Services/VaultAPI/Models/APIRequest.swift b/app/App/Services/VaultAPI/Models/APIRequest.swift new file mode 100644 index 00000000..4d6028aa --- /dev/null +++ b/app/App/Services/VaultAPI/Models/APIRequest.swift @@ -0,0 +1,35 @@ +// +// APIRequest.swift +// Vault +// +// Created by Charles Lanier on 14/06/2024. +// + +import Foundation + +public enum HTTPMethod: String { + case GET + case POST +} + +/// All requests must conform to this protocol +/// - Discussion: You must conform to Encodable too, so that all stored public parameters +/// of types conforming this protocol will be encoded as parameters. +public protocol APIRequest: Encodable { + /// Response (will be wrapped with a DataContainer) + associatedtype Response: Decodable + + /// Endpoint for this request (the last part of the URL) + var resourceName: String { get } + + var httpMethod: HTTPMethod { get } + + var headers: [String: String] { get } +} + +public extension APIRequest { + + var headers: [String: String] { + return [:] + } +} diff --git a/app/App/Services/VaultAPI/Models/Balance.swift b/app/App/Services/VaultAPI/Models/Balance.swift new file mode 100644 index 00000000..933bfbe4 --- /dev/null +++ b/app/App/Services/VaultAPI/Models/Balance.swift @@ -0,0 +1,12 @@ +// +// Balance.swift +// Vault +// +// Created by Charles Lanier on 14/06/2024. +// + +import Foundation + +public struct Balance: Decodable { + public let balance: String +} diff --git a/app/App/Services/VaultAPI/Models/Deployment.swift b/app/App/Services/VaultAPI/Models/Deployment.swift new file mode 100644 index 00000000..cbea60dd --- /dev/null +++ b/app/App/Services/VaultAPI/Models/Deployment.swift @@ -0,0 +1,13 @@ +// +// Deployment.swift +// Vault +// +// Created by Charles Lanier on 17/06/2024. +// + +import Foundation + +public struct Deployment: Decodable { + public let contract_address: String +} + diff --git a/app/App/Services/VaultAPI/Models/Empty.swift b/app/App/Services/VaultAPI/Models/Empty.swift new file mode 100644 index 00000000..6c315111 --- /dev/null +++ b/app/App/Services/VaultAPI/Models/Empty.swift @@ -0,0 +1,10 @@ +// +// Empty.swift +// Vault +// +// Created by Charles Lanier on 17/06/2024. +// + +import Foundation + +public struct Empty: Decodable {} diff --git a/app/App/Services/VaultAPI/Models/Error.swift b/app/App/Services/VaultAPI/Models/Error.swift new file mode 100644 index 00000000..537f4517 --- /dev/null +++ b/app/App/Services/VaultAPI/Models/Error.swift @@ -0,0 +1,13 @@ +// +// Error.swift +// Vault +// +// Created by Charles Lanier on 14/06/2024. +// + +import Foundation + +public struct ErrorResponse: Decodable { + /// Message that usually gives more information about some error + public let message: String? +} diff --git a/app/App/Services/VaultAPI/Models/Execution.swift b/app/App/Services/VaultAPI/Models/Execution.swift new file mode 100644 index 00000000..e4f0a01d --- /dev/null +++ b/app/App/Services/VaultAPI/Models/Execution.swift @@ -0,0 +1,12 @@ +// +// Execution.swift +// Vault +// +// Created by Charles Lanier on 16/06/2024. +// + +import Foundation + +public struct Execution: Decodable { + public let transaction_hash: String +} diff --git a/app/App/Services/VaultAPI/Models/FunkitStripeCheckout.swift b/app/App/Services/VaultAPI/Models/FunkitStripeCheckout.swift new file mode 100644 index 00000000..4f802c3d --- /dev/null +++ b/app/App/Services/VaultAPI/Models/FunkitStripeCheckout.swift @@ -0,0 +1,15 @@ +// +// FunkitStripeCheckout.swift +// Vault +// +// Created by Charles Lanier on 03/07/2024. +// + +import Foundation + + +public struct FunkitStripeCheckout: Decodable { + public let stripeCheckoutId: String + public let stripeRedirectUrl: String + public let funkitDepositAddress: String +} diff --git a/app/App/Services/VaultAPI/Models/FunkitStripeCheckoutQuote.swift b/app/App/Services/VaultAPI/Models/FunkitStripeCheckoutQuote.swift new file mode 100644 index 00000000..a2313aad --- /dev/null +++ b/app/App/Services/VaultAPI/Models/FunkitStripeCheckoutQuote.swift @@ -0,0 +1,19 @@ +// +// FunkitStripeCheckoutQuote.swift +// Vault +// +// Created by Charles Lanier on 03/07/2024. +// + +import Foundation + +public struct FunkitStripeCheckoutQuote: Decodable { + public let quoteId: String + public let estSubtotalUsd: Double + public let paymentTokenChain: String + public let paymentTokenSymbol: String + public let paymentTokenAmount: Double + public let networkFees: String + public let cardFees: String + public let totalUsd: String +} diff --git a/app/App/Services/VaultAPI/Models/Transaction.swift b/app/App/Services/VaultAPI/Models/Transaction.swift new file mode 100644 index 00000000..959fc572 --- /dev/null +++ b/app/App/Services/VaultAPI/Models/Transaction.swift @@ -0,0 +1,23 @@ +// +// Transactions.swift +// Vault +// +// Created by Charles Lanier on 19/06/2024. +// + +import Foundation + +public struct RawTransactionUser: Decodable { + public let nickname: String? + public let contract_address: String? + public let phone_number: String? + public let balance: String +} + +public struct RawTransaction: Decodable { + public let transaction_timestamp: String + public let transferId: String + public let amount: String + public let from: RawTransactionUser + public let to: RawTransactionUser +} diff --git a/app/App/Services/VaultAPI/Models/User.swift b/app/App/Services/VaultAPI/Models/User.swift new file mode 100644 index 00000000..7b6d37ed --- /dev/null +++ b/app/App/Services/VaultAPI/Models/User.swift @@ -0,0 +1,12 @@ +// +// User.swift +// Vault +// +// Created by Charles Lanier on 29/06/2024. +// + +import Foundation + +public struct RawUser: Decodable { + public let user: String +} diff --git a/app/App/Services/VaultAPI/Models/VaultError.swift b/app/App/Services/VaultAPI/Models/VaultError.swift new file mode 100644 index 00000000..050ef3a1 --- /dev/null +++ b/app/App/Services/VaultAPI/Models/VaultError.swift @@ -0,0 +1,15 @@ +// +// VaultError.swift +// Vault +// +// Created by Charles Lanier on 14/06/2024. +// + +import Foundation + +public enum VaultError: Error { + case encoding + case decoding + case unknown + case server(message: String) +} diff --git a/app/App/Services/VaultAPI/Models/VaultPage.swift b/app/App/Services/VaultAPI/Models/VaultPage.swift new file mode 100644 index 00000000..a12a19a8 --- /dev/null +++ b/app/App/Services/VaultAPI/Models/VaultPage.swift @@ -0,0 +1,15 @@ +// +// VaultPage.swift +// Vault +// +// Created by Charles Lanier on 20/06/2024. +// + +import Foundation + +public struct VaultPage: Decodable { + public let hasNext: Bool + public let startCursor: String? + public let endCursor: String? + public let items: [T] +} diff --git a/app/App/Services/VaultAPI/Requests/CreateFunkitStripeCheckout.swift b/app/App/Services/VaultAPI/Requests/CreateFunkitStripeCheckout.swift new file mode 100644 index 00000000..56b391bd --- /dev/null +++ b/app/App/Services/VaultAPI/Requests/CreateFunkitStripeCheckout.swift @@ -0,0 +1,41 @@ +// +// CreateFunkitStripeCheckout.swift +// Vault +// +// Created by Charles Lanier on 03/07/2024. +// + +import Foundation + +public struct CreateFunkitStripeCheckout: APIRequest { + + public typealias Response = FunkitStripeCheckout + + // Notice how we create a composed resourceName + public var resourceName: String { + return "create_funkit_stripe_checkout" + } + + public var httpMethod: HTTPMethod { + return .POST + } + + public var headers: [String : String] { + return ["Content-Type": "application/json"] + } + + // Parameters + public let quoteId: String + public let paymentTokenAmount: Double + public let estSubtotalUsd: Double + public let isNy: Bool + public let isEu: Bool + + public init(quoteId: String, paymentTokenAmount: Double, estSubtotalUsd: Double) { + self.quoteId = quoteId + self.paymentTokenAmount = paymentTokenAmount + self.estSubtotalUsd = estSubtotalUsd + self.isNy = false + self.isEu = true + } +} diff --git a/app/App/Services/VaultAPI/Requests/ExecuteFromOutside.swift b/app/App/Services/VaultAPI/Requests/ExecuteFromOutside.swift new file mode 100644 index 00000000..964521b9 --- /dev/null +++ b/app/App/Services/VaultAPI/Requests/ExecuteFromOutside.swift @@ -0,0 +1,38 @@ +// +// ExecuteFromOutside.swift +// Vault +// +// Created by Charles Lanier on 15/06/2024. +// + +import Foundation +import Starknet + +public struct ExecuteFromOutside: APIRequest { + public typealias Response = Execution + + // Notice how we create a composed resourceName + public var resourceName: String { + return "execute_from_outside" + } + + public var httpMethod: HTTPMethod { + return .POST + } + + public var headers: [String : String] { + return ["Content-Type": "application/json"] + } + + // Parameters + public let address: String + public let calldata: [String] + + public init(address: String, outsideExecution: OutsideExecution, signature: StarknetSignature) { + let rawOutsideExecution = outsideExecution.calldata.map { String($0.value, radix: 10) } + let rawSignautre = signature.map { String($0.value, radix: 10) } + + self.address = address + self.calldata = rawOutsideExecution + [String(rawSignautre.count, radix: 10)] + rawSignautre + } +} diff --git a/app/App/Services/VaultAPI/Requests/GetFunkitStripeCheckoutQuote.swift b/app/App/Services/VaultAPI/Requests/GetFunkitStripeCheckoutQuote.swift new file mode 100644 index 00000000..2cd9633c --- /dev/null +++ b/app/App/Services/VaultAPI/Requests/GetFunkitStripeCheckoutQuote.swift @@ -0,0 +1,36 @@ +// +// GetFunkitStripeCheckoutQuote.swift +// Vault +// +// Created by Charles Lanier on 03/07/2024. +// + +import Foundation + +public struct GetFunkitStripeCheckoutQuote: APIRequest { + + public typealias Response = FunkitStripeCheckoutQuote + + // Notice how we create a composed resourceName + public var resourceName: String { + return "get_funkit_stripe_checkout_quote" + } + + public var httpMethod: HTTPMethod { + return .GET + } + + // Parameters + public let address: String + public let tokenAmount: String + public let isNy: Bool + public let isEu: Bool + + public init(address: String, amount: String) { +// self.address = address + self.address = "0x0171eaf72B36Dd904509297A51c4744Dcaf2E20E327dd1e7b08808DC0283f0A3" + self.tokenAmount = amount + self.isNy = false + self.isEu = true + } +} diff --git a/app/App/Services/VaultAPI/Requests/GetOTP.swift b/app/App/Services/VaultAPI/Requests/GetOTP.swift new file mode 100644 index 00000000..45d30417 --- /dev/null +++ b/app/App/Services/VaultAPI/Requests/GetOTP.swift @@ -0,0 +1,35 @@ +// +// GetOTP.swift +// Vault +// +// Created by Charles Lanier on 17/06/2024. +// + +import Foundation +import PhoneNumberKit + +public struct GetOTP: APIRequest { + public typealias Response = Empty + + // Notice how we create a composed resourceName + public var resourceName: String { + return "get_otp" + } + + public var httpMethod: HTTPMethod { + return .POST + } + + public var headers: [String : String] { + return ["Content-Type": "application/json"] + } + + // Parameters + public let phone_number: String + public let nickname: String + + public init(phoneNumber: PhoneNumber, nickname: String) { + self.phone_number = phoneNumber.rawString() + self.nickname = nickname + } +} diff --git a/app/App/Services/VaultAPI/Requests/GetTransactionsHistory.swift b/app/App/Services/VaultAPI/Requests/GetTransactionsHistory.swift new file mode 100644 index 00000000..47688816 --- /dev/null +++ b/app/App/Services/VaultAPI/Requests/GetTransactionsHistory.swift @@ -0,0 +1,42 @@ +// +// GetTransactionsHistory.swift +// Vault +// +// Created by Charles Lanier on 19/06/2024. +// + +import Foundation + +public struct GetTransactionsHistory: APIRequest { + + public typealias Response = VaultPage + + // Notice how we create a composed resourceName + public var resourceName: String { + return "transaction_history" + } + + public var httpMethod: HTTPMethod { + return .GET + } + + // Parameters + public let address: String + public let first: Int? + public let after: String? + public let before: String? + + public init(address: String, first: Int?, after: String?) { + self.address = address + self.first = first + self.after = after + self.before = nil + } + + public init(address: String, first: Int?, before: String?) { + self.address = address + self.first = first + self.before = before + self.after = nil + } +} diff --git a/app/App/Services/VaultAPI/Requests/GetUser.swift b/app/App/Services/VaultAPI/Requests/GetUser.swift new file mode 100644 index 00000000..01fc0edd --- /dev/null +++ b/app/App/Services/VaultAPI/Requests/GetUser.swift @@ -0,0 +1,29 @@ +// +// GetUser.swift +// Vault +// +// Created by Charles Lanier on 29/06/2024. +// + +import Foundation + +public struct GetUser: APIRequest { + + public typealias Response = RawUser + + // Notice how we create a composed resourceName + public var resourceName: String { + return "get_user" + } + + public var httpMethod: HTTPMethod { + return .GET + } + + // Parameters + public let address: String + + public init(address: String) { + self.address = address + } +} diff --git a/app/App/Services/VaultAPI/Requests/VerifyOTP.swift b/app/App/Services/VaultAPI/Requests/VerifyOTP.swift new file mode 100644 index 00000000..9331bf4a --- /dev/null +++ b/app/App/Services/VaultAPI/Requests/VerifyOTP.swift @@ -0,0 +1,42 @@ +// +// VerifyOTP.swift +// Vault +// +// Created by Charles Lanier on 17/06/2024. +// + +import Foundation +import PhoneNumberKit + +public struct VerifyOTP: APIRequest { + public typealias Response = Deployment + + // Notice how we create a composed resourceName + public var resourceName: String { + return "verify_otp" + } + + public var httpMethod: HTTPMethod { + return .POST + } + + public var headers: [String : String] { + return ["Content-Type": "application/json"] + } + + // Parameters + public let phone_number: String + public let sent_otp: String + public let public_key_x: String + public let public_key_y: String + + public init(phoneNumber: PhoneNumber, sentOTP: String, publicKey: P256PublicKey) { + self.phone_number = phoneNumber.rawString() + self.sent_otp = sentOTP + self.public_key_x = publicKey.x.toHex() + self.public_key_y = publicKey.y.toHex() + } +} + + + diff --git a/app/App/Services/VaultAPI/Utils/BodyParamsEncoder.swift b/app/App/Services/VaultAPI/Utils/BodyParamsEncoder.swift new file mode 100644 index 00000000..200450b1 --- /dev/null +++ b/app/App/Services/VaultAPI/Utils/BodyParamsEncoder.swift @@ -0,0 +1,21 @@ +// +// BodyParamsEncoder.swift +// Vault +// +// Created by Charles Lanier on 16/06/2024. +// + +import Foundation + +enum BodyParamsEncoder { + static func encode(_ encodable: T) throws -> Data { + let parametersData = try JSONEncoder().encode(encodable) +// let parameters = try JSONDecoder().decode([String: HTTPParameter].self, from: parametersData) +// let body = parameters.reduce(into: [:] as [String: Any]) { acc, param in +// acc[param.key] = param.value.description +// } +// // Convert the dictionary into JSON data +// return try! JSONSerialization.data(withJSONObject: body, options: []) + return parametersData + } +} diff --git a/app/App/Services/VaultAPI/Utils/HTTPParameter.swift b/app/App/Services/VaultAPI/Utils/HTTPParameter.swift new file mode 100644 index 00000000..3cc7165c --- /dev/null +++ b/app/App/Services/VaultAPI/Utils/HTTPParameter.swift @@ -0,0 +1,51 @@ +// +// HTTPParameter.swift +// Vault +// +// Created by Charles Lanier on 14/06/2024. +// + +import Foundation + +// Utility type so that we can decode any type of HTTP parameter +// Useful when we have mixed types in a HTTP request +enum HTTPParameter: CustomStringConvertible, Decodable { + case string(String) + case bool(Bool) + case int(Int) + case double(Double) + case stringArray([String]) + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + + if let string = try? container.decode(String.self) { + self = .string(string) + } else if let bool = try? container.decode(Bool.self) { + self = .bool(bool) + } else if let int = try? container.decode(Int.self) { + self = .int(int) + } else if let double = try? container.decode(Double.self) { + self = .double(double) + } else if let stringArray = try? container.decode([String].self) { + self = .stringArray(stringArray) + } else { + throw VaultError.decoding + } + } + + var description: String { + switch self { + case .string(let string): + return string + case .bool(let bool): + return String(describing: bool) + case .int(let int): + return String(describing: int) + case .double(let double): + return String(describing: double) + case .stringArray(let stringArray): + return stringArray.description + } + } +} diff --git a/app/App/Services/VaultAPI/Utils/URLQueryItemEncoder.swift b/app/App/Services/VaultAPI/Utils/URLQueryItemEncoder.swift new file mode 100644 index 00000000..316afa46 --- /dev/null +++ b/app/App/Services/VaultAPI/Utils/URLQueryItemEncoder.swift @@ -0,0 +1,16 @@ +// +// URLQueryItemEncoder.swift +// Vault +// +// Created by Charles Lanier on 14/06/2024. +// + +import Foundation + +enum URLQueryItemEncoder { + static func encode(_ encodable: T) throws -> [URLQueryItem] { + let parametersData = try JSONEncoder().encode(encodable) + let parameters = try JSONDecoder().decode([String: HTTPParameter].self, from: parametersData) + return parameters.map { URLQueryItem(name: $0, value: $1.description) } + } +} diff --git a/app/App/Services/VaultAPI/VaultAPIClient.swift b/app/App/Services/VaultAPI/VaultAPIClient.swift new file mode 100644 index 00000000..d2e281be --- /dev/null +++ b/app/App/Services/VaultAPI/VaultAPIClient.swift @@ -0,0 +1,125 @@ +// +// VaultAPIClient.swift +// Vault +// +// Created by Charles Lanier on 14/06/2024. +// + +import Foundation + +enum Endpoint { + case getOTP(String, String) + case verifyOTP(String, String, String, String) +} + +enum Method { + case get + case post +} + +public typealias ResultCallback = (Result) -> Void + +class VaultService { + + static let shared = VaultService() + + private let baseEndpointUrl = AppConfiguration.API.baseURL + private let session = URLSession(configuration: .default) + + /// Sends a request to Vault servers, calling the completion method when finished + public func send(_ request: T, completion: @escaping ResultCallback) { + let urlRequest = self.endpoint(for: request) + + let task = session.dataTask(with: urlRequest) { data, response, error in + if + let data = data, + let httpResponse = response as? HTTPURLResponse + { + +#if DEBUG + print(data.base64EncodedString()) +#endif + + if httpResponse.isSuccessful { + // request is successful + + do { + // Decode the top level response as data + let vaultResponse = try JSONDecoder().decode(T.Response.self, from: data) + + completion(.success(vaultResponse)) + } catch { + completion(.failure(VaultError.decoding)) + } + } else { + // request failed + + do { + // Decode the top level response as error + let vaultResponse = try JSONDecoder().decode(ErrorResponse.self, from: data) + + if let message = vaultResponse.message { + completion(.failure(VaultError.server(message: message))) + } else { + completion(.failure(VaultError.unknown)) + } + } catch { + completion(.failure(VaultError.decoding)) + } + } + } else if let error = error { + completion(.failure(error)) + } + } + task.resume() + } + + /// Encodes a URL based on the given request + /// Everything needed for a public request to Vault servers is encoded directly in this URL + private func endpoint(for request: T) -> URLRequest { + guard let baseUrl = URL(string: request.resourceName, relativeTo: baseEndpointUrl) else { + fatalError("Bad resourceName: \(request.resourceName)") + } + + var components = URLComponents(url: baseUrl, resolvingAgainstBaseURL: true)! + + // Add query parameters + if (request.httpMethod == .GET) { + // Custom query items needed for this specific request + let customQueryItems: [URLQueryItem] + + do { + customQueryItems = try URLQueryItemEncoder.encode(request) + } catch { + fatalError("Wrong parameters: \(error)") + } + + components.queryItems = customQueryItems + } + + // Construct the final URL with all the previous data + var urlRequest = URLRequest(url: components.url!) + + // add Headers and body params + if (request.httpMethod == .POST) { + for (headerField, value) in request.headers { + urlRequest.addValue(value, forHTTPHeaderField: headerField) + } + + // Custom body params needed for this specific request + let customBodyParams: Data + + do { + customBodyParams = try BodyParamsEncoder.encode(request) + } catch { + fatalError("Wrong parameters: \(error)") + } + + urlRequest.httpBody = customBodyParams + } + + urlRequest.httpMethod = request.httpMethod.rawValue + + return urlRequest + } +} diff --git a/app/App/Services/VaultService.swift b/app/App/Services/VaultService.swift deleted file mode 100644 index 1faa4e5f..00000000 --- a/app/App/Services/VaultService.swift +++ /dev/null @@ -1,120 +0,0 @@ -// -// VaultService.swift -// Vault -// -// Created by Charles Lanier on 21/04/2024. -// - -import Foundation - -enum Endpoint { - case getOTP(String, String) - case verifyOTP(String, String, PublicKey) -} - -class VaultService { - - private func query(endpoint: Endpoint, completion: @escaping @Sendable (Result<[String: Any], Error>) -> Void) { - var url = Constants.vaultBaseURL - var body: [String: String] = [:] - - switch endpoint { - case let .getOTP(phoneNumber, nickname): - url.append(path: "/get_otp") - - body["phone_number"] = phoneNumber - body["nickname"] = nickname - break - - case let .verifyOTP(phoneNumber, otp, publicKey): - url.append(path: "/verify_otp") - - body["phone_number"] = phoneNumber - body["sent_otp"] = otp - body["public_key_x"] = publicKey.x.toHex() - body["public_key_y"] = publicKey.y.toHex() - } - - // Convert the dictionary into JSON data - guard let jsonData = try? JSONSerialization.data(withJSONObject: body, options: []) else { - print("Error: Unable to encode JSON") - return - } - - // Create a URLRequest object and configure it for a POST method - var request = URLRequest(url: url) - - request.httpMethod = "POST" - request.httpBody = jsonData - request.addValue("application/json", forHTTPHeaderField: "Content-Type") - - // fetch request - URLSession.shared.dataTask(with: request) { data, response, error in - guard - let data = data, - let httpResponse = response as? HTTPURLResponse, - error == nil - else { - DispatchQueue.main.async { - completion(.failure(error ?? "Unknown error")) - } - return - } - - #if DEBUG - print(data.base64EncodedString()) - #endif - - do { - // make sure this JSON is in the format we expect - if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] { - DispatchQueue.main.async { - if httpResponse.isSuccessful { - completion(.success(json)) - } else { - completion(.failure(json["message"] as? String ?? "Unkown error")) - } - } - } - } catch let error as NSError { - DispatchQueue.main.async { - completion(.failure(error)) - } - } - }.resume() - } - - func getOTP(phoneNumber: String, completion: @escaping (Result) -> Void) { - self.query(endpoint: .getOTP(phoneNumber, "chqrles")) { result in - switch result { - case .success(let json): - guard let _ = json["ok"] as? Bool else { - completion(.failure("Unkown Error")) - return - } - - completion(.success(Void())) - - case .failure(let error): - completion(.failure(error)) - } - } - } - - func verifyOTP(phoneNumber: String, otp: String, publicKey: PublicKey, completion: @escaping (Result) -> Void) { - self.query(endpoint: .verifyOTP(phoneNumber, otp, publicKey)) { result in - switch result { - case .success(let json): - guard let address = json["contract_address"] as? String else { - completion(.failure("Unkown Error")) - return - } - - completion(.success(address)) - - case .failure(let error): - completion(.failure(error)) - } - } - } -} diff --git a/app/App/Utils/Amount.swift b/app/App/Utils/Amount.swift new file mode 100644 index 00000000..0ed0b925 --- /dev/null +++ b/app/App/Utils/Amount.swift @@ -0,0 +1,91 @@ +// +// Amount.swift +// Vault +// +// Created by Charles Lanier on 05/06/2024. +// + +import Foundation +import BigInt + +struct Amount: Hashable { + var value = Uint256(clamping: 0) + var decimals: UInt8 + + private var decimalPlaces: BigUInt { + return BigUInt(10).power(Int(self.decimals)) + } + + private var doubleDecimalPlaces: Double { + return pow(10.0, Double(self.decimals)) + } + + static private let formatter: NumberFormatter = { + let formatter = NumberFormatter() + formatter.numberStyle = .decimal + formatter.locale = Locale(identifier: "en_US") // Set locale to US + formatter.roundingMode = .halfEven + + return formatter + }() + + // MARK: - Init + + public init?(from: Double, decimals: UInt8) { + self.decimals = decimals + + // Shift the decimal by multiplying with 10^n + let shiftedValue = from * self.doubleDecimalPlaces + + // Check if shiftedValue is an integer + guard shiftedValue.truncatingRemainder(dividingBy: 1.0) == 0 else { + print("Result is not an integer after shifting") + return nil + } + + self.value = Uint256(clamping: BigUInt(shiftedValue)) + } + + public init?(from: S, decimals: UInt8, radix: Int) { + self.decimals = decimals + + guard let value = BigUInt(from, radix: radix) else { + print("Cannot convert to BigUint") + return nil + } + + self.value = Uint256(clamping: value) + } + + // MARK: - Formatters + + public func toFixed(_ digits: Int = 2) -> String { + let formatter = Self.formatter + + formatter.maximumFractionDigits = digits + formatter.minimumFractionDigits = digits + + let (interger, decimals) = self.value.value.quotientAndRemainder(dividingBy: self.decimalPlaces) + + let doubleValue = Double(interger) + Double(decimals) / self.doubleDecimalPlaces + + return formatter.string(from: NSNumber(value: doubleValue))! + } +} + +extension Amount { + + static func usdc(from: Double) -> Self? { + return Self(from: from, decimals: Constants.usdcDecimals) + } + + static func usdc(from: String) -> Self? { + return Self(from: from, decimals: Constants.usdcDecimals, radix: 10) + } + + static func usdc(fromHex hex: String) -> Self? { + guard hex.hasPrefix("0x") else { return nil } + + return Self(from: hex.dropFirst(2), decimals: Constants.usdcDecimals, radix: 16) + } +} diff --git a/app/App/Components/Background.swift b/app/App/Utils/Background.swift similarity index 77% rename from app/App/Components/Background.swift rename to app/App/Utils/Background.swift index bf081d68..976a99e3 100644 --- a/app/App/Components/Background.swift +++ b/app/App/Utils/Background.swift @@ -9,7 +9,9 @@ import SwiftUI struct DefaultBackgroundModifier: ViewModifier { func body(content: Content) -> some View { - content.background(.background1) + content + .preferredColorScheme(.dark) + .background(.background1) } } diff --git a/app/App/Utils/Container.swift b/app/App/Utils/Container.swift new file mode 100644 index 00000000..0d203409 --- /dev/null +++ b/app/App/Utils/Container.swift @@ -0,0 +1,18 @@ +// +// Container.swift +// Vault +// +// Created by Charles Lanier on 19/05/2024. +// + +import Foundation + +struct Container: Identifiable { + let id = UUID() + + var elements: [T] + + init(_ elements: [T]) { + self.elements = elements + } +} diff --git a/app/App/Utils/Icon.swift b/app/App/Utils/Icon.swift new file mode 100644 index 00000000..c51558ae --- /dev/null +++ b/app/App/Utils/Icon.swift @@ -0,0 +1,17 @@ +// +// Icon.swift +// Vault +// +// Created by Charles Lanier on 19/05/2024. +// + +import SwiftUI + +extension Image { + public func iconify() -> some View { + self + .renderingMode(.template) + .resizable() + .aspectRatio(contentMode: .fit) + } +} diff --git a/app/App/Utils/NavigationBar.swift b/app/App/Utils/NavigationBar.swift new file mode 100644 index 00000000..32af8500 --- /dev/null +++ b/app/App/Utils/NavigationBar.swift @@ -0,0 +1,47 @@ +// +// NavigationBar.swift +// Vault +// +// Created by Charles Lanier on 19/05/2024. +// + +import SwiftUI + +struct NavigationBarModifier: UIViewControllerRepresentable { + + func makeUIViewController(context: Context) -> UIViewController { + let viewController = UIViewController() + + DispatchQueue.main.async { + if let navigationController = viewController.navigationController { + let topAppearance = UINavigationBarAppearance() + let scrolledAppearance = UINavigationBarAppearance() + + topAppearance.configureWithOpaqueBackground() + topAppearance.backgroundColor = .clear + topAppearance.backgroundImage = UIImage() + topAppearance.shadowImage = UIImage() + topAppearance.shadowColor = .clear + + scrolledAppearance.configureWithOpaqueBackground() + scrolledAppearance.backgroundColor = .clear + scrolledAppearance.backgroundImage = UIImage() + scrolledAppearance.shadowImage = UIImage() + scrolledAppearance.shadowColor = .clear + +// navigationController.navigationBar.standardAppearance = scrolledAppearance + navigationController.navigationBar.scrollEdgeAppearance = topAppearance + } + } + + return viewController + } + + func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} +} + +extension View { + func removeNavigationBarBorder() -> some View { + self.background(NavigationBarModifier()) + } +} diff --git a/app/App/Utils/P256Signer.swift b/app/App/Utils/P256Signer.swift new file mode 100644 index 00000000..dfdf1903 --- /dev/null +++ b/app/App/Utils/P256Signer.swift @@ -0,0 +1,37 @@ +// +// P256Signer.swift +// Vault +// +// Created by Charles Lanier on 02/06/2024. +// + +import Foundation +import Starknet + +public struct P256PublicKey { + var x: Uint256 + var y: Uint256 +} + +class P256Signer: StarknetSignerProtocol { + // mandatory to respect the protocol + public let publicKey: Felt = Felt(fromHex: "0xdead")! + + private let secureEnclaveManager = SecureEnclaveManager() + + public func sign(transaction: any StarknetTransaction) throws -> StarknetSignature { + try self.sign(transactionHash: transaction.hash!) + } + + public func sign(transactionHash: Felt) throws -> StarknetSignature { + let signature = try secureEnclaveManager.sign(hash: transactionHash) + + return [signature.r.low, signature.r.high, signature.s.low, signature.s.high] + } + + public func sign(typedData: StarknetTypedData, accountAddress: Felt) throws -> StarknetSignature { + let messageHash = try typedData.getMessageHash(accountAddress: accountAddress) + + return try self.sign(transactionHash: messageHash) + } +} diff --git a/app/App/Utils/String+Email.swift b/app/App/Utils/String+Email.swift new file mode 100644 index 00000000..c36900ff --- /dev/null +++ b/app/App/Utils/String+Email.swift @@ -0,0 +1,17 @@ +// +// String+Email.swift +// Vault +// +// Created by Charles Lanier on 02/07/2024. +// + +import Foundation + +extension String { + var isValidEmail: Bool { + let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" + + let emailPred = NSPredicate(format:"SELF MATCHES %@", emailRegEx) + return emailPred.evaluate(with: self) + } +} diff --git a/app/App/Utils/String+character.swift b/app/App/Utils/String+character.swift index fa22f054..d585aa75 100644 --- a/app/App/Utils/String+character.swift +++ b/app/App/Utils/String+character.swift @@ -9,6 +9,21 @@ import SwiftUI extension String { + var initials: String { + let words = self.split(separator: " ") + + switch words.count { + case 0: + return "" + + case 1: + return String(words[0].prefix(1)) + + default: + return String(words[0].prefix(1)) + String(words[1].prefix(1)) + } + } + func character(at index: Int) -> String? { guard index >= 0 && index < self.count else { return nil diff --git a/app/App/Utils/UIImage.swift b/app/App/Utils/UIImage.swift new file mode 100644 index 00000000..21e093a0 --- /dev/null +++ b/app/App/Utils/UIImage.swift @@ -0,0 +1,27 @@ +// +// UIImage.swift +// Vault +// +// Created by Charles Lanier on 20/05/2024. +// + +import UIKit + +extension UIImage { + + static func gradientImageWithBounds(bounds: CGRect, colors: [CGColor]) -> UIImage { + let gradientLayer = CAGradientLayer() + + gradientLayer.frame = bounds + gradientLayer.colors = colors + + UIGraphicsBeginImageContext(gradientLayer.bounds.size) + + gradientLayer.render(in: UIGraphicsGetCurrentContext()!) + let image = UIGraphicsGetImageFromCurrentImageContext() + + UIGraphicsEndImageContext() + + return image! + } +} diff --git a/app/App/Utils/USDCAmount.swift b/app/App/Utils/USDCAmount.swift deleted file mode 100644 index 3e3edecd..00000000 --- a/app/App/Utils/USDCAmount.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// Amount.swift -// Vault -// -// Created by Charles Lanier on 02/04/2024. -// - -import Foundation - -class USDCAmount { - - // MARK: Properties - - private let rawAmount: Double - private var amount: Double { - get { - return self.rawAmount / Constants.usdcDecimalPlaces - } - } - - private lazy var formatter: NumberFormatter = { - let formatter = NumberFormatter() - formatter.numberStyle = .decimal - formatter.locale = Locale(identifier: "en_US") // Set locale to US - formatter.roundingMode = .halfEven - - return formatter - }() - - // MARK: Initializer - - init(_ rawAmount: Double) { - self.rawAmount = rawAmount - - } - - // MARK: Methods - - func toFixed(_ digits: Int = 2) -> String { - self.formatter.maximumFractionDigits = digits - self.formatter.minimumFractionDigits = digits - - return formatter.string(from: NSNumber(value: self.amount)) ?? "\(self.amount)" - } -} diff --git a/app/App/Utils/Uint256.swift b/app/App/Utils/Uint256.swift new file mode 100644 index 00000000..c391e250 --- /dev/null +++ b/app/App/Utils/Uint256.swift @@ -0,0 +1,55 @@ +// +// Uint256.swift +// Vault +// +// Created by Charles Lanier on 02/06/2024. +// + +import Foundation +import Starknet +import BigInt + +public struct Uint256: NumAsHexProtocol { + public var value: BigUInt + + public static let max = BigUInt(2).power(256) + + public var low: Felt + public var high: Felt + + public init?(_ exactly: some BinaryInteger) { + let value = BigUInt(exactly: exactly) + + guard let value, value < Uint256.max else { + return nil + } + + let (high, low) = value.quotientAndRemainder(dividingBy: BigUInt(2).power(128)) + + self.low = Felt(low)! + self.high = Felt(high)! + + self.value = value + } + + public init(clamping: some BinaryInteger) { + let value = BigUInt(clamping: clamping) + + self.value = value < Uint256.max ? value : Uint256.max - 1 + + let (high, low) = value.quotientAndRemainder(dividingBy: BigUInt(2).power(128)) + + self.low = Felt(low)! + self.high = Felt(high)! + } + + public init?(fromHex hex: String) { + guard hex.hasPrefix("0x") else { return nil } + + if let value = BigUInt(hex.dropFirst(2), radix: 16) { + self.init(value) + } else { + return nil + } + } +} diff --git a/app/App/VaultApp.swift b/app/App/VaultApp.swift index f4b59661..94683163 100644 --- a/app/App/VaultApp.swift +++ b/app/App/VaultApp.swift @@ -9,9 +9,15 @@ import SwiftUI @main struct VaultApp: App { + + @StateObject private var model = Model() + @StateObject private var txHistoryModel: PaginationModel = PaginationModel(threshold: 7, pageSize: 15) + var body: some Scene { WindowGroup { ContentView() + .environmentObject(self.model) + .environmentObject(self.txHistoryModel) } } } diff --git a/app/Dev.xcconfig b/app/Dev.xcconfig new file mode 100644 index 00000000..67c5c1cb --- /dev/null +++ b/app/Dev.xcconfig @@ -0,0 +1,13 @@ +// +// Development.xcconfig +// Vault +// +// Created by Charles Lanier on 18/06/2024. +// + +// Configuration settings file format documentation can be found at: +// https://help.apple.com/xcode/#/dev745c5c974 + +API_BASE_URL = api-development-684f.up.railway.app +PRIVATE_KEY_LABEL = com.vault.keys.privateKey.dev +SN_NETWORK = SN_SEPOLIA diff --git a/app/Staging.xcconfig b/app/Staging.xcconfig new file mode 100644 index 00000000..8468009a --- /dev/null +++ b/app/Staging.xcconfig @@ -0,0 +1,13 @@ +// +// Staging.xcconfig +// Vault +// +// Created by Charles Lanier on 18/06/2024. +// + +// Configuration settings file format documentation can be found at: +// https://help.apple.com/xcode/#/dev745c5c974 + +API_BASE_URL = api-production-e1fe.up.railway.app +PRIVATE_KEY_LABEL = com.vault.keys.privateKey.staging +SN_NETWORK = SN_SEPOLIA diff --git a/app/Vault-Info.plist b/app/Vault-Info.plist index 82dfdbce..af07e3c9 100644 --- a/app/Vault-Info.plist +++ b/app/Vault-Info.plist @@ -2,6 +2,27 @@ + API_BASE_URL + $(API_BASE_URL) + CFBundleGetInfoString + + CFBundleURLTypes + + + CFBundleTypeRole + Viewer + CFBundleURLName + com.chqrles.Vault + CFBundleURLSchemes + + vltfinance + + + + PRIVATE_KEY_LABEL + $(PRIVATE_KEY_LABEL) + SN_NETWORK + $(SN_NETWORK) UIAppFonts Sofia Pro Medium.otf diff --git a/app/Vault.xcodeproj/project.pbxproj b/app/Vault.xcodeproj/project.pbxproj index 2f1641bf..0475e828 100644 --- a/app/Vault.xcodeproj/project.pbxproj +++ b/app/Vault.xcodeproj/project.pbxproj @@ -7,53 +7,107 @@ objects = { /* Begin PBXBuildFile section */ + 9C09176C2C2466280068FAF5 /* PageInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C09176B2C2466280068FAF5 /* PageInfo.swift */; }; + 9C09176F2C2573580068FAF5 /* Transactions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C09176E2C2573580068FAF5 /* Transactions.swift */; }; + 9C0917712C25737C0068FAF5 /* PageableSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C0917702C25737C0068FAF5 /* PageableSource.swift */; }; + 9C0917742C2573B00068FAF5 /* Page.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C0917732C2573B00068FAF5 /* Page.swift */; }; + 9C09177B2C25D2810068FAF5 /* RequestingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C09177A2C25D2810068FAF5 /* RequestingView.swift */; }; + 9C1C9D842C0D093F0028C8FD /* P256Signer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C1C9D832C0D093F0028C8FD /* P256Signer.swift */; }; + 9C1C9D862C0D0F320028C8FD /* Uint256.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C1C9D852C0D0F320028C8FD /* Uint256.swift */; }; + 9C1C9D8C2C0DEBAE0028C8FD /* CountryData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C1C9D8B2C0DEBAE0028C8FD /* CountryData.swift */; }; + 9C1C9D902C0F5DE40028C8FD /* Popover.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C1C9D8F2C0F5DE40028C8FD /* Popover.swift */; }; + 9C1C9D922C0FB4760028C8FD /* SendingConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C1C9D912C0FB4760028C8FD /* SendingConfirmationView.swift */; }; + 9C1C9D942C0FBA900028C8FD /* Avatar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C1C9D932C0FBA900028C8FD /* Avatar.swift */; }; + 9C1C9D962C105E3D0028C8FD /* Amount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C1C9D952C105E3D0028C8FD /* Amount.swift */; }; + 9C286B562C32E84200D27A9D /* OnrampAmountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C286B552C32E84200D27A9D /* OnrampAmountView.swift */; }; + 9C286B582C32EB9B00D27A9D /* OnrampView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C286B572C32EB9B00D27A9D /* OnrampView.swift */; }; + 9C2D8D522C35887E008D6174 /* Placeholder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C2D8D512C35887E008D6174 /* Placeholder.swift */; }; + 9C2D8D542C35A582008D6174 /* CreateFunkitStripeCheckout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C2D8D532C35A582008D6174 /* CreateFunkitStripeCheckout.swift */; }; + 9C2D8D562C35CA13008D6174 /* FunkitStripeCheckout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C2D8D552C35CA13008D6174 /* FunkitStripeCheckout.swift */; }; + 9C2D8D582C360036008D6174 /* Line.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C2D8D572C360036008D6174 /* Line.swift */; }; 9C2E73C32BF39635004FFFD1 /* BalanceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C2E73C22BF39635004FFFD1 /* BalanceView.swift */; }; 9C2E73C52BF3CB86004FFFD1 /* Background.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C2E73C42BF3CB86004FFFD1 /* Background.swift */; }; 9C4F45192BDE949D00D44CBE /* SecureEnclaveManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C4F45182BDE949D00D44CBE /* SecureEnclaveManager.swift */; }; 9C4F451B2BDE999000D44CBE /* String+Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C4F451A2BDE999000D44CBE /* String+Error.swift */; }; 9C4F451D2BDEEF0E00D44CBE /* Data+from.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C4F451C2BDEEF0E00D44CBE /* Data+from.swift */; }; + 9C59C9442C30B2790074F23B /* GetUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C59C9432C30B2790074F23B /* GetUser.swift */; }; + 9C59C9462C30B35D0074F23B /* User.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C59C9452C30B35D0074F23B /* User.swift */; }; 9C5CFDA62BC69446001776E1 /* CountryPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C5CFDA52BC69446001776E1 /* CountryPickerView.swift */; }; 9C5CFDA82BC73DB0001776E1 /* SearchBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C5CFDA72BC73DB0001776E1 /* SearchBar.swift */; }; - 9C5CFDAE2BC828C9001776E1 /* PhoneNumberModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C5CFDAD2BC828C9001776E1 /* PhoneNumberModel.swift */; }; + 9C5CFDAE2BC828C9001776E1 /* Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C5CFDAD2BC828C9001776E1 /* Model.swift */; }; 9C5CFDB02BC828DE001776E1 /* Locale.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C5CFDAF2BC828DE001776E1 /* Locale.swift */; }; 9C5CFDB22BC82C57001776E1 /* Array+indexed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C5CFDB12BC82C57001776E1 /* Array+indexed.swift */; }; + 9C6259082BF79F140039DE9C /* FancyAmount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C6259072BF79F140039DE9C /* FancyAmount.swift */; }; + 9C62590A2BF79F8C0039DE9C /* NumPad.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C6259092BF79F8C0039DE9C /* NumPad.swift */; }; + 9C62590C2BF9FFF60039DE9C /* Container.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C62590B2BF9FFF60039DE9C /* Container.swift */; }; + 9C62590E2BFA7D260039DE9C /* Icon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C62590D2BFA7D260039DE9C /* Icon.swift */; }; + 9C6259102BFAADB90039DE9C /* NavigationBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C62590F2BFAADB90039DE9C /* NavigationBar.swift */; }; + 9C6259132BFB5A990039DE9C /* SendingAmountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C6259122BFB5A990039DE9C /* SendingAmountView.swift */; }; + 9C6259152BFB5B9A0039DE9C /* SendingRecipientView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C6259142BFB5B9A0039DE9C /* SendingRecipientView.swift */; }; + 9C6259172BFB6A910039DE9C /* UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C6259162BFB6A910039DE9C /* UIImage.swift */; }; + 9C6259192BFCDCEC0039DE9C /* NewRecipientView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C6259182BFCDCEC0039DE9C /* NewRecipientView.swift */; }; + 9C62591B2BFCF0650039DE9C /* SendingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C62591A2BFCF0650039DE9C /* SendingView.swift */; }; + 9C62591D2BFE20AF0039DE9C /* Contact.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C62591C2BFE20AF0039DE9C /* Contact.swift */; }; + 9C62591F2BFE56000039DE9C /* ContactRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C62591E2BFE56000039DE9C /* ContactRow.swift */; }; + 9C6259212BFE56590039DE9C /* NoAvatar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C6259202BFE56590039DE9C /* NoAvatar.swift */; }; 9C637CD62BB09334005816B4 /* NotificationsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C637CD52BB09334005816B4 /* NotificationsView.swift */; }; 9C637CD92BB09710005816B4 /* NotificationsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C637CD82BB09710005816B4 /* NotificationsManager.swift */; }; 9C637CDB2BB09C4D005816B4 /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C637CDA2BB09C4D005816B4 /* Notification.swift */; }; 9C637CDE2BB0A564005816B4 /* AppIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C637CDD2BB0A564005816B4 /* AppIcon.swift */; }; 9C637CE02BB0C0E5005816B4 /* Input.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C637CDF2BB0C0E5005816B4 /* Input.swift */; }; + 9C6ABFCC2C21C9C9000227D2 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C6ABFCB2C21C9C9000227D2 /* ErrorView.swift */; }; + 9C6ABFD02C21D21A000227D2 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C6ABFCF2C21D21A000227D2 /* Configuration.swift */; }; + 9C84F4172C2FF5E500196EA0 /* AddSendingConfirmation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C84F4162C2FF5E500196EA0 /* AddSendingConfirmation.swift */; }; + 9C9098772C2AC9FE002A5833 /* RequestingAmountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C9098762C2AC9FE002A5833 /* RequestingAmountView.swift */; }; + 9C9098792C2AED57002A5833 /* ActivityView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C9098782C2AED57002A5833 /* ActivityView.swift */; }; + 9C9F9EEF2C22368200620AC6 /* APIRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C9F9EDC2C22368200620AC6 /* APIRequest.swift */; }; + 9C9F9EF02C22368200620AC6 /* Balance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C9F9EDD2C22368200620AC6 /* Balance.swift */; }; + 9C9F9EF12C22368200620AC6 /* Deployment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C9F9EDE2C22368200620AC6 /* Deployment.swift */; }; + 9C9F9EF22C22368200620AC6 /* Empty.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C9F9EDF2C22368200620AC6 /* Empty.swift */; }; + 9C9F9EF32C22368200620AC6 /* Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C9F9EE02C22368200620AC6 /* Error.swift */; }; + 9C9F9EF42C22368200620AC6 /* Execution.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C9F9EE12C22368200620AC6 /* Execution.swift */; }; + 9C9F9EF52C22368200620AC6 /* VaultError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C9F9EE22C22368200620AC6 /* VaultError.swift */; }; + 9C9F9EF62C22368200620AC6 /* ExecuteFromOutside.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C9F9EE42C22368200620AC6 /* ExecuteFromOutside.swift */; }; + 9C9F9EF82C22368200620AC6 /* GetOTP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C9F9EE62C22368200620AC6 /* GetOTP.swift */; }; + 9C9F9EF92C22368200620AC6 /* VerifyOTP.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C9F9EE72C22368200620AC6 /* VerifyOTP.swift */; }; + 9C9F9EFA2C22368200620AC6 /* BodyParamsEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C9F9EE92C22368200620AC6 /* BodyParamsEncoder.swift */; }; + 9C9F9EFB2C22368200620AC6 /* HTTPParameter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C9F9EEA2C22368200620AC6 /* HTTPParameter.swift */; }; + 9C9F9EFC2C22368200620AC6 /* URLQueryItemEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C9F9EEB2C22368200620AC6 /* URLQueryItemEncoder.swift */; }; + 9C9F9EFD2C22368200620AC6 /* VaultAPIClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9C9F9EED2C22368200620AC6 /* VaultAPIClient.swift */; }; + 9CC82FFA2C1BB9A30089042C /* OutsideExecution.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CC82FF92C1BB9A30089042C /* OutsideExecution.swift */; }; + 9CD0988A2C22E32300FDDD8F /* GetTransactionsHistory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CD098892C22E32300FDDD8F /* GetTransactionsHistory.swift */; }; + 9CD0988C2C22E33800FDDD8F /* Transaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CD0988B2C22E33800FDDD8F /* Transaction.swift */; }; + 9CD0988E2C22EBE500FDDD8F /* History.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CD0988D2C22EBE500FDDD8F /* History.swift */; }; 9CD1BE8A2BCD4F5B0077A60B /* OTPInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CD1BE892BCD4F5B0077A60B /* OTPInput.swift */; }; 9CD1BE8C2BCD5D790077A60B /* String+character.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CD1BE8B2BCD5D790077A60B /* String+character.swift */; }; - 9CD1BE902BD53F7A0077A60B /* VaultService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CD1BE8F2BD53F7A0077A60B /* VaultService.swift */; }; - 9CD1BE962BD589AD0077A60B /* RegistrationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CD1BE952BD589AD0077A60B /* RegistrationModel.swift */; }; 9CD1BE982BD7C51A0077A60B /* PhoneNumber+Parse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CD1BE972BD7C51A0077A60B /* PhoneNumber+Parse.swift */; }; 9CD390EB2BE1977E00238FE9 /* HTTPURLReponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CD390EA2BE1977E00238FE9 /* HTTPURLReponse.swift */; }; - 9CD390ED2BE3CF5D00238FE9 /* TransferView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CD390EC2BE3CF5D00238FE9 /* TransferView.swift */; }; - 9CD390EF2BE5284000238FE9 /* AmountInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CD390EE2BE5284000238FE9 /* AmountInput.swift */; }; 9CD390F52BE93A2400238FE9 /* Sofia Pro Medium.otf in Resources */ = {isa = PBXBuildFile; fileRef = 9CD390F42BE93A2400238FE9 /* Sofia Pro Medium.otf */; }; 9CD390F72BEA3D2A00238FE9 /* EarnView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CD390F62BEA3D2A00238FE9 /* EarnView.swift */; }; 9CD390FB2BEAB6C600238FE9 /* WebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CD390FA2BEAB6C600238FE9 /* WebView.swift */; }; - 9CD7789D2BB1846600BA4677 /* AccessCodeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CD7789C2BB1846600BA4677 /* AccessCodeView.swift */; }; 9CD7789F2BB185C200BA4677 /* CelebrationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CD7789E2BB185C200BA4677 /* CelebrationView.swift */; }; 9CD778A32BB1939200BA4677 /* ConfettiSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 9CD778A22BB1939200BA4677 /* ConfettiSwiftUI */; }; - 9CD778A82BB2CE7D00BA4677 /* SettingsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CD778A72BB2CE7D00BA4677 /* SettingsModel.swift */; }; 9CD778AB2BBAF8BE00BA4677 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CD778AA2BBAF8BE00BA4677 /* HomeView.swift */; }; 9CD778AE2BBC1A7300BA4677 /* GraphView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CD778AD2BBC1A7300BA4677 /* GraphView.swift */; }; 9CD778B02BBC1A9C00BA4677 /* View+if.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CD778AF2BBC1A9C00BA4677 /* View+if.swift */; }; 9CD778B32BBC38F400BA4677 /* HistoricalGraph.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CD778B22BBC38F400BA4677 /* HistoricalGraph.swift */; }; 9CD778B62BBCB0DE00BA4677 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CD778B52BBCB0DE00BA4677 /* Constants.swift */; }; - 9CD778B82BBCB2A100BA4677 /* USDCAmount.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CD778B72BBCB2A100BA4677 /* USDCAmount.swift */; }; 9CD778BA2BBCB89C00BA4677 /* TransferRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CD778B92BBCB89C00BA4677 /* TransferRow.swift */; }; 9CD778BC2BBED06700BA4677 /* CustomTabBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CD778BB2BBED06700BA4677 /* CustomTabBar.swift */; }; 9CD778C02BBF1E5C00BA4677 /* EdgeBorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CD778BF2BBF1E5C00BA4677 /* EdgeBorder.swift */; }; 9CD778C22BBF1E9200BA4677 /* View+border.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CD778C12BBF1E9200BA4677 /* View+border.swift */; }; 9CD778C42BC1B63800BA4677 /* BudgetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CD778C32BC1B63800BA4677 /* BudgetView.swift */; }; - 9CD778C62BC1DEEA00BA4677 /* NavigationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CD778C52BC1DEEA00BA4677 /* NavigationModel.swift */; }; + 9CD778C62BC1DEEA00BA4677 /* Navigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CD778C52BC1DEEA00BA4677 /* Navigation.swift */; }; 9CD778CB2BC3113A00BA4677 /* PhoneRequestView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CD778CA2BC3113A00BA4677 /* PhoneRequestView.swift */; }; 9CD778CD2BC3115E00BA4677 /* PhoneValidationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CD778CC2BC3115E00BA4677 /* PhoneValidationView.swift */; }; 9CD778D02BC43F7400BA4677 /* PhoneNumberKit in Frameworks */ = {isa = PBXBuildFile; productRef = 9CD778CF2BC43F7400BA4677 /* PhoneNumberKit */; }; 9CD778D22BC4423C00BA4677 /* PhoneInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CD778D12BC4423C00BA4677 /* PhoneInput.swift */; }; 9CD778D42BC4426D00BA4677 /* View+placeholder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CD778D32BC4426D00BA4677 /* View+placeholder.swift */; }; + 9CDAF8362C32F98600BCFEDF /* OnrampStripeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CDAF8352C32F98600BCFEDF /* OnrampStripeView.swift */; }; + 9CDAF83C2C346D7C00BCFEDF /* String+Email.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CDAF83B2C346D7C00BCFEDF /* String+Email.swift */; }; + 9CDAF8402C355CA100BCFEDF /* GetFunkitStripeCheckoutQuote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CDAF83F2C355CA100BCFEDF /* GetFunkitStripeCheckoutQuote.swift */; }; + 9CDAF8422C355E4100BCFEDF /* FunkitStripeCheckoutQuote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CDAF8412C355E4100BCFEDF /* FunkitStripeCheckoutQuote.swift */; }; + 9CDD91B92C24129E00EA20CC /* VaultPage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CDD91B82C24129E00EA20CC /* VaultPage.swift */; }; 9CDFD3FF2BAB3428000466B9 /* VaultApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CDFD3FE2BAB3428000466B9 /* VaultApp.swift */; }; 9CDFD4012BAB3428000466B9 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CDFD4002BAB3428000466B9 /* ContentView.swift */; }; 9CDFD4032BAB3429000466B9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9CDFD4022BAB3429000466B9 /* Assets.xcassets */; }; @@ -71,6 +125,7 @@ 9CE7A7572BE050C0008509FE /* Starknet in Frameworks */ = {isa = PBXBuildFile; productRef = 9CE7A7562BE050C0008509FE /* Starknet */; }; 9CE7A75B2BE12CBC008509FE /* Data+Hex.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CE7A75A2BE12CBC008509FE /* Data+Hex.swift */; }; 9CE7A75D2BE15C21008509FE /* SpinnerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CE7A75C2BE15C21008509FE /* SpinnerView.swift */; }; + 9CE837E32C241DFA005A740A /* PaginationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CE837E22C241DFA005A740A /* PaginationModel.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -91,51 +146,107 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 9C09176B2C2466280068FAF5 /* PageInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageInfo.swift; sourceTree = ""; }; + 9C09176E2C2573580068FAF5 /* Transactions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Transactions.swift; sourceTree = ""; }; + 9C0917702C25737C0068FAF5 /* PageableSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageableSource.swift; sourceTree = ""; }; + 9C0917732C2573B00068FAF5 /* Page.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Page.swift; sourceTree = ""; }; + 9C09177A2C25D2810068FAF5 /* RequestingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestingView.swift; sourceTree = ""; }; + 9C1C9D832C0D093F0028C8FD /* P256Signer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = P256Signer.swift; sourceTree = ""; }; + 9C1C9D852C0D0F320028C8FD /* Uint256.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Uint256.swift; sourceTree = ""; }; + 9C1C9D8B2C0DEBAE0028C8FD /* CountryData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountryData.swift; sourceTree = ""; }; + 9C1C9D8F2C0F5DE40028C8FD /* Popover.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Popover.swift; sourceTree = ""; }; + 9C1C9D912C0FB4760028C8FD /* SendingConfirmationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendingConfirmationView.swift; sourceTree = ""; }; + 9C1C9D932C0FBA900028C8FD /* Avatar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Avatar.swift; sourceTree = ""; }; + 9C1C9D952C105E3D0028C8FD /* Amount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Amount.swift; sourceTree = ""; }; + 9C286B552C32E84200D27A9D /* OnrampAmountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnrampAmountView.swift; sourceTree = ""; }; + 9C286B572C32EB9B00D27A9D /* OnrampView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnrampView.swift; sourceTree = ""; }; + 9C2D8D512C35887E008D6174 /* Placeholder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Placeholder.swift; sourceTree = ""; }; + 9C2D8D532C35A582008D6174 /* CreateFunkitStripeCheckout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateFunkitStripeCheckout.swift; sourceTree = ""; }; + 9C2D8D552C35CA13008D6174 /* FunkitStripeCheckout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FunkitStripeCheckout.swift; sourceTree = ""; }; + 9C2D8D572C360036008D6174 /* Line.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Line.swift; sourceTree = ""; }; 9C2E73C22BF39635004FFFD1 /* BalanceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BalanceView.swift; sourceTree = ""; }; 9C2E73C42BF3CB86004FFFD1 /* Background.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Background.swift; sourceTree = ""; }; 9C4F45182BDE949D00D44CBE /* SecureEnclaveManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecureEnclaveManager.swift; sourceTree = ""; }; 9C4F451A2BDE999000D44CBE /* String+Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Error.swift"; sourceTree = ""; }; 9C4F451C2BDEEF0E00D44CBE /* Data+from.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+from.swift"; sourceTree = ""; }; + 9C59C9432C30B2790074F23B /* GetUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetUser.swift; sourceTree = ""; }; + 9C59C9452C30B35D0074F23B /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; 9C5CFDA52BC69446001776E1 /* CountryPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountryPickerView.swift; sourceTree = ""; }; 9C5CFDA72BC73DB0001776E1 /* SearchBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchBar.swift; sourceTree = ""; }; - 9C5CFDAD2BC828C9001776E1 /* PhoneNumberModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneNumberModel.swift; sourceTree = ""; }; + 9C5CFDAD2BC828C9001776E1 /* Model.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Model.swift; sourceTree = ""; }; 9C5CFDAF2BC828DE001776E1 /* Locale.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Locale.swift; sourceTree = ""; }; 9C5CFDB12BC82C57001776E1 /* Array+indexed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+indexed.swift"; sourceTree = ""; }; + 9C6259072BF79F140039DE9C /* FancyAmount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FancyAmount.swift; sourceTree = ""; }; + 9C6259092BF79F8C0039DE9C /* NumPad.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumPad.swift; sourceTree = ""; }; + 9C62590B2BF9FFF60039DE9C /* Container.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Container.swift; sourceTree = ""; }; + 9C62590D2BFA7D260039DE9C /* Icon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Icon.swift; sourceTree = ""; }; + 9C62590F2BFAADB90039DE9C /* NavigationBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationBar.swift; sourceTree = ""; }; + 9C6259122BFB5A990039DE9C /* SendingAmountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendingAmountView.swift; sourceTree = ""; }; + 9C6259142BFB5B9A0039DE9C /* SendingRecipientView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendingRecipientView.swift; sourceTree = ""; }; + 9C6259162BFB6A910039DE9C /* UIImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImage.swift; sourceTree = ""; }; + 9C6259182BFCDCEC0039DE9C /* NewRecipientView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewRecipientView.swift; sourceTree = ""; }; + 9C62591A2BFCF0650039DE9C /* SendingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendingView.swift; sourceTree = ""; }; + 9C62591C2BFE20AF0039DE9C /* Contact.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Contact.swift; sourceTree = ""; }; + 9C62591E2BFE56000039DE9C /* ContactRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactRow.swift; sourceTree = ""; }; + 9C6259202BFE56590039DE9C /* NoAvatar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoAvatar.swift; sourceTree = ""; }; 9C637CD52BB09334005816B4 /* NotificationsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsView.swift; sourceTree = ""; }; 9C637CD82BB09710005816B4 /* NotificationsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsManager.swift; sourceTree = ""; }; 9C637CDA2BB09C4D005816B4 /* Notification.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Notification.swift; sourceTree = ""; }; 9C637CDD2BB0A564005816B4 /* AppIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIcon.swift; sourceTree = ""; }; 9C637CDF2BB0C0E5005816B4 /* Input.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Input.swift; sourceTree = ""; }; + 9C6ABFCB2C21C9C9000227D2 /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = ""; }; + 9C6ABFCD2C21CF94000227D2 /* Dev.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Dev.xcconfig; sourceTree = ""; }; + 9C6ABFCE2C21D0CC000227D2 /* Staging.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Staging.xcconfig; sourceTree = ""; }; + 9C6ABFCF2C21D21A000227D2 /* Configuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = ""; }; + 9C84F4162C2FF5E500196EA0 /* AddSendingConfirmation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddSendingConfirmation.swift; sourceTree = ""; }; + 9C9098762C2AC9FE002A5833 /* RequestingAmountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestingAmountView.swift; sourceTree = ""; }; + 9C9098782C2AED57002A5833 /* ActivityView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityView.swift; sourceTree = ""; }; + 9C9F9EDC2C22368200620AC6 /* APIRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = APIRequest.swift; sourceTree = ""; }; + 9C9F9EDD2C22368200620AC6 /* Balance.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Balance.swift; sourceTree = ""; }; + 9C9F9EDE2C22368200620AC6 /* Deployment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Deployment.swift; sourceTree = ""; }; + 9C9F9EDF2C22368200620AC6 /* Empty.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Empty.swift; sourceTree = ""; }; + 9C9F9EE02C22368200620AC6 /* Error.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Error.swift; sourceTree = ""; }; + 9C9F9EE12C22368200620AC6 /* Execution.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Execution.swift; sourceTree = ""; }; + 9C9F9EE22C22368200620AC6 /* VaultError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VaultError.swift; sourceTree = ""; }; + 9C9F9EE42C22368200620AC6 /* ExecuteFromOutside.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExecuteFromOutside.swift; sourceTree = ""; }; + 9C9F9EE62C22368200620AC6 /* GetOTP.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GetOTP.swift; sourceTree = ""; }; + 9C9F9EE72C22368200620AC6 /* VerifyOTP.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VerifyOTP.swift; sourceTree = ""; }; + 9C9F9EE92C22368200620AC6 /* BodyParamsEncoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BodyParamsEncoder.swift; sourceTree = ""; }; + 9C9F9EEA2C22368200620AC6 /* HTTPParameter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTTPParameter.swift; sourceTree = ""; }; + 9C9F9EEB2C22368200620AC6 /* URLQueryItemEncoder.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLQueryItemEncoder.swift; sourceTree = ""; }; + 9C9F9EED2C22368200620AC6 /* VaultAPIClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VaultAPIClient.swift; sourceTree = ""; }; + 9CC82FF92C1BB9A30089042C /* OutsideExecution.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OutsideExecution.swift; sourceTree = ""; }; + 9CD098892C22E32300FDDD8F /* GetTransactionsHistory.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetTransactionsHistory.swift; sourceTree = ""; }; + 9CD0988B2C22E33800FDDD8F /* Transaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Transaction.swift; sourceTree = ""; }; + 9CD0988D2C22EBE500FDDD8F /* History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = History.swift; sourceTree = ""; }; 9CD1BE892BCD4F5B0077A60B /* OTPInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OTPInput.swift; sourceTree = ""; }; 9CD1BE8B2BCD5D790077A60B /* String+character.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+character.swift"; sourceTree = ""; }; - 9CD1BE8F2BD53F7A0077A60B /* VaultService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VaultService.swift; sourceTree = ""; }; - 9CD1BE952BD589AD0077A60B /* RegistrationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegistrationModel.swift; sourceTree = ""; }; 9CD1BE972BD7C51A0077A60B /* PhoneNumber+Parse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PhoneNumber+Parse.swift"; sourceTree = ""; }; 9CD390EA2BE1977E00238FE9 /* HTTPURLReponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPURLReponse.swift; sourceTree = ""; }; - 9CD390EC2BE3CF5D00238FE9 /* TransferView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransferView.swift; sourceTree = ""; }; - 9CD390EE2BE5284000238FE9 /* AmountInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AmountInput.swift; sourceTree = ""; }; 9CD390F42BE93A2400238FE9 /* Sofia Pro Medium.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "Sofia Pro Medium.otf"; sourceTree = ""; }; 9CD390F62BEA3D2A00238FE9 /* EarnView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EarnView.swift; sourceTree = ""; }; 9CD390FA2BEAB6C600238FE9 /* WebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebView.swift; sourceTree = ""; }; - 9CD7789C2BB1846600BA4677 /* AccessCodeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccessCodeView.swift; sourceTree = ""; }; 9CD7789E2BB185C200BA4677 /* CelebrationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CelebrationView.swift; sourceTree = ""; }; - 9CD778A72BB2CE7D00BA4677 /* SettingsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsModel.swift; sourceTree = ""; }; 9CD778AA2BBAF8BE00BA4677 /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; }; 9CD778AD2BBC1A7300BA4677 /* GraphView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphView.swift; sourceTree = ""; }; 9CD778AF2BBC1A9C00BA4677 /* View+if.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+if.swift"; sourceTree = ""; }; 9CD778B22BBC38F400BA4677 /* HistoricalGraph.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoricalGraph.swift; sourceTree = ""; }; 9CD778B52BBCB0DE00BA4677 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; - 9CD778B72BBCB2A100BA4677 /* USDCAmount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = USDCAmount.swift; sourceTree = ""; }; 9CD778B92BBCB89C00BA4677 /* TransferRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransferRow.swift; sourceTree = ""; }; 9CD778BB2BBED06700BA4677 /* CustomTabBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTabBar.swift; sourceTree = ""; }; 9CD778BF2BBF1E5C00BA4677 /* EdgeBorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EdgeBorder.swift; sourceTree = ""; }; 9CD778C12BBF1E9200BA4677 /* View+border.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+border.swift"; sourceTree = ""; }; 9CD778C32BC1B63800BA4677 /* BudgetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BudgetView.swift; sourceTree = ""; }; - 9CD778C52BC1DEEA00BA4677 /* NavigationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationModel.swift; sourceTree = ""; }; + 9CD778C52BC1DEEA00BA4677 /* Navigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Navigation.swift; sourceTree = ""; }; 9CD778CA2BC3113A00BA4677 /* PhoneRequestView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneRequestView.swift; sourceTree = ""; }; 9CD778CC2BC3115E00BA4677 /* PhoneValidationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneValidationView.swift; sourceTree = ""; }; 9CD778D12BC4423C00BA4677 /* PhoneInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhoneInput.swift; sourceTree = ""; }; 9CD778D32BC4426D00BA4677 /* View+placeholder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+placeholder.swift"; sourceTree = ""; }; + 9CDAF8352C32F98600BCFEDF /* OnrampStripeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnrampStripeView.swift; sourceTree = ""; }; + 9CDAF83B2C346D7C00BCFEDF /* String+Email.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Email.swift"; sourceTree = ""; }; + 9CDAF83F2C355CA100BCFEDF /* GetFunkitStripeCheckoutQuote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetFunkitStripeCheckoutQuote.swift; sourceTree = ""; }; + 9CDAF8412C355E4100BCFEDF /* FunkitStripeCheckoutQuote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FunkitStripeCheckoutQuote.swift; sourceTree = ""; }; + 9CDD91B82C24129E00EA20CC /* VaultPage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VaultPage.swift; sourceTree = ""; }; 9CDFD3FB2BAB3428000466B9 /* Vault.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Vault.app; sourceTree = BUILT_PRODUCTS_DIR; }; 9CDFD3FE2BAB3428000466B9 /* VaultApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VaultApp.swift; sourceTree = ""; }; 9CDFD4002BAB3428000466B9 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -156,6 +267,7 @@ 9CDFD4422BADA297000466B9 /* BiometricAuthManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BiometricAuthManager.swift; sourceTree = ""; }; 9CE7A75A2BE12CBC008509FE /* Data+Hex.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Hex.swift"; sourceTree = ""; }; 9CE7A75C2BE15C21008509FE /* SpinnerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpinnerView.swift; sourceTree = ""; }; + 9CE837E22C241DFA005A740A /* PaginationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaginationModel.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -186,6 +298,95 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 9C09176D2C25734C0068FAF5 /* Sources */ = { + isa = PBXGroup; + children = ( + 9C09176E2C2573580068FAF5 /* Transactions.swift */, + ); + path = Sources; + sourceTree = ""; + }; + 9C0917722C25739B0068FAF5 /* Utils */ = { + isa = PBXGroup; + children = ( + 9C0917702C25737C0068FAF5 /* PageableSource.swift */, + 9C09176B2C2466280068FAF5 /* PageInfo.swift */, + 9C0917732C2573B00068FAF5 /* Page.swift */, + ); + path = Utils; + sourceTree = ""; + }; + 9C0917752C25D1520068FAF5 /* Modifiers */ = { + isa = PBXGroup; + children = ( + 9C84F4152C2FF47200196EA0 /* Sending */, + 9C1C9D8F2C0F5DE40028C8FD /* Popover.swift */, + 9CDFD43F2BAD83E7000466B9 /* ThemedText.swift */, + 9C2D8D512C35887E008D6174 /* Placeholder.swift */, + ); + path = Modifiers; + sourceTree = ""; + }; + 9C0917772C25D1980068FAF5 /* Input */ = { + isa = PBXGroup; + children = ( + 9CD1BE892BCD4F5B0077A60B /* OTPInput.swift */, + 9CD778D12BC4423C00BA4677 /* PhoneInput.swift */, + 9C637CDF2BB0C0E5005816B4 /* Input.swift */, + ); + path = Input; + sourceTree = ""; + }; + 9C0917782C25D1BA0068FAF5 /* Avatar */ = { + isa = PBXGroup; + children = ( + 9C1C9D932C0FBA900028C8FD /* Avatar.swift */, + 9C6259202BFE56590039DE9C /* NoAvatar.swift */, + ); + path = Avatar; + sourceTree = ""; + }; + 9C0917792C25D2700068FAF5 /* Requesting */ = { + isa = PBXGroup; + children = ( + 9C09177A2C25D2810068FAF5 /* RequestingView.swift */, + 9C9098762C2AC9FE002A5833 /* RequestingAmountView.swift */, + ); + path = Requesting; + sourceTree = ""; + }; + 9C286B542C32E67700D27A9D /* Onramp */ = { + isa = PBXGroup; + children = ( + 9C286B552C32E84200D27A9D /* OnrampAmountView.swift */, + 9C286B572C32EB9B00D27A9D /* OnrampView.swift */, + 9CDAF8352C32F98600BCFEDF /* OnrampStripeView.swift */, + ); + path = Onramp; + sourceTree = ""; + }; + 9C6259042BF78CF20039DE9C /* Home */ = { + isa = PBXGroup; + children = ( + 9C2E73C22BF39635004FFFD1 /* BalanceView.swift */, + 9CD778B92BBCB89C00BA4677 /* TransferRow.swift */, + 9CD778AA2BBAF8BE00BA4677 /* HomeView.swift */, + ); + path = Home; + sourceTree = ""; + }; + 9C6259112BFB5A820039DE9C /* Sending */ = { + isa = PBXGroup; + children = ( + 9C6259182BFCDCEC0039DE9C /* NewRecipientView.swift */, + 9C6259122BFB5A990039DE9C /* SendingAmountView.swift */, + 9C6259142BFB5B9A0039DE9C /* SendingRecipientView.swift */, + 9C62591A2BFCF0650039DE9C /* SendingView.swift */, + 9C62591E2BFE56000039DE9C /* ContactRow.swift */, + ); + path = Sending; + sourceTree = ""; + }; 9C637CD72BB096F5005816B4 /* Managers */ = { isa = PBXGroup; children = ( @@ -201,7 +402,6 @@ children = ( 9C637CDD2BB0A564005816B4 /* AppIcon.swift */, 9CD778AF2BBC1A9C00BA4677 /* View+if.swift */, - 9CD778B72BBCB2A100BA4677 /* USDCAmount.swift */, 9CD778BF2BBF1E5C00BA4677 /* EdgeBorder.swift */, 9CD778C12BBF1E9200BA4677 /* View+border.swift */, 9CD778D32BC4426D00BA4677 /* View+placeholder.swift */, @@ -213,14 +413,86 @@ 9C4F451C2BDEEF0E00D44CBE /* Data+from.swift */, 9CE7A75A2BE12CBC008509FE /* Data+Hex.swift */, 9CD390EA2BE1977E00238FE9 /* HTTPURLReponse.swift */, + 9C62590B2BF9FFF60039DE9C /* Container.swift */, + 9C2E73C42BF3CB86004FFFD1 /* Background.swift */, + 9C62590D2BFA7D260039DE9C /* Icon.swift */, + 9C62590F2BFAADB90039DE9C /* NavigationBar.swift */, + 9C6259162BFB6A910039DE9C /* UIImage.swift */, + 9C1C9D832C0D093F0028C8FD /* P256Signer.swift */, + 9C1C9D852C0D0F320028C8FD /* Uint256.swift */, + 9C1C9D952C105E3D0028C8FD /* Amount.swift */, + 9CDAF83B2C346D7C00BCFEDF /* String+Email.swift */, + ); + path = Utils; + sourceTree = ""; + }; + 9C84F4152C2FF47200196EA0 /* Sending */ = { + isa = PBXGroup; + children = ( + 9C1C9D912C0FB4760028C8FD /* SendingConfirmationView.swift */, + 9C84F4162C2FF5E500196EA0 /* AddSendingConfirmation.swift */, + ); + path = Sending; + sourceTree = ""; + }; + 9C9F9EE32C22368200620AC6 /* Models */ = { + isa = PBXGroup; + children = ( + 9CDD91B82C24129E00EA20CC /* VaultPage.swift */, + 9C9F9EDC2C22368200620AC6 /* APIRequest.swift */, + 9C9F9EDD2C22368200620AC6 /* Balance.swift */, + 9C9F9EDE2C22368200620AC6 /* Deployment.swift */, + 9C9F9EDF2C22368200620AC6 /* Empty.swift */, + 9C9F9EE02C22368200620AC6 /* Error.swift */, + 9C9F9EE12C22368200620AC6 /* Execution.swift */, + 9C9F9EE22C22368200620AC6 /* VaultError.swift */, + 9CD0988B2C22E33800FDDD8F /* Transaction.swift */, + 9C59C9452C30B35D0074F23B /* User.swift */, + 9CDAF8412C355E4100BCFEDF /* FunkitStripeCheckoutQuote.swift */, + 9C2D8D552C35CA13008D6174 /* FunkitStripeCheckout.swift */, + ); + path = Models; + sourceTree = ""; + }; + 9C9F9EE82C22368200620AC6 /* Requests */ = { + isa = PBXGroup; + children = ( + 9C9F9EE42C22368200620AC6 /* ExecuteFromOutside.swift */, + 9C9F9EE62C22368200620AC6 /* GetOTP.swift */, + 9C9F9EE72C22368200620AC6 /* VerifyOTP.swift */, + 9CD098892C22E32300FDDD8F /* GetTransactionsHistory.swift */, + 9C59C9432C30B2790074F23B /* GetUser.swift */, + 9CDAF83F2C355CA100BCFEDF /* GetFunkitStripeCheckoutQuote.swift */, + 9C2D8D532C35A582008D6174 /* CreateFunkitStripeCheckout.swift */, + ); + path = Requests; + sourceTree = ""; + }; + 9C9F9EEC2C22368200620AC6 /* Utils */ = { + isa = PBXGroup; + children = ( + 9C9F9EE92C22368200620AC6 /* BodyParamsEncoder.swift */, + 9C9F9EEA2C22368200620AC6 /* HTTPParameter.swift */, + 9C9F9EEB2C22368200620AC6 /* URLQueryItemEncoder.swift */, ); path = Utils; sourceTree = ""; }; + 9C9F9EEE2C22368200620AC6 /* VaultAPI */ = { + isa = PBXGroup; + children = ( + 9C9F9EE32C22368200620AC6 /* Models */, + 9C9F9EE82C22368200620AC6 /* Requests */, + 9C9F9EEC2C22368200620AC6 /* Utils */, + 9C9F9EED2C22368200620AC6 /* VaultAPIClient.swift */, + ); + path = VaultAPI; + sourceTree = ""; + }; 9CD1BE8E2BD510420077A60B /* Services */ = { isa = PBXGroup; children = ( - 9CD1BE8F2BD53F7A0077A60B /* VaultService.swift */, + 9C9F9EEE2C22368200620AC6 /* VaultAPI */, ); path = Services; sourceTree = ""; @@ -235,29 +507,30 @@ 9CD778A92BBAF89F00BA4677 /* Core */ = { isa = PBXGroup; children = ( - 9CD778B12BBC38B700BA4677 /* Cards */, - 9CD778AA2BBAF8BE00BA4677 /* HomeView.swift */, - 9CD778C32BC1B63800BA4677 /* BudgetView.swift */, - 9CD390EC2BE3CF5D00238FE9 /* TransferView.swift */, + 9C286B542C32E67700D27A9D /* Onramp */, + 9C0917792C25D2700068FAF5 /* Requesting */, + 9C6259112BFB5A820039DE9C /* Sending */, + 9C6259042BF78CF20039DE9C /* Home */, + 9CD778B12BBC38B700BA4677 /* Budget */, 9CD390F62BEA3D2A00238FE9 /* EarnView.swift */, ); path = Core; sourceTree = ""; }; - 9CD778B12BBC38B700BA4677 /* Cards */ = { + 9CD778B12BBC38B700BA4677 /* Budget */ = { isa = PBXGroup; children = ( + 9CD778C32BC1B63800BA4677 /* BudgetView.swift */, 9CD778B22BBC38F400BA4677 /* HistoricalGraph.swift */, - 9CD778B92BBCB89C00BA4677 /* TransferRow.swift */, - 9C2E73C22BF39635004FFFD1 /* BalanceView.swift */, ); - path = Cards; + path = Budget; sourceTree = ""; }; 9CD778B42BBCB0CD00BA4677 /* Constants */ = { isa = PBXGroup; children = ( 9CD778B52BBCB0DE00BA4677 /* Constants.swift */, + 9C6ABFCF2C21D21A000227D2 /* Configuration.swift */, ); path = Constants; sourceTree = ""; @@ -265,6 +538,8 @@ 9CDFD3F22BAB3428000466B9 = { isa = PBXGroup; children = ( + 9C6ABFCE2C21D0CC000227D2 /* Staging.xcconfig */, + 9C6ABFCD2C21CF94000227D2 /* Dev.xcconfig */, 9CDFD4352BAB80D5000466B9 /* Vault-Info.plist */, 9CDFD3FD2BAB3428000466B9 /* App */, 9CDFD40E2BAB3429000466B9 /* VaultTests */, @@ -291,7 +566,7 @@ 9CD778B42BBCB0CD00BA4677 /* Constants */, 9C637CDC2BB0A4DA005816B4 /* Utils */, 9C637CD72BB096F5005816B4 /* Managers */, - 9CDFD4412BADA288000466B9 /* Model */, + 9CDFD4412BADA288000466B9 /* Models */, 9CDFD4362BAB86E6000466B9 /* Components */, 9CDFD4302BAB7F74000466B9 /* fonts */, 9CDFD4282BAB44E8000466B9 /* Navigation */, @@ -325,6 +600,7 @@ 9CDFD42B2BAB4CF8000466B9 /* Onboarding */, 9CDFD4002BAB3428000466B9 /* ContentView.swift */, 9CD778BB2BBED06700BA4677 /* CustomTabBar.swift */, + 9C6ABFCB2C21C9C9000227D2 /* ErrorView.swift */, ); path = Navigation; sourceTree = ""; @@ -335,7 +611,6 @@ 9CDFD42C2BAB4D1D000466B9 /* WelcomeView.swift */, 9CDFD42E2BAB7641000466B9 /* OnboardingView.swift */, 9CDFD4392BAC4561000466B9 /* AskSurnameView.swift */, - 9CD7789C2BB1846600BA4677 /* AccessCodeView.swift */, 9CDFD43B2BAC8BE6000466B9 /* FaceIDView.swift */, 9CDFD43D2BAD7F12000466B9 /* Shared.swift */, 9C637CD52BB09334005816B4 /* NotificationsView.swift */, @@ -357,32 +632,46 @@ 9CDFD4362BAB86E6000466B9 /* Components */ = { isa = PBXGroup; children = ( - 9CD778AD2BBC1A7300BA4677 /* GraphView.swift */, - 9CDFD4372BAB8934000466B9 /* Buttons.swift */, - 9CDFD43F2BAD83E7000466B9 /* ThemedText.swift */, - 9C637CDA2BB09C4D005816B4 /* Notification.swift */, - 9C637CDF2BB0C0E5005816B4 /* Input.swift */, - 9C5CFDA72BC73DB0001776E1 /* SearchBar.swift */, - 9CD778D12BC4423C00BA4677 /* PhoneInput.swift */, - 9C5CFDA52BC69446001776E1 /* CountryPickerView.swift */, - 9CD1BE892BCD4F5B0077A60B /* OTPInput.swift */, - 9CE7A75C2BE15C21008509FE /* SpinnerView.swift */, - 9CD390EE2BE5284000238FE9 /* AmountInput.swift */, + 9C0917752C25D1520068FAF5 /* Modifiers */, + 9C0917772C25D1980068FAF5 /* Input */, + 9C0917782C25D1BA0068FAF5 /* Avatar */, + 9C6259092BF79F8C0039DE9C /* NumPad.swift */, + 9C6259072BF79F140039DE9C /* FancyAmount.swift */, 9CD390FA2BEAB6C600238FE9 /* WebView.swift */, - 9C2E73C42BF3CB86004FFFD1 /* Background.swift */, + 9CE7A75C2BE15C21008509FE /* SpinnerView.swift */, + 9C5CFDA52BC69446001776E1 /* CountryPickerView.swift */, + 9C5CFDA72BC73DB0001776E1 /* SearchBar.swift */, + 9C637CDA2BB09C4D005816B4 /* Notification.swift */, + 9CDFD4372BAB8934000466B9 /* Buttons.swift */, + 9CD778AD2BBC1A7300BA4677 /* GraphView.swift */, + 9C9098782C2AED57002A5833 /* ActivityView.swift */, + 9C2D8D572C360036008D6174 /* Line.swift */, ); path = Components; sourceTree = ""; }; - 9CDFD4412BADA288000466B9 /* Model */ = { + 9CDFD4412BADA288000466B9 /* Models */ = { isa = PBXGroup; children = ( - 9CD778C52BC1DEEA00BA4677 /* NavigationModel.swift */, - 9C5CFDAD2BC828C9001776E1 /* PhoneNumberModel.swift */, - 9CD778A72BB2CE7D00BA4677 /* SettingsModel.swift */, - 9CD1BE952BD589AD0077A60B /* RegistrationModel.swift */, + 9CE837E12C241DE9005A740A /* Pagination */, + 9CD778C52BC1DEEA00BA4677 /* Navigation.swift */, + 9C5CFDAD2BC828C9001776E1 /* Model.swift */, + 9C62591C2BFE20AF0039DE9C /* Contact.swift */, + 9C1C9D8B2C0DEBAE0028C8FD /* CountryData.swift */, + 9CC82FF92C1BB9A30089042C /* OutsideExecution.swift */, + 9CD0988D2C22EBE500FDDD8F /* History.swift */, ); - path = Model; + path = Models; + sourceTree = ""; + }; + 9CE837E12C241DE9005A740A /* Pagination */ = { + isa = PBXGroup; + children = ( + 9C0917722C25739B0068FAF5 /* Utils */, + 9C09176D2C25734C0068FAF5 /* Sources */, + 9CE837E22C241DFA005A740A /* PaginationModel.swift */, + ); + path = Pagination; sourceTree = ""; }; /* End PBXGroup section */ @@ -526,40 +815,85 @@ buildActionMask = 2147483647; files = ( 9CD390EB2BE1977E00238FE9 /* HTTPURLReponse.swift in Sources */, + 9C9F9EFA2C22368200620AC6 /* BodyParamsEncoder.swift in Sources */, + 9C9F9EFC2C22368200620AC6 /* URLQueryItemEncoder.swift in Sources */, + 9C9F9EF42C22368200620AC6 /* Execution.swift in Sources */, 9C5CFDA62BC69446001776E1 /* CountryPickerView.swift in Sources */, - 9CD778A82BB2CE7D00BA4677 /* SettingsModel.swift in Sources */, - 9CD1BE962BD589AD0077A60B /* RegistrationModel.swift in Sources */, + 9C9F9EEF2C22368200620AC6 /* APIRequest.swift in Sources */, + 9C62590A2BF79F8C0039DE9C /* NumPad.swift in Sources */, 9CE7A75B2BE12CBC008509FE /* Data+Hex.swift in Sources */, 9CD778BC2BBED06700BA4677 /* CustomTabBar.swift in Sources */, 9C637CE02BB0C0E5005816B4 /* Input.swift in Sources */, 9CD778B02BBC1A9C00BA4677 /* View+if.swift in Sources */, + 9C1C9D862C0D0F320028C8FD /* Uint256.swift in Sources */, 9C5CFDA82BC73DB0001776E1 /* SearchBar.swift in Sources */, + 9C0917742C2573B00068FAF5 /* Page.swift in Sources */, 9CD7789F2BB185C200BA4677 /* CelebrationView.swift in Sources */, + 9CDAF8402C355CA100BCFEDF /* GetFunkitStripeCheckoutQuote.swift in Sources */, 9CD390FB2BEAB6C600238FE9 /* WebView.swift in Sources */, + 9CDAF83C2C346D7C00BCFEDF /* String+Email.swift in Sources */, + 9C09176F2C2573580068FAF5 /* Transactions.swift in Sources */, + 9C1C9D962C105E3D0028C8FD /* Amount.swift in Sources */, 9CD778C22BBF1E9200BA4677 /* View+border.swift in Sources */, + 9C6259172BFB6A910039DE9C /* UIImage.swift in Sources */, + 9C1C9D842C0D093F0028C8FD /* P256Signer.swift in Sources */, + 9C9F9EF52C22368200620AC6 /* VaultError.swift in Sources */, + 9C9F9EFB2C22368200620AC6 /* HTTPParameter.swift in Sources */, + 9C59C9442C30B2790074F23B /* GetUser.swift in Sources */, + 9C9F9EF32C22368200620AC6 /* Error.swift in Sources */, + 9C9F9EF12C22368200620AC6 /* Deployment.swift in Sources */, + 9CD0988E2C22EBE500FDDD8F /* History.swift in Sources */, + 9C9F9EF02C22368200620AC6 /* Balance.swift in Sources */, + 9C2D8D582C360036008D6174 /* Line.swift in Sources */, + 9C1C9D8C2C0DEBAE0028C8FD /* CountryData.swift in Sources */, + 9C09177B2C25D2810068FAF5 /* RequestingView.swift in Sources */, + 9C286B562C32E84200D27A9D /* OnrampAmountView.swift in Sources */, 9CDFD4382BAB8934000466B9 /* Buttons.swift in Sources */, + 9C2D8D562C35CA13008D6174 /* FunkitStripeCheckout.swift in Sources */, + 9C1C9D902C0F5DE40028C8FD /* Popover.swift in Sources */, + 9C1C9D942C0FBA900028C8FD /* Avatar.swift in Sources */, + 9C62591B2BFCF0650039DE9C /* SendingView.swift in Sources */, 9CDFD4432BADA297000466B9 /* BiometricAuthManager.swift in Sources */, 9CD778B62BBCB0DE00BA4677 /* Constants.swift in Sources */, 9CE7A75D2BE15C21008509FE /* SpinnerView.swift in Sources */, + 9C2D8D542C35A582008D6174 /* CreateFunkitStripeCheckout.swift in Sources */, 9C637CD92BB09710005816B4 /* NotificationsManager.swift in Sources */, + 9C9F9EF92C22368200620AC6 /* VerifyOTP.swift in Sources */, + 9C59C9462C30B35D0074F23B /* User.swift in Sources */, + 9C6259132BFB5A990039DE9C /* SendingAmountView.swift in Sources */, + 9C6259192BFCDCEC0039DE9C /* NewRecipientView.swift in Sources */, 9C4F451D2BDEEF0E00D44CBE /* Data+from.swift in Sources */, 9CD778D22BC4423C00BA4677 /* PhoneInput.swift in Sources */, 9CDFD4012BAB3428000466B9 /* ContentView.swift in Sources */, 9CD778B32BBC38F400BA4677 /* HistoricalGraph.swift in Sources */, + 9C286B582C32EB9B00D27A9D /* OnrampView.swift in Sources */, + 9C9F9EF82C22368200620AC6 /* GetOTP.swift in Sources */, 9C637CDE2BB0A564005816B4 /* AppIcon.swift in Sources */, + 9C6ABFCC2C21C9C9000227D2 /* ErrorView.swift in Sources */, + 9C9098772C2AC9FE002A5833 /* RequestingAmountView.swift in Sources */, 9CD1BE8C2BCD5D790077A60B /* String+character.swift in Sources */, - 9CD778B82BBCB2A100BA4677 /* USDCAmount.swift in Sources */, 9C637CD62BB09334005816B4 /* NotificationsView.swift in Sources */, 9CD778AB2BBAF8BE00BA4677 /* HomeView.swift in Sources */, + 9CC82FFA2C1BB9A30089042C /* OutsideExecution.swift in Sources */, + 9C6259102BFAADB90039DE9C /* NavigationBar.swift in Sources */, + 9C9F9EF22C22368200620AC6 /* Empty.swift in Sources */, 9CD778C42BC1B63800BA4677 /* BudgetView.swift in Sources */, 9C5CFDB02BC828DE001776E1 /* Locale.swift in Sources */, - 9CD7789D2BB1846600BA4677 /* AccessCodeView.swift in Sources */, + 9C9F9EF62C22368200620AC6 /* ExecuteFromOutside.swift in Sources */, + 9C09176C2C2466280068FAF5 /* PageInfo.swift in Sources */, 9CD778CB2BC3113A00BA4677 /* PhoneRequestView.swift in Sources */, 9CDFD42F2BAB7641000466B9 /* OnboardingView.swift in Sources */, 9CD390F72BEA3D2A00238FE9 /* EarnView.swift in Sources */, + 9CDD91B92C24129E00EA20CC /* VaultPage.swift in Sources */, + 9CE837E32C241DFA005A740A /* PaginationModel.swift in Sources */, 9CD778CD2BC3115E00BA4677 /* PhoneValidationView.swift in Sources */, + 9C6ABFD02C21D21A000227D2 /* Configuration.swift in Sources */, 9C4F451B2BDE999000D44CBE /* String+Error.swift in Sources */, + 9CD0988C2C22E33800FDDD8F /* Transaction.swift in Sources */, 9CDFD42D2BAB4D1D000466B9 /* WelcomeView.swift in Sources */, + 9C62590C2BF9FFF60039DE9C /* Container.swift in Sources */, + 9C2D8D522C35887E008D6174 /* Placeholder.swift in Sources */, + 9C6259152BFB5B9A0039DE9C /* SendingRecipientView.swift in Sources */, 9C637CDB2BB09C4D005816B4 /* Notification.swift in Sources */, 9CDFD4402BAD83E7000466B9 /* ThemedText.swift in Sources */, 9CD1BE8A2BCD4F5B0077A60B /* OTPInput.swift in Sources */, @@ -567,20 +901,30 @@ 9CD778C02BBF1E5C00BA4677 /* EdgeBorder.swift in Sources */, 9CDFD43C2BAC8BE6000466B9 /* FaceIDView.swift in Sources */, 9C4F45192BDE949D00D44CBE /* SecureEnclaveManager.swift in Sources */, - 9CD390EF2BE5284000238FE9 /* AmountInput.swift in Sources */, + 9C6259212BFE56590039DE9C /* NoAvatar.swift in Sources */, + 9C84F4172C2FF5E500196EA0 /* AddSendingConfirmation.swift in Sources */, + 9C62591D2BFE20AF0039DE9C /* Contact.swift in Sources */, + 9C62590E2BFA7D260039DE9C /* Icon.swift in Sources */, 9C2E73C52BF3CB86004FFFD1 /* Background.swift in Sources */, 9C5CFDB22BC82C57001776E1 /* Array+indexed.swift in Sources */, + 9C0917712C25737C0068FAF5 /* PageableSource.swift in Sources */, 9CD778D42BC4426D00BA4677 /* View+placeholder.swift in Sources */, 9CDFD43A2BAC4561000466B9 /* AskSurnameView.swift in Sources */, 9C2E73C32BF39635004FFFD1 /* BalanceView.swift in Sources */, - 9CD390ED2BE3CF5D00238FE9 /* TransferView.swift in Sources */, 9CD778BA2BBCB89C00BA4677 /* TransferRow.swift in Sources */, - 9C5CFDAE2BC828C9001776E1 /* PhoneNumberModel.swift in Sources */, + 9C6259082BF79F140039DE9C /* FancyAmount.swift in Sources */, + 9C62591F2BFE56000039DE9C /* ContactRow.swift in Sources */, + 9C5CFDAE2BC828C9001776E1 /* Model.swift in Sources */, + 9CDAF8362C32F98600BCFEDF /* OnrampStripeView.swift in Sources */, + 9CD0988A2C22E32300FDDD8F /* GetTransactionsHistory.swift in Sources */, + 9C9098792C2AED57002A5833 /* ActivityView.swift in Sources */, 9CD1BE982BD7C51A0077A60B /* PhoneNumber+Parse.swift in Sources */, + 9CDAF8422C355E4100BCFEDF /* FunkitStripeCheckoutQuote.swift in Sources */, + 9C1C9D922C0FB4760028C8FD /* SendingConfirmationView.swift in Sources */, + 9C9F9EFD2C22368200620AC6 /* VaultAPIClient.swift in Sources */, 9CDFD43E2BAD7F12000466B9 /* Shared.swift in Sources */, 9CDFD3FF2BAB3428000466B9 /* VaultApp.swift in Sources */, - 9CD1BE902BD53F7A0077A60B /* VaultService.swift in Sources */, - 9CD778C62BC1DEEA00BA4677 /* NavigationModel.swift in Sources */, + 9CD778C62BC1DEEA00BA4677 /* Navigation.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -619,6 +963,7 @@ /* Begin XCBuildConfiguration section */ 9CD1BE912BD563710077A60B /* Debug(Staging) */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 9C6ABFCE2C21D0CC000227D2 /* Staging.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; @@ -693,13 +1038,17 @@ INFOPLIST_FILE = "Vault-Info.plist"; INFOPLIST_KEY_CFBundleDisplayName = Vault; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.finance"; + INFOPLIST_KEY_NSCameraUsageDescription = "We need access to the camera for the KYC process during the onramp"; + INFOPLIST_KEY_NSContactsUsageDescription = "We need access to your contacts to show them in the app."; INFOPLIST_KEY_NSFaceIDUsageDescription = "Authenticate to access your secure data."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UIRequiresFullScreen = YES; INFOPLIST_KEY_UIStatusBarStyle = ""; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -753,6 +1102,7 @@ }; 9CDFD41D2BAB3429000466B9 /* Debug(Development) */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 9C6ABFCD2C21CF94000227D2 /* Dev.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; @@ -816,6 +1166,7 @@ }; 9CDFD41E2BAB3429000466B9 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 9C6ABFCE2C21D0CC000227D2 /* Staging.xcconfig */; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; @@ -883,13 +1234,17 @@ INFOPLIST_FILE = "Vault-Info.plist"; INFOPLIST_KEY_CFBundleDisplayName = Vault; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.finance"; + INFOPLIST_KEY_NSCameraUsageDescription = "We need access to the camera for the KYC process during the onramp"; + INFOPLIST_KEY_NSContactsUsageDescription = "We need access to your contacts to show them in the app."; INFOPLIST_KEY_NSFaceIDUsageDescription = "Authenticate to access your secure data."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UIRequiresFullScreen = YES; INFOPLIST_KEY_UIStatusBarStyle = ""; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -916,13 +1271,17 @@ INFOPLIST_FILE = "Vault-Info.plist"; INFOPLIST_KEY_CFBundleDisplayName = Vault; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.finance"; + INFOPLIST_KEY_NSCameraUsageDescription = "We need access to the camera for the KYC process during the onramp"; + INFOPLIST_KEY_NSContactsUsageDescription = "We need access to your contacts to show them in the app."; INFOPLIST_KEY_NSFaceIDUsageDescription = "Authenticate to access your secure data."; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UIRequiresFullScreen = YES; INFOPLIST_KEY_UIStatusBarStyle = ""; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/app/Vault.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/app/Vault.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 00000000..768db7e4 --- /dev/null +++ b/app/Vault.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,51 @@ +{ + "originHash" : "1fbc22d98a38f7670b283d4e1502f232da4fa7b0bcb28be022908ef3d3a31380", + "pins" : [ + { + "identity" : "bigint", + "kind" : "remoteSourceControl", + "location" : "https://github.com/attaswift/BigInt.git", + "state" : { + "revision" : "0ed110f7555c34ff468e72e1686e59721f2b0da6", + "version" : "5.3.0" + } + }, + { + "identity" : "confettiswiftui", + "kind" : "remoteSourceControl", + "location" : "https://github.com/simibac/ConfettiSwiftUI.git", + "state" : { + "revision" : "f45961f97bbae6fff6e2e64546fea0189425ad92", + "version" : "1.1.0" + } + }, + { + "identity" : "cryptoswift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/krzyzanowskim/CryptoSwift.git", + "state" : { + "revision" : "039f56c5d7960f277087a0be51f5eb04ed0ec073", + "version" : "1.5.1" + } + }, + { + "identity" : "phonenumberkit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/marmelroy/PhoneNumberKit", + "state" : { + "revision" : "ee5d7114934e60812c9b47c333f01b67d002be2d", + "version" : "3.7.10" + } + }, + { + "identity" : "starknet.swift", + "kind" : "remoteSourceControl", + "location" : "https://github.com/software-mansion/starknet.swift", + "state" : { + "branch" : "main", + "revision" : "4e20de5a4117e951bbd3aec28413d33d4620c560" + } + } + ], + "version" : 3 +} diff --git a/app/Vault.xcodeproj/xcshareddata/xcschemes/Vault(Development).xcscheme b/app/Vault.xcodeproj/xcshareddata/xcschemes/Vault(Development).xcscheme index 87e8f7d6..ebd5fc33 100644 --- a/app/Vault.xcodeproj/xcshareddata/xcschemes/Vault(Development).xcscheme +++ b/app/Vault.xcodeproj/xcshareddata/xcschemes/Vault(Development).xcscheme @@ -74,13 +74,6 @@ ReferencedContainer = "container:Vault.xcodeproj"> - - - - - - - -