Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ swiftformat .

### MVVM with Observable Pattern
The app uses iOS 17's `@Observable` macro for state management with clean separation between:
- **Views**: SwiftUI views (99% SwiftUI, UIKit only for mail view)
- **Views**: SwiftUI views
- **ViewModels**: Observable state containers that bridge views and data
- **Models**: Domain objects and data structures
- **Repositories**: Data access layer implementing CRUD operations
Expand Down
16 changes: 0 additions & 16 deletions NativeAppTemplate.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,6 @@
01E0A5B725BD0FCD00298D35 /* OfflineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01E0A5B225BD0FC700298D35 /* OfflineView.swift */; };
01E0A5B825BD0FCD00298D35 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01E0A5B325BD0FC700298D35 /* ErrorView.swift */; };
01E0A60125BD149200298D35 /* MainButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01E0A5F925BD148800298D35 /* MainButtonView.swift */; };
A1B2C3D401000001 /* GlassButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D401000002 /* GlassButtonStyle.swift */; };
A1B2C3D401000003 /* GlassCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = A1B2C3D401000004 /* GlassCard.swift */; };
01E0A60C25BD440300298D35 /* SignInEmailAndPasswordView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01E0A60B25BD440300298D35 /* SignInEmailAndPasswordView.swift */; };
01E0A63025BD53FD00298D35 /* Shop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01E0A62F25BD53FD00298D35 /* Shop.swift */; };
Expand All @@ -166,7 +165,6 @@
01E727212B020ECC004AC043 /* Bundle+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01E727202B020ECC004AC043 /* Bundle+Extensions.swift */; };
01ED197B2A037B9E00CD4735 /* AppTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01ED197A2A037B9E00CD4735 /* AppTabView.swift */; };
01EE363E29A6DCEB009BCD9D /* ShopkeeperEditView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01EE363D29A6DCEB009BCD9D /* ShopkeeperEditView.swift */; };
01FA23A12B00CE5700F1D446 /* MailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01FA23A02B00CE5700F1D446 /* MailView.swift */; };
01FC03E22B3329B700E6CD8E /* NeedAppUpdatesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01FC03E12B3329B700E6CD8E /* NeedAppUpdatesView.swift */; };
7249A60C06FE44338E16BC50 /* CertificatePinningDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C597C0551370446BB931F19B /* CertificatePinningDelegate.swift */; };
/* End PBXBuildFile section */
Expand Down Expand Up @@ -332,7 +330,6 @@
01E0A5B325BD0FC700298D35 /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = "<group>"; };
01E0A5B525BD0FC700298D35 /* LoadingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = "<group>"; };
01E0A5F925BD148800298D35 /* MainButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainButtonView.swift; sourceTree = "<group>"; };
A1B2C3D401000002 /* GlassButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlassButtonStyle.swift; sourceTree = "<group>"; };
A1B2C3D401000004 /* GlassCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlassCard.swift; sourceTree = "<group>"; };
01E0A60B25BD440300298D35 /* SignInEmailAndPasswordView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInEmailAndPasswordView.swift; sourceTree = "<group>"; };
01E0A62F25BD53FD00298D35 /* Shop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shop.swift; sourceTree = "<group>"; };
Expand All @@ -342,7 +339,6 @@
01E727202B020ECC004AC043 /* Bundle+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+Extensions.swift"; sourceTree = "<group>"; };
01ED197A2A037B9E00CD4735 /* AppTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppTabView.swift; sourceTree = "<group>"; };
01EE363D29A6DCEB009BCD9D /* ShopkeeperEditView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShopkeeperEditView.swift; sourceTree = "<group>"; };
01FA23A02B00CE5700F1D446 /* MailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MailView.swift; sourceTree = "<group>"; };
01FC03E12B3329B700E6CD8E /* NeedAppUpdatesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NeedAppUpdatesView.swift; sourceTree = "<group>"; };
C597C0551370446BB931F19B /* CertificatePinningDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CertificatePinningDelegate.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
Expand Down Expand Up @@ -492,14 +488,6 @@
path = "Shop List";
sourceTree = "<group>";
};
016EF40325E1E6630038195C /* UIKit */ = {
isa = PBXGroup;
children = (
01FA23A02B00CE5700F1D446 /* MailView.swift */,
);
path = UIKit;
sourceTree = "<group>";
};
0172030625A9642D008FD63B /* Networking */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -674,7 +662,6 @@
01011B542864434900B70D04 /* Shop Detail */,
0158B9F525C16366008EC9D5 /* Shop List */,
01467355299901E50005423D /* Shop Settings */,
016EF40325E1E6630038195C /* UIKit */,
);
path = UI;
sourceTree = "<group>";
Expand Down Expand Up @@ -832,7 +819,6 @@
isa = PBXGroup;
children = (
0172788A2D7D936E00CE424F /* Tags */,
A1B2C3D401000002 /* GlassButtonStyle.swift */,
A1B2C3D401000004 /* GlassCard.swift */,
01E0A5F925BD148800298D35 /* MainButtonView.swift */,
);
Expand Down Expand Up @@ -1017,7 +1003,6 @@
017278832D7D935700CE424F /* ImageSaver.swift in Sources */,
01D8AE8B2AB453C1009AFFBA /* ShopBasicSettingsView.swift in Sources */,
01E0A60125BD149200298D35 /* MainButtonView.swift in Sources */,
A1B2C3D401000001 /* GlassButtonStyle.swift in Sources */,
A1B2C3D401000003 /* GlassCard.swift in Sources */,
0182D39A25B4424B001E881D /* LoggedInShopkeeperKeychainStore.swift in Sources */,
01ED197B2A037B9E00CD4735 /* AppTabView.swift in Sources */,
Expand Down Expand Up @@ -1070,7 +1055,6 @@
0172787B2D7D903500CE424F /* ItemTagAdapter.swift in Sources */,
010F86AE2621A2A900B6C62A /* ShopDetailView.swift in Sources */,
011F6DF1259EF16400BED22E /* App.swift in Sources */,
01FA23A12B00CE5700F1D446 /* MailView.swift in Sources */,
017278092D7D4F7400CE424F /* Onboarding.swift in Sources */,
01467357299902230005423D /* ShopSettingsView.swift in Sources */,
017278792D7D900100CE424F /* ItemTagsRequest.swift in Sources */,
Expand Down
3 changes: 0 additions & 3 deletions NativeAppTemplate/Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,6 @@ extension String {
static let supportWebsiteUrl: String = "https://nativeapptemplate.com"
static let howToUseUrl: String = "https://myturntag.com/how"
static let faqsUrl: String = "https://nativeapptemplate.com/faqs"
static let discussionsUrl: String = "https://github.com/nativeapptemplate/NativeAppTemplate-Free-iOS/discussions"
static let privacyPolicyUrl: String = "https://nativeapptemplate.com/privacy"
static let termsOfUseUrl: String = "https://nativeapptemplate.com/terms"

Expand All @@ -225,9 +224,7 @@ extension String {
static let supportWebsite = "Support Website"
static let howToUse = "How To Use"
static let faqs = "FAQs"
static let discussions = "Discussions"
static let rateApp = "Rate or Review the App"
static let emailUs = "Email Us"
static let contact = "Contact"
static let privacyPolicy = "Privacy Policy"
static let termsOfUse = "Terms of Use"
Expand Down
55 changes: 29 additions & 26 deletions NativeAppTemplate/UI/Settings/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@
// NativeAppTemplate
//

import MessageUI
import StoreKit
import SwiftUI

struct SettingsView: View {
@Environment(DataManager.self) private var dataManager
@Environment(MessageBus.self) private var messageBus
@Environment(TabViewModel.self) private var tabViewModel
@Environment(\.requestReview) private var requestReview
@State private var viewModel: SettingsViewModel

init(
Expand Down Expand Up @@ -64,18 +65,13 @@ struct SettingsView: View {
Label(String.faqs, systemImage: "questionmark")
}

Link(destination: URL(string: String.discussionsUrl)!) {
Label(String.discussions, systemImage: "bubble.left.and.bubble.right")
Link(destination: supportEmailURL) {
Label(String.contact, systemImage: "envelope")
}

Button {
MFMailComposeViewController.canSendMail() ? viewModel.isShowingMailView.toggle() : viewModel
.alertNoMail.toggle()
requestReview()
} label: {
Label(String.contact, systemImage: "envelope")
}

Link(destination: URL(string: "\(String.appStoreUrl)?action=write-review")!) {
Label(String.rateApp, systemImage: "hand.thumbsup")
}

Expand Down Expand Up @@ -111,22 +107,29 @@ struct SettingsView: View {
}
.navigationTitle(String.settings)
.navigationBarTitleDisplayMode(.inline)
.sheet(isPresented: $viewModel.isShowingMailView) {
let systemVersion = UIDevice.current.systemVersion
let device = Utility.deviceModel

MailView(
result: $viewModel.result,
recipients: [String.supportMail],
subject: "\(Bundle.main.displayName) for iPhone support",
messageBody: "\n\n\n-----\n\(Bundle.main.displayName) " +
"\(Bundle.main.appVersionLong)\n\(device) " +
"(\(systemVersion))\n\(Locale.preferredLanguages[0])"
)
}
.alert(
"NO MAIL SETUP",
isPresented: $viewModel.alertNoMail
) {}
}

var supportEmailURL: URL {
let appName = Bundle.main.displayName
let appVersion = "\(Bundle.main.appVersionLong)"
let device = Utility.deviceModel
let systemVersion = UIDevice.current.systemVersion
let locale = Locale.current
let region = locale.region?.identifier ?? "Unknown"
let language = locale.language.languageCode?.identifier ?? "Unknown"

let body = """


---
App: \(appName) \(appVersion)
Device: \(device)
iOS: \(systemVersion)
Region: \(region)
Locale: \(language)-\(region)
"""

let encodedBody = body.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? ""
return URL(string: "mailto:\(String.supportMail)?body=\(encodedBody)")!
}
}
5 changes: 0 additions & 5 deletions NativeAppTemplate/UI/Settings/SettingsViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,11 @@
// NativeAppTemplate
//

import MessageUI
import Observation
import SwiftUI

@Observable
@MainActor
final class SettingsViewModel {
var isShowingMailView = false
var alertNoMail = false
var result: Result<MFMailComposeResult, Error>?
private(set) var messageBus: MessageBus

private let sessionController: SessionControllerProtocol
Expand Down
50 changes: 0 additions & 50 deletions NativeAppTemplate/UI/Shared/GlassButtonStyle.swift

This file was deleted.

72 changes: 0 additions & 72 deletions NativeAppTemplate/UI/UIKit/MailView.swift

This file was deleted.

22 changes: 0 additions & 22 deletions NativeAppTemplateTests/UI/Settings/SettingsViewModelTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,6 @@ struct SettingsViewModelTest {
messageBus: messageBus
)

#expect(viewModel.isShowingMailView == false)
#expect(viewModel.alertNoMail == false)
#expect(viewModel.result == nil)
#expect(viewModel.messageBus === messageBus)
}

Expand Down Expand Up @@ -130,25 +127,6 @@ struct SettingsViewModelTest {
#expect(tabViewModel.selectedTab == .shops)
}

@Test
func statePropertiesAreObservable() {
let viewModel = SettingsViewModel(
sessionController: sessionController,
tabViewModel: tabViewModel,
messageBus: messageBus
)

// Test that properties can be set (indicating they're observable)
viewModel.isShowingMailView = true
#expect(viewModel.isShowingMailView == true)

viewModel.alertNoMail = true
#expect(viewModel.alertNoMail == true)

viewModel.result = .success(.sent)
#expect(viewModel.result != nil)
}

@Test
func messageBusIsAccessible() {
let viewModel = SettingsViewModel(
Expand Down
Loading