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">
-
-
-
-
-
-
-
-