Skip to content

Commit

Permalink
이메일 보내기 기능 (#96)
Browse files Browse the repository at this point in the history
* 🚧 Add `MailRowItem`, `MailSheetView`

* ✨ Implement `MailSheetView` for email sending feature

- Implemented `MailSheetView` to present a modal view for sending emails upon button press

* ✨ Implement email sending functionality with fallback

- Implemented the functionality to send emails directly from the app. If unable to send an email, added code to redirect users to the Mail app store page

* ♿ Improve accessibility features

- Updated the app to hide images from Accessibility tools to avoid unnecessary distractions.
- Added descriptive text for version information

* ✨ Implement `DeviceProvider` protocol for dynamic mail body content

- Created DeviceProvider protocol to abstract device-related information for use in composing email bodies.
- Modified MailView to rely on DeviceProvider for dynamically populating the mail body with relevant device information, enhancing the detail and relevance of email communications.

* ✅ Add DeviceInformationProvider tests
  • Loading branch information
WhiteHyun authored Mar 11, 2024
1 parent 96edb92 commit dec3498
Show file tree
Hide file tree
Showing 9 changed files with 370 additions and 13 deletions.
38 changes: 34 additions & 4 deletions PyeonHaeng-iOS.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@
BA05640B2B6248EA003D6DC7 /* HomeAPISupport in Frameworks */ = {isa = PBXBuildFile; productRef = BA05640A2B6248EA003D6DC7 /* HomeAPISupport */; };
BA05640D2B6248EA003D6DC7 /* Network in Frameworks */ = {isa = PBXBuildFile; productRef = BA05640C2B6248EA003D6DC7 /* Network */; };
BA0623D82B68E51400A0A3B2 /* Log in Frameworks */ = {isa = PBXBuildFile; productRef = BA0623D72B68E51400A0A3B2 /* Log */; };
BA097F0E2B9CA82A002D3E1E /* MailSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA097F0D2B9CA82A002D3E1E /* MailSheetView.swift */; };
BA1688E02B99B85500A8F462 /* NoticeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA1688DF2B99B85500A8F462 /* NoticeView.swift */; };
BA28F17C2B6155450052855E /* PyeonHaeng_iOSTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA28F17B2B6155450052855E /* PyeonHaeng_iOSTests.swift */; };
BA28F17C2B6155450052855E /* ProductConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA28F17B2B6155450052855E /* ProductConfigurationTests.swift */; };
BA28F1852B6155810052855E /* OnboardingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA28F1842B6155810052855E /* OnboardingView.swift */; };
BA28F1882B6155910052855E /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA28F1872B6155910052855E /* HomeView.swift */; };
BA28F18B2B6155BD0052855E /* ProductInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA28F18A2B6155BD0052855E /* ProductInfoView.swift */; };
Expand Down Expand Up @@ -47,6 +48,11 @@
BAE159D82B65FA6F002DCF94 /* HomeProductDetailSelectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAE159D72B65FA6F002DCF94 /* HomeProductDetailSelectionView.swift */; };
BAE159DA2B65FC35002DCF94 /* HomeProductListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAE159D92B65FC35002DCF94 /* HomeProductListView.swift */; };
BAE159DE2B663A9A002DCF94 /* HomeProductSorterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAE159DD2B663A9A002DCF94 /* HomeProductSorterView.swift */; };
BAE7A1E62B9D7CA70022FEBB /* MailRowItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAE7A1E52B9D7CA70022FEBB /* MailRowItem.swift */; };
BAE7A1E82B9DA0360022FEBB /* DeviceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAE7A1E72B9DA0360022FEBB /* DeviceProvider.swift */; };
BAE7A1EA2B9DA4960022FEBB /* MockDeviceInformationProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAE7A1E92B9DA4960022FEBB /* MockDeviceInformationProvider.swift */; };
BAE7A1EB2B9DA4A50022FEBB /* DeviceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAE7A1E72B9DA0360022FEBB /* DeviceProvider.swift */; };
BAE7A1ED2B9DA5090022FEBB /* DeviceInformationProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAE7A1EC2B9DA5090022FEBB /* DeviceInformationProviderTests.swift */; };
BAF2BEB32B61236100931AF0 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = BAF2BEB22B61236100931AF0 /* Localizable.xcstrings */; };
E50176262B6A204F0098D1BE /* ProductInfoLineGraphView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E50176252B6A204F0098D1BE /* ProductInfoLineGraphView.swift */; };
E5028D5C2B96BA9400B36C16 /* SearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA28F1902B61566E0052855E /* SearchView.swift */; };
Expand Down Expand Up @@ -82,9 +88,10 @@
BA0564022B62179A003D6DC7 /* Core */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Core; sourceTree = "<group>"; };
BA0564032B6219D4003D6DC7 /* APIService */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = APIService; sourceTree = "<group>"; };
BA0564052B624646003D6DC7 /* Shared.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Shared.xcconfig; path = XCConfig/Shared.xcconfig; sourceTree = SOURCE_ROOT; };
BA097F0D2B9CA82A002D3E1E /* MailSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MailSheetView.swift; sourceTree = "<group>"; };
BA1688DF2B99B85500A8F462 /* NoticeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeView.swift; sourceTree = "<group>"; };
BA28F1792B6155450052855E /* PyeonHaeng-iOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "PyeonHaeng-iOSTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
BA28F17B2B6155450052855E /* PyeonHaeng_iOSTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PyeonHaeng_iOSTests.swift; sourceTree = "<group>"; };
BA28F17B2B6155450052855E /* ProductConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductConfigurationTests.swift; sourceTree = "<group>"; };
BA28F1842B6155810052855E /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = "<group>"; };
BA28F1872B6155910052855E /* HomeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = "<group>"; };
BA28F18A2B6155BD0052855E /* ProductInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductInfoView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -117,6 +124,10 @@
BAE159D72B65FA6F002DCF94 /* HomeProductDetailSelectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeProductDetailSelectionView.swift; sourceTree = "<group>"; };
BAE159D92B65FC35002DCF94 /* HomeProductListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeProductListView.swift; sourceTree = "<group>"; };
BAE159DD2B663A9A002DCF94 /* HomeProductSorterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeProductSorterView.swift; sourceTree = "<group>"; };
BAE7A1E52B9D7CA70022FEBB /* MailRowItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MailRowItem.swift; sourceTree = "<group>"; };
BAE7A1E72B9DA0360022FEBB /* DeviceProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceProvider.swift; sourceTree = "<group>"; };
BAE7A1E92B9DA4960022FEBB /* MockDeviceInformationProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockDeviceInformationProvider.swift; sourceTree = "<group>"; };
BAE7A1EC2B9DA5090022FEBB /* DeviceInformationProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceInformationProviderTests.swift; sourceTree = "<group>"; };
BAF2BEB22B61236100931AF0 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
E50176252B6A204F0098D1BE /* ProductInfoLineGraphView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductInfoLineGraphView.swift; sourceTree = "<group>"; };
E50584522B763C8C002FDACF /* ProductInfoViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductInfoViewModel.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -175,6 +186,16 @@
name = Frameworks;
sourceTree = "<group>";
};
BA097F0C2B9CA820002D3E1E /* Mail */ = {
isa = PBXGroup;
children = (
BA097F0D2B9CA82A002D3E1E /* MailSheetView.swift */,
BAE7A1E52B9D7CA70022FEBB /* MailRowItem.swift */,
BAE7A1E72B9DA0360022FEBB /* DeviceProvider.swift */,
);
path = Mail;
sourceTree = "<group>";
};
BA1688EB2B99D0B700A8F462 /* Notice */ = {
isa = PBXGroup;
children = (
Expand Down Expand Up @@ -224,7 +245,8 @@
isa = PBXGroup;
children = (
BA849A352B8F4FB0004495BF /* Mocks */,
BA28F17B2B6155450052855E /* PyeonHaeng_iOSTests.swift */,
BA28F17B2B6155450052855E /* ProductConfigurationTests.swift */,
BAE7A1EC2B9DA5090022FEBB /* DeviceInformationProviderTests.swift */,
);
path = "PyeonHaeng-iOSTests";
sourceTree = "<group>";
Expand Down Expand Up @@ -273,6 +295,7 @@
BA28F18C2B6155EC0052855E /* SettingsScene */ = {
isa = PBXGroup;
children = (
BA097F0C2B9CA820002D3E1E /* Mail */,
BA1688EB2B99D0B700A8F462 /* Notice */,
BA28F18D2B6156420052855E /* SettingsView.swift */,
);
Expand Down Expand Up @@ -319,6 +342,7 @@
isa = PBXGroup;
children = (
BA849A362B8F4FC0004495BF /* MockPaginatable.swift */,
BAE7A1E92B9DA4960022FEBB /* MockDeviceInformationProvider.swift */,
);
path = Mocks;
sourceTree = "<group>";
Expand Down Expand Up @@ -563,8 +587,11 @@
buildActionMask = 2147483647;
files = (
BA849A372B8F4FC0004495BF /* MockPaginatable.swift in Sources */,
BAE7A1EA2B9DA4960022FEBB /* MockDeviceInformationProvider.swift in Sources */,
BA849A342B8F4F36004495BF /* ProductConfiguration.swift in Sources */,
BA28F17C2B6155450052855E /* PyeonHaeng_iOSTests.swift in Sources */,
BA28F17C2B6155450052855E /* ProductConfigurationTests.swift in Sources */,
BAE7A1EB2B9DA4A50022FEBB /* DeviceProvider.swift in Sources */,
BAE7A1ED2B9DA5090022FEBB /* DeviceInformationProviderTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -573,6 +600,7 @@
buildActionMask = 2147483647;
files = (
E5028D5D2B96BA9F00B36C16 /* ProductInfoDetailView.swift in Sources */,
BAE7A1E62B9D7CA70022FEBB /* MailRowItem.swift in Sources */,
E5028D5C2B96BA9400B36C16 /* SearchView.swift in Sources */,
BAE159DA2B65FC35002DCF94 /* HomeProductListView.swift in Sources */,
BA5E51932B9AC8510036209A /* NoticeDetailView.swift in Sources */,
Expand All @@ -591,10 +619,12 @@
E57F2AA82B774CA700E12B3D /* ProductInfoDependency.swift in Sources */,
E55DD5122B91DE9500AA63C0 /* SearchListCardView.swift in Sources */,
9CE4B4712B6F0B57002DC446 /* OnboardingPage.swift in Sources */,
BA097F0E2B9CA82A002D3E1E /* MailSheetView.swift in Sources */,
E52F371B2B947DC8000EBAD5 /* SearchViewModel.swift in Sources */,
BAE159DE2B663A9A002DCF94 /* HomeProductSorterView.swift in Sources */,
BA1688E02B99B85500A8F462 /* NoticeView.swift in Sources */,
BAE159D82B65FA6F002DCF94 /* HomeProductDetailSelectionView.swift in Sources */,
BAE7A1E82B9DA0360022FEBB /* DeviceProvider.swift in Sources */,
E50176262B6A204F0098D1BE /* ProductInfoLineGraphView.swift in Sources */,
BAB5CF252B6B7C5A008B24BF /* AppRootComponent.swift in Sources */,
BAA4D9AF2B5A1795005999F8 /* SplashView.swift in Sources */,
Expand Down
68 changes: 66 additions & 2 deletions PyeonHaeng-iOS/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,38 @@
}
}
},
"Cannot Send Mail" : {
"localizations" : {
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "メールを送信できません"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "메일을 보낼 수 없음"
}
}
}
},
"Close" : {
"localizations" : {
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "閉じる"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "닫기"
}
}
}
},
"Contact Us" : {
"localizations" : {
"ja" : {
Expand Down Expand Up @@ -292,6 +324,22 @@
}
}
},
"Open App Store" : {
"localizations" : {
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "App Storeを開く"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "앱 스토어 열기"
}
}
}
},
"Select Promotion Items" : {
"localizations" : {
"ja" : {
Expand Down Expand Up @@ -324,6 +372,22 @@
}
}
},
"Your device is not configured to send mail.\nWould you like to install a mail app from the App Store?" : {
"localizations" : {
"ja" : {
"stringUnit" : {
"state" : "translated",
"value" : "お使いのデバイスはメールを送信するために設定されていません。App Storeからメールアプリをインストールしますか?"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "기기에서 메일을 보낼 수 있도록 설정되어 있지 않아요. 앱 스토어에서 메일 앱을 설치할까요?"
}
}
}
},
"개당" : {
"extractionState" : "manual",
"localizations" : {
Expand Down Expand Up @@ -446,13 +510,13 @@
},
"ja" : {
"stringUnit" : {
"state" : "needs_review",
"state" : "translated",
"value" : "検索する商品名を入力してください。"
}
},
"ko" : {
"stringUnit" : {
"state" : "needs_review",
"state" : "translated",
"value" : "검색할 상품이름을 입력해주세요."
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
//
// DeviceProvider.swift
// PyeonHaeng-iOS
//
// Created by 홍승현 on 3/10/24.
//

import UIKit

// MARK: - DeviceInformationProvider

protocol DeviceInformationProvider {
var deviceModel: String { get }
var deviceOS: String { get }
var appVersion: String { get }
}

// MARK: - SystemDeviceProvider

struct SystemDeviceProvider: DeviceInformationProvider {
var deviceModel: String {
deviceIdentifier()
}

var deviceOS: String {
UIDevice.current.systemVersion
}

var appVersion: String {
Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "Unknown"
}

/// 기종을 가져오는 함수
private func deviceIdentifier() -> String {
var systemInfo = utsname()
uname(&systemInfo)
let machineMirror = Mirror(reflecting: systemInfo.machine)
let identifier = machineMirror.children.reduce("") { identifier, element in
guard let value = element.value as? Int8, value != 0 else { return identifier }
return identifier + String(UnicodeScalar(UInt8(value)))
}
return identifier
}
}
119 changes: 119 additions & 0 deletions PyeonHaeng-iOS/Sources/Scenes/SettingsScene/Mail/MailRowItem.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
//
// MailRowItem.swift
// PyeonHaeng-iOS
//
// Created by 홍승현 on 3/10/24.
//

import DesignSystem
import MessageUI
import SwiftUI

// MARK: - MailRowItem

struct MailRowItem: View {
@State private var isMailPresented: Bool = false
@State private var showAlert: Bool = false
@Environment(\.openURL) var openURL

private let deviceProvider: DeviceInformationProvider

// MARK: Initializations

init(deviceProvider: DeviceInformationProvider) {
self.deviceProvider = deviceProvider
}

// MARK: Body

var body: some View {
Button(action: attemptToSendMail) {
HStack {
Image.notePencil
.renderingMode(.template)
.foregroundStyle(.gray900)
Text("Contact Us")
.font(.b1)
Spacer()
Image(systemName: Constants.disclosureImageName)
.font(.system(size: Metrics.disclosureSize, weight: .semibold)) // Styled to look like a disclosure indicator
.foregroundStyle(.gray.opacity(Metrics.disclosureOpacity))
}
}
.sheet(isPresented: $isMailPresented) {
MailSheetView(
subject: Constants.emailSubject,
recipients: [Constants.emailAddress],
messageBody: generateDefaultMessageBody()
)
}
.alert(isPresented: $showAlert) {
Alert(
title: Text(Constants.alertTitle),
message: Text(Constants.alertDescription),
primaryButton: .default(Text(Constants.openAppStoreButtonText), action: moveToMailApp),
secondaryButton: .cancel(Text(Constants.closeButtonText))
)
}
}

// MARK: Private methods

/// Attempts to present the mail view or shows an alert if mail cannot be sent.
private func attemptToSendMail() {
if MFMailComposeViewController.canSendMail() {
isMailPresented = true
} else {
showAlert = true
}
}

/// Opens the Mail app's page in the App Store.
private func moveToMailApp() {
if let url = URL(string: Constants.emailURL) {
openURL(url)
}
}

/// mail default contents
private func generateDefaultMessageBody() -> String {
"""
Please write your message here.
-------------------
Device Model : \(deviceProvider.deviceModel)
Device OS : \(deviceProvider.deviceOS)
App Version : \(deviceProvider.appVersion)
-------------------
"""
}
}

// MARK: - Metrics

private enum Metrics {
static let disclosureSize: CGFloat = 14
static let disclosureOpacity: CGFloat = 0.5
}

// MARK: - Constants

private enum Constants {
static let alertTitle: LocalizedStringKey = "Cannot Send Mail"
static let alertDescription: LocalizedStringKey = """
Your device is not configured to send mail.
Would you like to install a mail app from the App Store?
"""

static let disclosureImageName: String = "chevron.right"

static let openAppStoreButtonText: LocalizedStringKey = "Open App Store"
static let closeButtonText: LocalizedStringKey = "Close"

/// Represents the URL to the Mail app in the App Store.
static let emailURL: String = "https://apps.apple.com/app/mail/id1108187098"
static let emailSubject: String = "<편행> 문의하기"
static let emailAddress: String = "[email protected]"
}
Loading

0 comments on commit dec3498

Please sign in to comment.