diff --git a/.gitignore b/.gitignore index cd75295..f0ee037 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,9 @@ +## xcconfig hide +*.xcconfig + +## Firebase +GoogleService-Info.plist + ## User settings xcuserdata/ diff --git a/AppPackage/Package.swift b/AppPackage/Package.swift index a67ca71..4fecae2 100644 --- a/AppPackage/Package.swift +++ b/AppPackage/Package.swift @@ -9,7 +9,7 @@ let package = Package( name: "AppPackage", targets: [ "TimeTableFeature", - "Share", + "ShareFeature", "MakePromise", "PromiseManagement", "LoginFeature", @@ -48,7 +48,8 @@ let package = Package( .package(url: "https://github.com/siteline/SwiftUI-Introspect.git", from: "0.1.4"), .package(url: "https://github.com/devxoul/Then.git", from: "3.0.0"), .package(url: "https://github.com/kakao/kakao-ios-sdk.git", from: "2.14.0"), - .package(url: "https://github.com/Team-Planz/Planz-iOS-Secrets.git", branch: "main") + .package(url: "https://github.com/Team-Planz/Planz-iOS-Secrets.git", branch: "main"), + .package(url: "https://github.com/firebase/firebase-ios-sdk", from: "8.10.0") ], targets: [ .target( @@ -107,10 +108,13 @@ let package = Package( ] ), .target( - name: "Share", + name: "ShareFeature", dependencies: [ "DesignSystem", - .product(name: "ComposableArchitecture", package: "swift-composable-architecture") + "Repository", + .product(name: "ComposableArchitecture", package: "swift-composable-architecture"), + "APIClient", + .product(name: "Planz-iOS-Secrets", package: "Planz-iOS-Secrets") ] ), .target( @@ -183,6 +187,16 @@ let package = Package( dependencies: [ "Entity" ] + ), + .target( + name: "Repository", + dependencies: [ + .product(name: "FirebaseAuth", package: "firebase-ios-sdk"), + .product(name: "FirebaseFirestore", package: "firebase-ios-sdk"), + .product(name: "FirebaseDynamicLinks", package: "firebase-ios-sdk"), + .product(name: "KakaoSDK", package: "kakao-ios-sdk"), + .product(name: "Planz-iOS-Secrets", package: "Planz-iOS-Secrets") + ] ) ] ) diff --git a/AppPackage/Sources/Repository/FirebaseRepository.swift b/AppPackage/Sources/Repository/FirebaseRepository.swift new file mode 100644 index 0000000..1195f57 --- /dev/null +++ b/AppPackage/Sources/Repository/FirebaseRepository.swift @@ -0,0 +1,37 @@ +// +// File.swift +// +// +// Created by 한상준 on 2023/04/10. +// + +import FirebaseAuth +import FirebaseCore +import FirebaseDynamicLinks +import FirebaseFirestore +import Foundation +import Planz_iOS_Secrets +public protocol FirebaseRepository { + func getDynamicLink(id: Int?) -> URL? +} + +public class FirebaseRepositoryImpl: FirebaseRepository { + public init() {} + + private func getDeepLink(id: Int?) -> URL? { + if let id { + return URL(string: "\(Secrets.Firebase.domain.value)/?plandId=\(id)") + } else { + return URL(string: Secrets.Firebase.domain.value) + } + } + + public func getDynamicLink(id: Int? = nil) -> URL? { + guard let link = getDeepLink(id: id) else { return nil } + let dynamicLinksDomainURIPrefix = Secrets.Firebase.prefix.value + let linkBuilder = DynamicLinkComponents(link: link, domainURIPrefix: dynamicLinksDomainURIPrefix) + linkBuilder?.iOSParameters = DynamicLinkIOSParameters(bundleID: Secrets.App.iosBundleId.value) + linkBuilder?.androidParameters = DynamicLinkAndroidParameters(packageName: Secrets.App.androidBundleId.value) + return linkBuilder?.url + } +} diff --git a/AppPackage/Sources/Repository/KakaoRepository.swift b/AppPackage/Sources/Repository/KakaoRepository.swift new file mode 100644 index 0000000..5ce4a29 --- /dev/null +++ b/AppPackage/Sources/Repository/KakaoRepository.swift @@ -0,0 +1,39 @@ +// +// File.swift +// +// +// Created by 한상준 on 2023/04/10. +// + +import KakaoSDKShare +import KakaoSDKTemplate +import Planz_iOS_Secrets + +public protocol KakaoRepository { + func getKakaoTalkSharingResult(url: String) async -> Result +} + +public enum KakaoError: Error { + case kakaoTalkSharingInAvailable +} + +public class KakaoRepositoryImpl: KakaoRepository { + public init() {} + public func getKakaoTalkSharingResult(url _: String) async -> Result { + if ShareApi.isKakaoTalkSharingAvailable() { + // TODO: 이전 화면에서 전달해주는 id 값에 맞춰서 공유 링크의 파라미터로 추가하도록 수정 필요 + return await withCheckedContinuation { continuation in + ShareApi.shared.shareCustom(templateId: Int64(Secrets.Kakao.templateId.value)!, templateArgs: ["": ""]) { linkResult, error in + if let error = error { + continuation.resume(returning: .failure(error)) + } else { + guard let linkResult = linkResult else { return } + continuation.resume(returning: .success(linkResult)) + } + } + } + } else { + return .failure(KakaoError.kakaoTalkSharingInAvailable) + } + } +} diff --git a/AppPackage/Sources/Share/ShareLinkCopyView.swift b/AppPackage/Sources/Share/ShareLinkCopyView.swift deleted file mode 100644 index 846d31f..0000000 --- a/AppPackage/Sources/Share/ShareLinkCopyView.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// ShareLinkCopyView.swift -// -// -// Created by 한상준 on 2023/02/12. -// - -import DesignSystem -import SwiftUI - -struct ShareLinkCopyView: View { - public var body: some View { - HStack { - HStack { - Text("url/link/1234/1234") - Spacer() - Button("복사") {} - .font(.system(size: 14)) - .foregroundColor(PDS.COLOR.purple9.scale) - } - .padding(EdgeInsets(top: 16, leading: 20, bottom: 16, trailing: 20)) - .background(PDS.COLOR.white3.scale) - .border(PDS.COLOR.gray2.scale, width: 1) - .cornerRadius(10) - } - .padding(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 20)) - } -} diff --git a/AppPackage/Sources/Share/ShareView.swift b/AppPackage/Sources/Share/ShareView.swift deleted file mode 100644 index 9afc6c2..0000000 --- a/AppPackage/Sources/Share/ShareView.swift +++ /dev/null @@ -1,74 +0,0 @@ -// -// ShareView.swift -// -// -// Created by 한상준 on 2023/02/07. -// - -import DesignSystem -import SwiftUI - -public struct ShareView: View { - let mailImage: String = "mailIllustration" - public init() {} - - public var body: some View { - VStack(spacing: .zero) { - VStack(spacing: 50) { - VStack(spacing: 6) { - HStack(spacing: .zero) { - EmptyView().frame(width: 20) - Text("이제 약속을 ") - .font(.system(size: 20)) - .fontWeight(.bold) - Text("공유") - .font(.system(size: 20)) - .foregroundColor(PDS.COLOR.purple9.scale) - .fontWeight(.bold) - Text("하세요!") - .font(.system(size: 20)) - .fontWeight(.bold) - Spacer() - } - .padding(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 20)) - HStack(spacing: .zero) { - Text("최대 10명까지 응답 가능") - .font(.system(size: 16)) - Spacer() - } - .padding(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 20)) - HStack(spacing: .zero) { - Text("*본인포함") - .font(.system(size: 12)) - Spacer() - } - .padding(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 20)) - } - PDS.Icon.mailIllustration.image - .resizable() - .aspectRatio(contentMode: .fit) - } - Spacer() - VStack(spacing: 12) { - ShareLinkCopyView() - Button {} label: { - HStack(spacing: 0) { - PDS.Icon.kakao.image - Text("카카오톡으로 약속 공유하기") - .lineLimit(1) - }.frame(maxWidth: .infinity) - } - .buttonStyle(KakaoShareButtonStyle()) - } - } - .background(PDS.COLOR.white1.scale) - } -} - -#if DEBUG - struct ShareView_Previews: PreviewProvider { - static var previews: some View { - ShareView() - } - } -#endif diff --git a/AppPackage/Sources/Share/KakaoShareButtonStyle.swift b/AppPackage/Sources/ShareFeature/KakaoShareButtonStyle.swift similarity index 100% rename from AppPackage/Sources/Share/KakaoShareButtonStyle.swift rename to AppPackage/Sources/ShareFeature/KakaoShareButtonStyle.swift diff --git a/AppPackage/Sources/ShareFeature/ShareLinkCopyView.swift b/AppPackage/Sources/ShareFeature/ShareLinkCopyView.swift new file mode 100644 index 0000000..ef1fbf2 --- /dev/null +++ b/AppPackage/Sources/ShareFeature/ShareLinkCopyView.swift @@ -0,0 +1,39 @@ +// +// ShareLinkCopyView.swift +// +// +// Created by 한상준 on 2023/02/12. +// + +import ComposableArchitecture +import DesignSystem +import SwiftUI + +struct ShareLinkCopyView: View { + public let store: StoreOf + public init(store: StoreOf) { + self.store = store + } + + public var body: some View { + WithViewStore(self.store) { viewStore in + HStack { + HStack { + Text(viewStore.linkForShare) + .lineLimit(1) + Spacer() + Button("복사") { + viewStore.send(.copyLinkTapped) + } + .font(.system(size: 14)) + .foregroundColor(PDS.COLOR.purple9.scale) + } + .padding(EdgeInsets(top: 16, leading: 20, bottom: 16, trailing: 20)) + .background(PDS.COLOR.white3.scale) + .border(PDS.COLOR.gray2.scale, width: 1) + .cornerRadius(10) + } + .padding(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 20)) + } + } +} diff --git a/AppPackage/Sources/ShareFeature/SharePromiseStore.swift b/AppPackage/Sources/ShareFeature/SharePromiseStore.swift new file mode 100644 index 0000000..8811a4c --- /dev/null +++ b/AppPackage/Sources/ShareFeature/SharePromiseStore.swift @@ -0,0 +1,70 @@ +// +// File.swift +// +// +// Created by 한상준 on 2023/04/01. +// + +import APIClient +import ComposableArchitecture +import Foundation +import Planz_iOS_Secrets +import Repository +import UIKit + +public struct SharePromiseFeature: ReducerProtocol { + let firebaseRepository: FirebaseRepository + let kakaoRepository: KakaoRepository + public init( + firebaseRepository: FirebaseRepository = FirebaseRepositoryImpl(), + kakaoRepository: KakaoRepository = KakaoRepositoryImpl() + ) { + self.firebaseRepository = firebaseRepository + self.kakaoRepository = kakaoRepository + } + + public struct State: Equatable { + var linkForShare = "" + var id: Int + public init(id: Int) { + self.id = id + } + } + + public enum Action: Equatable { + case viewDidAppear + case copyLinkTapped + case shareAsKakaoTapped + } + + func shareViaKakao(link: String) { + Task { + let sharingResult = await self.kakaoRepository.getKakaoTalkSharingResult(url: link) + do { + let url = try sharingResult.get().url + await UIApplication.shared.open(url) + } catch { + // TODO: 공유 에러 팝업 노출 + print(error) + } + } + } + + public var body: some ReducerProtocol { + Reduce { state, action in + switch action { + case .viewDidAppear: + if let link = self.firebaseRepository.getDynamicLink(id: state.id) { + state.linkForShare = link.absoluteString + } + return .none + case .copyLinkTapped: + UIPasteboard.general.string = state.linkForShare + return .none + case .shareAsKakaoTapped: + shareViaKakao(link: state.linkForShare) + return .none + } + } + } +} diff --git a/AppPackage/Sources/ShareFeature/SharePromiseView.swift b/AppPackage/Sources/ShareFeature/SharePromiseView.swift new file mode 100644 index 0000000..61218dd --- /dev/null +++ b/AppPackage/Sources/ShareFeature/SharePromiseView.swift @@ -0,0 +1,87 @@ +// +// ShareView.swift +// +// +// Created by 한상준 on 2023/02/07. +// + +import ComposableArchitecture +import DesignSystem +import SwiftUI + +public struct SharePromiseView: View { + let mailImage: String = "mailIllustration" + + public let store: StoreOf + + public init(store: StoreOf) { + self.store = store + } + + public var body: some View { + WithViewStore(self.store) { viewStore in + VStack(spacing: .zero) { + VStack(spacing: 50) { + VStack(spacing: 6) { + HStack(spacing: .zero) { + EmptyView().frame(width: 20) + Text("이제 약속을 ") + .font(.system(size: 20)) + .fontWeight(.bold) + Text("공유") + .font(.system(size: 20)) + .foregroundColor(PDS.COLOR.purple9.scale) + .fontWeight(.bold) + Text("하세요!") + .font(.system(size: 20)) + .fontWeight(.bold) + Spacer() + } + .padding(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 20)) + HStack(spacing: .zero) { + Text("최대 10명까지 응답 가능") + .font(.system(size: 16)) + Spacer() + } + .padding(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 20)) + HStack(spacing: .zero) { + Text("*본인포함") + .font(.system(size: 12)) + Spacer() + } + .padding(EdgeInsets(top: 0, leading: 20, bottom: 0, trailing: 20)) + } + PDS.Icon.mailIllustration.image + .resizable() + .aspectRatio(contentMode: .fit) + } + Spacer() + VStack(spacing: 12) { + ShareLinkCopyView(store: store) + Button { + viewStore.send(.shareAsKakaoTapped) + } label: { + HStack(spacing: 0) { + PDS.Icon.kakao.image + Text("카카오톡으로 약속 공유하기") + .lineLimit(1) + }.frame(maxWidth: .infinity) + } + .buttonStyle(KakaoShareButtonStyle()) + } + } + .background(PDS.COLOR.white1.scale) + .onAppear(perform: { + viewStore.send(.viewDidAppear) + }) + } + } +} + +#if DEBUG + struct ShareView_Previews: PreviewProvider { + static var previews: some View { + SharePromiseView(store: .init(initialState: SharePromiseFeature.State(), reducer: SharePromiseFeature()._printChanges())) + } + } +#endif diff --git a/Planz.xcodeproj/project.pbxproj b/Planz.xcodeproj/project.pbxproj index 60b8aa2..badef23 100644 --- a/Planz.xcodeproj/project.pbxproj +++ b/Planz.xcodeproj/project.pbxproj @@ -89,6 +89,13 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 496EFF7A29E06A5000351034 /* Secrets */ = { + isa = PBXGroup; + children = ( + ); + path = Secrets; + sourceTree = ""; + }; 6F47598D28D9D0CE005CC91B = { isa = PBXGroup; children = ( @@ -117,6 +124,7 @@ 6F47599828D9D0CE005CC91B /* Planz */ = { isa = PBXGroup; children = ( + 496EFF7A29E06A5000351034 /* Secrets */, AE79C84329863F400098BA28 /* Info.plist */, 6F47599928D9D0CE005CC91B /* PlanzApp.swift */, 6F47599D28D9D0CF005CC91B /* Assets.xcassets */, diff --git a/Planz/Info.plist b/Planz/Info.plist index a719cf5..a624320 100644 --- a/Planz/Info.plist +++ b/Planz/Info.plist @@ -2,6 +2,22 @@ + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + $(KakaoURLScheme) + + + + LSApplicationQueriesSchemes + + kakaokompassauth + kakaolink + UIApplicationSceneManifest UIApplicationSupportsMultipleScenes diff --git a/Planz/PlanzApp.swift b/Planz/PlanzApp.swift index 3dfdb50..a752572 100644 --- a/Planz/PlanzApp.swift +++ b/Planz/PlanzApp.swift @@ -1,9 +1,24 @@ import AppFeature import ComposableArchitecture +import Firebase +import KakaoSDKCommon +import Planz_iOS_Secrets +import ShareFeature import SwiftUI +class AppDelegate: NSObject, UIApplicationDelegate { + func application(_: UIApplication, + didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { + FirebaseApp.configure() + KakaoSDK.initSDK(appKey: Secrets.Kakao.appKey.value) + return true + } +} + @main struct PlanzApp: App { + @UIApplicationDelegateAdaptor(AppDelegate.self) var delegate + let store: StoreOf = .init( initialState: .launchScreen, reducer: AppCore()