Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement friend request via Kakao #306

Merged
merged 9 commits into from
Oct 2, 2024
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
37 changes: 36 additions & 1 deletion SNUTT-2022/SNUTT.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,10 @@
CE6CA91429E3CFB4004E92B1 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = BE6D893228EFD662000607A6 /* Localizable.strings */; };
CE72435F2B30235300F9E0D7 /* SearchLectureSceneViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE72435E2B30235300F9E0D7 /* SearchLectureSceneViewModel.swift */; };
CE7243612B30240D00F9E0D7 /* InteractiveDismissKeyboardModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7243602B30240D00F9E0D7 /* InteractiveDismissKeyboardModifier.swift */; };
CE8590182C5E1B5C00ECFE9E /* KakaoSDKShare in Frameworks */ = {isa = PBXBuildFile; productRef = CE8590172C5E1B5C00ECFE9E /* KakaoSDKShare */; };
CE85901A2C5E219C00ECFE9E /* KakaoSDKCommon in Frameworks */ = {isa = PBXBuildFile; productRef = CE8590192C5E219C00ECFE9E /* KakaoSDKCommon */; };
CE85901C2C5E21D600ECFE9E /* KakaoSDKTemplate in Frameworks */ = {isa = PBXBuildFile; productRef = CE85901B2C5E21D600ECFE9E /* KakaoSDKTemplate */; };
CE85901E2C5E549600ECFE9E /* FriendState.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE85901D2C5E549600ECFE9E /* FriendState.swift */; };
CE98204B2A09FBDD001037F5 /* DebugState.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE98204A2A09FBDD001037F5 /* DebugState.swift */; };
CE9820502A0A0BB7001037F5 /* NetworkLogListScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE98204F2A0A0BB7001037F5 /* NetworkLogListScene.swift */; };
CE9820522A0A0BE9001037F5 /* NetworkLogEntryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE9820512A0A0BE9001037F5 /* NetworkLogEntryView.swift */; };
Expand Down Expand Up @@ -579,6 +583,7 @@
CE6CA91129E24676004E92B1 /* TimetableAccessoryCircularView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimetableAccessoryCircularView.swift; sourceTree = "<group>"; };
CE72435E2B30235300F9E0D7 /* SearchLectureSceneViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchLectureSceneViewModel.swift; sourceTree = "<group>"; };
CE7243602B30240D00F9E0D7 /* InteractiveDismissKeyboardModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractiveDismissKeyboardModifier.swift; sourceTree = "<group>"; };
CE85901D2C5E549600ECFE9E /* FriendState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FriendState.swift; sourceTree = "<group>"; };
CE98204A2A09FBDD001037F5 /* DebugState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugState.swift; sourceTree = "<group>"; };
CE98204F2A0A0BB7001037F5 /* NetworkLogListScene.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkLogListScene.swift; sourceTree = "<group>"; };
CE9820512A0A0BE9001037F5 /* NetworkLogEntryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkLogEntryView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -636,12 +641,15 @@
731C244E2C4442590015877B /* KakaoSDK in Frameworks */,
B8B7501D2B6274A5004F6272 /* KakaoMapsSDK_SPM in Frameworks */,
BE4CD86328F5A56200BA9BBC /* FirebaseAnalyticsSwift in Frameworks */,
CE85901C2C5E21D600ECFE9E /* KakaoSDKTemplate in Frameworks */,
BE779B1828E3DD5B009960EB /* FacebookLogin in Frameworks */,
CEF992F72BF3B8FC00F0FFA4 /* ReactNativeKit in Frameworks */,
736AF8512C2F279900ED9C1A /* GoogleSignInSwift in Frameworks */,
BE4CD86528F5A56200BA9BBC /* FirebaseCrashlytics in Frameworks */,
BEDE34D42879A59B00525014 /* Alamofire in Frameworks */,
BE4CD86128F5A56200BA9BBC /* FirebaseAnalytics in Frameworks */,
CE85901A2C5E219C00ECFE9E /* KakaoSDKCommon in Frameworks */,
CE8590182C5E1B5C00ECFE9E /* KakaoSDKShare in Frameworks */,
736AF84F2C2F279900ED9C1A /* GoogleSignIn in Frameworks */,
731C24502C4442590015877B /* KakaoSDKAuth in Frameworks */,
);
Expand Down Expand Up @@ -673,6 +681,7 @@
B87B315A28D5A70F005C170B /* TimetableState.swift */,
CE63A3BB2A21B06900A633FC /* RoutingState.swift */,
B87B315828D5A70F005C170B /* UserState.swift */,
CE85901D2C5E549600ECFE9E /* FriendState.swift */,
BE2CB3622959C0CC00FCF0F0 /* ReviewState.swift */,
CEDDCA812A6AF66D00474D4E /* VacancyState.swift */,
B87DF6F82918B7AD008BB95B /* PopupState.swift */,
Expand Down Expand Up @@ -1240,6 +1249,8 @@
731C244D2C4442590015877B /* KakaoSDK */,
731C244F2C4442590015877B /* KakaoSDKAuth */,
731C24512C4442590015877B /* KakaoSDKCommon */,
CE8590172C5E1B5C00ECFE9E /* KakaoSDKShare */,
CE85901B2C5E21D600ECFE9E /* KakaoSDKTemplate */,
);
productName = SNUTT;
productReference = BE682BB22879E24D009EBCB7 /* SNUTT.app */;
Expand Down Expand Up @@ -1474,6 +1485,7 @@
B8823BC32BC2ED41003A3B69 /* BuildingRouter.swift in Sources */,
B8AF8D3E28C72A880056DE62 /* ValidationUtils.swift in Sources */,
CEDDCA7E2A6AEF2200474D4E /* VacancyLectureList.swift in Sources */,
CE85901E2C5E549600ECFE9E /* FriendState.swift in Sources */,
B82EA54C2B62C8490029FDF3 /* LectureMapView.swift in Sources */,
BE8BB3AD285D763B00070A66 /* SearchLectureScene.swift in Sources */,
BE682C0528881852009EBCB7 /* SearchService.swift in Sources */,
Expand Down Expand Up @@ -2394,7 +2406,15 @@
repositoryURL = "https://github.com/wafflestudio/ios-rn-prebuilt";
requirement = {
kind = exactVersion;
version = 0.17.0;
version = 0.22.0;
};
};
CE8590162C5E1B5C00ECFE9E /* XCRemoteSwiftPackageReference "kakao-ios-sdk" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/kakao/kakao-ios-sdk";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 2.22.5;
};
};
/* End XCRemoteSwiftPackageReference section */
Expand Down Expand Up @@ -2460,6 +2480,21 @@
package = BEDE34D22879A59B00525014 /* XCRemoteSwiftPackageReference "Alamofire" */;
productName = Alamofire;
};
CE8590172C5E1B5C00ECFE9E /* KakaoSDKShare */ = {
isa = XCSwiftPackageProductDependency;
package = CE8590162C5E1B5C00ECFE9E /* XCRemoteSwiftPackageReference "kakao-ios-sdk" */;
productName = KakaoSDKShare;
};
CE8590192C5E219C00ECFE9E /* KakaoSDKCommon */ = {
isa = XCSwiftPackageProductDependency;
package = CE8590162C5E1B5C00ECFE9E /* XCRemoteSwiftPackageReference "kakao-ios-sdk" */;
productName = KakaoSDKCommon;
};
CE85901B2C5E21D600ECFE9E /* KakaoSDKTemplate */ = {
isa = XCSwiftPackageProductDependency;
package = CE8590162C5E1B5C00ECFE9E /* XCRemoteSwiftPackageReference "kakao-ios-sdk" */;
productName = KakaoSDKTemplate;
};
CEF992F62BF3B8FC00F0FFA4 /* ReactNativeKit */ = {
isa = XCSwiftPackageProductDependency;
package = CE1F49AF2A8DB5A400B81E4E /* XCRemoteSwiftPackageReference "ios-rn-prebuilt" */;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,17 +114,17 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/wafflestudio/ios-rn-prebuilt",
"state" : {
"revision" : "0fab694088eaca04b868a00df42ba9d153e409fd",
"version" : "0.17.0"
"revision" : "3ba8aad9239857b8e2901e3da3e006db3eede0a3",
"version" : "0.22.0"
}
},
{
"identity" : "kakao-ios-sdk",
"kind" : "remoteSourceControl",
"location" : "https://github.com/kakao/kakao-ios-sdk",
"state" : {
"revision" : "08089eeffc9b442da1c7343a70bf66c6de1a72c9",
"version" : "2.22.4"
"revision" : "66b3bddc2657e8ccb7a16fa0264aac99c57be09b",
"version" : "2.22.5"
}
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
buildConfiguration = "Debug"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
Expand Down
1 change: 1 addition & 0 deletions SNUTT-2022/SNUTT/AppState/AppState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ final class AppState {
var system = SystemState()
var search = SearchState()
var timetable = TimetableState()
var friend = FriendState()

var menu = MenuState()
var notification = NotificationState()
Expand Down
7 changes: 7 additions & 0 deletions SNUTT-2022/SNUTT/AppState/DeepLinkHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ struct DeepLinkHandler {
try await handleTimetableLecture(parameters: urlComponents.queryItems)
case "bookmarks":
try await handleBookmark(parameters: urlComponents.queryItems)
case "kakaolink" where urlComponents.queryItems?["type"] == RNEvent.addFriendKakao.rawValue:
handleKakaoAddFriendRequest(parameters: urlComponents.queryItems)
default:
return
}
Expand Down Expand Up @@ -96,6 +98,11 @@ extension DeepLinkHandler {
}
appState.routing.notificationList.routeToLectureDetail(with: lecture)
}

private func handleKakaoAddFriendRequest(parameters: Parameters?) {
appState.friend.pendingFriendRequestToken = parameters?["requestToken"]
appState.system.selectedTab = .friends
}
}

extension DeepLinkHandler.Parameters {
Expand Down
6 changes: 6 additions & 0 deletions SNUTT-2022/SNUTT/AppState/States/FriendState.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import Foundation

@MainActor
class FriendState {
@Published var pendingFriendRequestToken: String?
}
29 changes: 10 additions & 19 deletions SNUTT-2022/SNUTT/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,6 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>FacebookAdvertiserIDCollectionEnabled</key>
<false/>
<key>KAKAO_PHASE</key>
<string></string>
<key>KAKAO_APP_KEY</key>
<string>$(KAKAO_APP_KEY)</string>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>nmap</string>
<string>kakaomap</string>
</array>
<key>NMFClientId</key>
<string>$(NAVER_MAP_CLIENT_ID)</string>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>ACCESS_TOKEN_TEST</key>
<string>$(ACCESS_TOKEN_TEST)</string>
<key>API_KEY</key>
Expand All @@ -46,7 +27,17 @@
<string>kakao$(KAKAO_APP_KEY)</string>
</array>
</dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>kakao$(KAKAO_APP_KEY)</string>
</array>
</dict>
</array>
<key>FacebookAdvertiserIDCollectionEnabled</key>
<false/>
<key>FacebookAppID</key>
<string>$(FACEBOOK_APP_ID)</string>
<key>FacebookClientToken</key>
Expand Down
10 changes: 1 addition & 9 deletions SNUTT-2022/SNUTT/Repositories/NetworkUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -99,21 +99,13 @@ final class Logger: EventMonitor {
}

// Event called when any type of Request is resumed.
func requestDidResume(_ request: Request) {
debugPrint("Resuming: \(request)")
}
func requestDidResume(_: Request) {}

// Event called whenever a DataRequest has parsed a response.
func request<Value>(_: DataRequest, didParseResponse response: DataResponse<Value, AFError>) {
Task {
await logStore?.log(response: response)
}

if response.error == nil {
debugPrint("Finished: \(response)")
} else {
debugPrint("Error: \(response)")
}
}
}

Expand Down
14 changes: 6 additions & 8 deletions SNUTT-2022/SNUTT/Services/FriendsService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,13 @@
//

import Alamofire
import Combine
import Foundation
import ReactNativeKit

#if DEBUG
enum RNEvent: String, SupportedEvent {
case addFriendKakao = "add-friend-kakao"
}
#endif
enum RNEvent: String, SupportedEvent {
case addFriendKakao = "add-friend-kakao"
}

@MainActor
protocol FriendsServiceProtocol: Sendable {
Expand All @@ -24,9 +23,6 @@ struct FriendsService: FriendsServiceProtocol, ConfigsProvidable {
var appState: AppState
var webRepositories: AppEnvironment.WebRepositories
var localRepositories: AppEnvironment.LocalRepositories
#if DEBUG
static let eventEmitter: EventEmitter<RNEvent> = .init()
#endif

private let rnBundlePath = "ReactNativeBundles"

Expand All @@ -42,6 +38,8 @@ struct FriendsService: FriendsServiceProtocol, ConfigsProvidable {
localRepositories.userDefaultsRepository
}

// MARK: Kakao Friend Request

// MARK: RN Bundle Management

func fetchReactNativeBundleUrl() async throws -> URL {
Expand Down
105 changes: 105 additions & 0 deletions SNUTT-2022/SNUTT/ViewModels/FriendsViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,53 @@
// Created by 박신홍 on 2023/07/15.
//

import Combine
import Foundation
import KakaoSDKCommon
import KakaoSDKShare
import KakaoSDKTemplate
import ReactNativeKit
import UIKit

class FriendsViewModel: BaseViewModel, ObservableObject {
private let eventEmitter: EventEmitter<RNEvent> = .init()
private var cancellables = Set<AnyCancellable>()

@Published private(set) var friendRequestError: FriendRequestError?

var isErrorAlertPresented: Bool {
get { friendRequestError != nil }
set {
if !newValue {
friendRequestError = nil
}
}
}

override init(container: DIContainer) {
super.init(container: container)
appState.friend.$pendingFriendRequestToken
.compactMap { $0 }
.sink { token in
Task { @MainActor [weak self] in
guard let self else { return }
appState.friend.pendingFriendRequestToken = nil
await eventEmitter.emitEvent(.addFriendKakao, payload: ["requestToken": token])
}
}
.store(in: &cancellables)
}

var accessToken: String? {
appState.user.accessToken
}

func startListeningEvents() {
Task {
await listenJSEventStream()
}
}

func fetchReactNativeBundleUrl() async -> URL? {
do {
return try await services.friendsService.fetchReactNativeBundleUrl()
Expand All @@ -25,3 +61,72 @@ class FriendsViewModel: BaseViewModel, ObservableObject {
return nil
}
}

// MARK: Kakao Friend Request

extension FriendsViewModel {
enum FriendRequestError: LocalizedError {
case shareUnavailable
case preparationFailed
case unknownError

var errorDescription: String? {
switch self {
case .shareUnavailable:
"카카오톡 초대 기능을 사용할 수 없습니다"
case .preparationFailed, .unknownError:
"알 수 없는 오류가 발생했습니다"
}
}

var failureReason: String? {
nil
}

var helpAnchor: String? { nil }

var recoverySuggestion: String? {
switch self {
case .shareUnavailable:
"카카오톡이 설치되어 있는지 확인해보세요."
case .preparationFailed, .unknownError:
"개발자 괴롭히기를 통해 문의해주세요."
}
}
}

func listenJSEventStream() async {
for await event in await eventEmitter.jsEventStream {
switch event.name {
case RNEvent.addFriendKakao.rawValue:
guard let requestToken = event.payload?["requestToken"] as? String else { return }
do {
try await sendKakaoMessage(with: requestToken)
} catch let error as FriendRequestError {
self.friendRequestError = error
} catch {
assertionFailure(error.localizedDescription)
}
default:
continue
}
}
}

func sendKakaoMessage(with requestToken: String) async throws {
try await Task.sleep(nanoseconds: 500_000_000)
guard ShareApi.isKakaoTalkSharingAvailable() else { throw FriendRequestError.shareUnavailable }
let params = ["type": RNEvent.addFriendKakao.rawValue, "requestToken": requestToken]
let link = Link(androidExecutionParams: params, iosExecutionParams: params)
let button = Button(title: "수락하기", link: link)
let feedTemplate = FeedTemplate(content: .init(title: "SNUTT : 서울대학교 시간표 앱", imageUrl: URL(string: "https://is1-ssl.mzstatic.com/image/thumb/PurpleSource122/v4/f0/c6/58/f0c6581d-dd41-3bad-9d9a-516561d35af1/0d1dfc21-5d2e-4dcf-8cff-c6eb25fe7284_2_2.png/460x0w.webp"), description: "스누티티 친구 초대가 도착했어요", link: link), buttons: [button])
let feedTemplateJsonData = try SdkJSONEncoder.custom.encode(feedTemplate)
guard let templateJsonObject = SdkUtils.toJsonObject(feedTemplateJsonData) else { throw FriendRequestError.preparationFailed }
ShareApi.shared.shareDefault(templateObject: templateJsonObject) { sharingResult, _ in
if let sharingResult {
UIApplication.shared.open(sharingResult.url,
options: [:], completionHandler: nil)
}
}
}
}
Loading
Loading