Skip to content
This repository was archived by the owner on Mar 26, 2026. It is now read-only.

Commit 7298393

Browse files
committed
Add "Add to Contacts" feature from transaction and transfer screens
- Add context menu option to add recipient address to contacts - Reuse existing ContactsScene with Mode enum for add address flow - Hide "Add to Contact" option when address already has a name - Add ContactServiceTestKit for testing - Update localization
1 parent 5dbc4de commit 7298393

64 files changed

Lines changed: 356 additions & 106 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Features/Contacts/Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ let package = Package(
5353
"Primitives",
5454
"PrimitivesComponents",
5555
.product(name: "PrimitivesTestKit", package: "Primitives"),
56-
.product(name: "StoreTestKit", package: "Store"),
5756
.product(name: "ContactService", package: "FeatureServices"),
57+
.product(name: "ContactServiceTestKit", package: "FeatureServices"),
5858
]
5959
),
6060
]

Features/Contacts/Sources/Scenes/ContactsScene.swift

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public struct ContactsScene: View {
2121
ForEach(model.contacts) { contact in
2222
NavigationCustomLink(
2323
with: ListItemView(model: model.listItemModel(for: contact)),
24-
action: { model.isPresentingContact = contact }
24+
action: { model.onSelectContact(contact) }
2525
)
2626
}
2727
.onDelete(perform: model.deleteContacts)
@@ -39,27 +39,16 @@ public struct ContactsScene: View {
3939
.navigationTitle(model.title)
4040
.toolbar {
4141
ToolbarItem(placement: .primaryAction) {
42-
Button {
43-
model.isPresentingAddContact = true
44-
} label: {
42+
Button(action: model.onSelectAddContact) {
4543
Images.System.plus
4644
}
4745
}
4846
}
49-
.sheet(isPresented: $model.isPresentingAddContact) {
47+
.sheet(item: $model.isPresentingContact) {
5048
ManageContactNavigationStack(
5149
model: ManageContactViewModel(
5250
service: model.service,
53-
mode: .add,
54-
onComplete: model.onAddContactComplete
55-
)
56-
)
57-
}
58-
.sheet(item: $model.isPresentingContact) { contact in
59-
ManageContactNavigationStack(
60-
model: ManageContactViewModel(
61-
service: model.service,
62-
mode: .edit(contact),
51+
mode: $0,
6352
onComplete: model.onManageContactComplete
6453
)
6554
)

Features/Contacts/Sources/Scenes/ManageContactNavigationStack.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@ import SwiftUI
44
import Primitives
55
import Components
66

7-
struct ManageContactNavigationStack: View {
7+
public struct ManageContactNavigationStack: View {
88

99
@State private var model: ManageContactViewModel
1010

11-
init(model: ManageContactViewModel) {
11+
public init(model: ManageContactViewModel) {
1212
_model = State(initialValue: model)
1313
}
1414

15-
var body: some View {
15+
public var body: some View {
1616
NavigationStack {
1717
ManageContactScene(model: $model)
1818
.toolbarDismissItem(type: .close, placement: .cancellationAction)

Features/Contacts/Sources/Scenes/ManageContactScene.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,9 @@ extension ManageContactScene {
111111
private func onSave() {
112112
focusedField = .none
113113
model.onSave()
114-
dismiss()
114+
if model.shouldDismissOnSave {
115+
dismiss()
116+
}
115117
}
116118
}
117119

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright (c). Gem Wallet. All rights reserved.
2+
3+
import Foundation
4+
import Primitives
5+
6+
public struct AddAddressInput: Sendable {
7+
public let chain: Chain
8+
public let address: String
9+
public let memo: String?
10+
public let name: String?
11+
12+
public init(
13+
chain: Chain,
14+
address: String,
15+
memo: String? = nil,
16+
name: String? = nil
17+
) {
18+
self.chain = chain
19+
self.address = address
20+
self.memo = memo
21+
self.name = name
22+
}
23+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
// Copyright (c). Gem Wallet. All rights reserved.
2+
3+
import Foundation
4+
import Primitives
5+
6+
extension ManageContactViewModel {
7+
public enum Mode: Identifiable {
8+
case add
9+
case create(AddAddressInput)
10+
case append(ContactData)
11+
case edit(ContactData)
12+
13+
public var id: String {
14+
switch self {
15+
case .add: "add"
16+
case .create(let input): "create-\(input.chain.rawValue)-\(input.address)"
17+
case .append(let contactData): "append-\(contactData.contact.id)"
18+
case .edit(let contactData): "edit-\(contactData.contact.id)"
19+
}
20+
}
21+
22+
var contact: Contact? {
23+
switch self {
24+
case .add, .create: nil
25+
case .append(let contactData), .edit(let contactData): contactData.contact
26+
}
27+
}
28+
}
29+
}

Features/Contacts/Sources/ViewModels/ContactsViewModel.swift

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,23 @@ import Localization
1111
@Observable
1212
@MainActor
1313
public final class ContactsViewModel {
14+
15+
public enum Mode {
16+
case view
17+
case addAddress(AddAddressInput, onComplete: () -> Void)
18+
}
19+
1420
let service: ContactService
21+
let mode: Mode
1522

1623
public let query: ObservableQuery<ContactsRequest>
1724
var contacts: [ContactData] { query.value }
1825

19-
var isPresentingContact: ContactData?
20-
var isPresentingAddContact = false
26+
var isPresentingContact: ManageContactViewModel.Mode?
2127

22-
public init(service: ContactService) {
28+
public init(service: ContactService, mode: Mode) {
2329
self.service = service
30+
self.mode = mode
2431
self.query = ObservableQuery(ContactsRequest(), initialValue: [])
2532
}
2633

@@ -38,12 +45,37 @@ public final class ContactsViewModel {
3845
)
3946
}
4047

41-
func onAddContactComplete() {
42-
isPresentingAddContact = false
48+
func onSelectAddContact() {
49+
switch mode {
50+
case .view:
51+
isPresentingContact = .add
52+
case .addAddress(let input, _):
53+
isPresentingContact = .create(input)
54+
}
55+
}
56+
57+
func onSelectContact(_ contact: ContactData) {
58+
switch mode {
59+
case .view:
60+
isPresentingContact = .edit(contact)
61+
case .addAddress(let input, _):
62+
let newAddress = ContactAddress.new(
63+
contactId: contact.contact.id,
64+
chain: input.chain,
65+
address: input.address,
66+
memo: input.memo
67+
)
68+
isPresentingContact = .append(ContactData(contact: contact.contact, addresses: contact.addresses + [newAddress]))
69+
}
4370
}
4471

4572
func onManageContactComplete() {
46-
isPresentingContact = nil
73+
switch mode {
74+
case .view:
75+
isPresentingContact = nil
76+
case .addAddress(_, let onComplete):
77+
onComplete()
78+
}
4779
}
4880

4981
func deleteContacts(at offsets: IndexSet) {

Features/Contacts/Sources/ViewModels/ManageContactViewModel.swift

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,6 @@ import Formatters
1717
@MainActor
1818
public final class ManageContactViewModel {
1919

20-
public enum Mode {
21-
case add
22-
case edit(ContactData)
23-
24-
var contact: Contact? {
25-
switch self {
26-
case .add: nil
27-
case .edit(let contactData): contactData.contact
28-
}
29-
}
30-
}
31-
3220
private let service: ContactService
3321
private let mode: Mode
3422
private let onComplete: (() -> Void)?
@@ -58,7 +46,18 @@ public final class ManageContactViewModel {
5846
switch mode {
5947
case .add:
6048
self.contactId = UUID().uuidString
61-
case .edit(let contactData):
49+
case .create(let input):
50+
self.contactId = UUID().uuidString
51+
self.nameInputModel.text = input.name ?? ""
52+
self.addresses = [
53+
ContactAddress.new(
54+
contactId: contactId,
55+
chain: input.chain,
56+
address: input.address,
57+
memo: input.memo
58+
)
59+
]
60+
case .append(let contactData), .edit(let contactData):
6261
self.contactId = contactData.contact.id
6362
self.nameInputModel.text = contactData.contact.name
6463
self.description = contactData.contact.description ?? ""
@@ -70,8 +69,8 @@ public final class ManageContactViewModel {
7069

7170
var isAddMode: Bool {
7271
switch mode {
73-
case .add: true
74-
case .edit: false
72+
case .add, .create: true
73+
case .append, .edit: false
7574
}
7675
}
7776
var buttonTitle: String { Localized.Common.save }
@@ -80,6 +79,13 @@ public final class ManageContactViewModel {
8079
var contactSectionTitle: String { Localized.Contacts.contact }
8180
var addressesSectionTitle: String { Localized.Contacts.addresses }
8281

82+
var shouldDismissOnSave: Bool {
83+
switch mode {
84+
case .add, .edit: true
85+
case .create, .append: false
86+
}
87+
}
88+
8389
var buttonState: ButtonState {
8490
guard nameInputModel.isValid,
8591
nameInputModel.text.isNotEmpty,
@@ -126,8 +132,8 @@ public final class ManageContactViewModel {
126132
func onSave() {
127133
do {
128134
switch mode {
129-
case .add: try service.addContact(currentContact, addresses: addresses)
130-
case .edit: try service.updateContact(currentContact, addresses: addresses)
135+
case .add, .create: try service.addContact(currentContact, addresses: addresses)
136+
case .append, .edit: try service.updateContact(currentContact, addresses: addresses)
131137
}
132138
onComplete?()
133139
} catch {

Features/Contacts/Tests/ContactsTests/ManageContactViewModelTests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
import Testing
44
import Primitives
55
import PrimitivesTestKit
6-
import StoreTestKit
76
import ContactService
7+
import ContactServiceTestKit
88
import Components
99

1010
@testable import Contacts
@@ -48,7 +48,7 @@ struct ManageContactViewModelTests {
4848
extension ManageContactViewModel {
4949
static func mock(mode: Mode) -> ManageContactViewModel {
5050
ManageContactViewModel(
51-
service: ContactService(store: .mock(), addressStore: .mock()),
51+
service: .mock(),
5252
mode: mode,
5353
onComplete: nil
5454
)

Features/Transactions/Package.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ let package = Package(
2424
.package(name: "ChainServices", path: "../../Packages/ChainServices"),
2525
.package(name: "FeatureServices", path: "../../Packages/FeatureServices"),
2626
.package(name: "InfoSheet", path: "../InfoSheet"),
27+
.package(name: "Contacts", path: "../Contacts"),
2728
],
2829
targets: [
2930
.target(
@@ -39,7 +40,9 @@ let package = Package(
3940
.product(name: "TransactionsService", package: "FeatureServices"),
4041
.product(name: "WalletService", package: "FeatureServices"),
4142
"Preferences",
42-
"InfoSheet"
43+
"InfoSheet",
44+
"Contacts",
45+
.product(name: "ContactService", package: "FeatureServices")
4346
],
4447
path: "Sources"
4548
),
@@ -48,6 +51,8 @@ let package = Package(
4851
dependencies: [
4952
.product(name: "PrimitivesTestKit", package: "Primitives"),
5053
.product(name: "PreferencesTestKit", package: "Preferences"),
54+
.product(name: "ContactService", package: "FeatureServices"),
55+
.product(name: "ContactServiceTestKit", package: "FeatureServices"),
5156
"Transactions",
5257
"PrimitivesComponents"
5358
]

0 commit comments

Comments
 (0)