Skip to content

Commit

Permalink
Implement friend request via Kakao (#306)
Browse files Browse the repository at this point in the history
* Implement friend request via Kakao

* Apply SwiftFormat changes

* Fix as reviews

* Apply SwiftFormat changes

* Bump ReactNativeKit version

* Close modal before sending request

* Merge from master

* Add sleep

---------

Co-authored-by: shp7724 <[email protected]>
  • Loading branch information
shp7724 and shp7724 authored Oct 2, 2024
1 parent 2d8cfc4 commit 7d7b6e1
Show file tree
Hide file tree
Showing 12 changed files with 195 additions and 48 deletions.
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

0 comments on commit 7d7b6e1

Please sign in to comment.