diff --git a/SodaLive.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/SodaLive.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/SodaLive.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/SodaLive.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SodaLive.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..410e712 --- /dev/null +++ b/SodaLive.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,104 @@ +{ + "pins" : [ + { + "identity" : "abseil-cpp-binary", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/abseil-cpp-binary.git", + "state" : { + "revision" : "bfc0b6f81adc06ce5121eb23f628473638d67c5c", + "version" : "1.2022062300.0" + } + }, + { + "identity" : "firebase-ios-sdk", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/firebase-ios-sdk.git", + "state" : { + "revision" : "df2171b0c6afb9e9d4f7e07669d558c510b9f6be", + "version" : "10.13.0" + } + }, + { + "identity" : "googleappmeasurement", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleAppMeasurement.git", + "state" : { + "revision" : "03b9beee1a61f62d32c521e172e192a1663a5e8b", + "version" : "10.13.0" + } + }, + { + "identity" : "googledatatransport", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleDataTransport.git", + "state" : { + "revision" : "aae45a320fd0d11811820335b1eabc8753902a40", + "version" : "9.2.5" + } + }, + { + "identity" : "googleutilities", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/GoogleUtilities.git", + "state" : { + "revision" : "c38ce365d77b04a9a300c31061c5227589e5597b", + "version" : "7.11.5" + } + }, + { + "identity" : "grpc-binary", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/grpc-binary.git", + "state" : { + "revision" : "f1b366129d1125be7db83247e003fc333104b569", + "version" : "1.50.2" + } + }, + { + "identity" : "gtm-session-fetcher", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/gtm-session-fetcher.git", + "state" : { + "revision" : "d415594121c9e8a4f9d79cecee0965cf35e74dbd", + "version" : "3.1.1" + } + }, + { + "identity" : "leveldb", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/leveldb.git", + "state" : { + "revision" : "0706abcc6b0bd9cedfbb015ba840e4a780b5159b", + "version" : "1.22.2" + } + }, + { + "identity" : "nanopb", + "kind" : "remoteSourceControl", + "location" : "https://github.com/firebase/nanopb.git", + "state" : { + "revision" : "819d0a2173aff699fb8c364b6fb906f7cdb1a692", + "version" : "2.30909.0" + } + }, + { + "identity" : "promises", + "kind" : "remoteSourceControl", + "location" : "https://github.com/google/promises.git", + "state" : { + "revision" : "e70e889c0196c76d22759eb50d6a0270ca9f1d9e", + "version" : "2.3.1" + } + }, + { + "identity" : "swift-protobuf", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-protobuf.git", + "state" : { + "revision" : "ce20dc083ee485524b802669890291c0d8090170", + "version" : "1.22.1" + } + } + ], + "version" : 2 +} diff --git a/SodaLive/Resources/Assets.xcassets/splash_bubble.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/splash_bubble.imageset/Contents.json new file mode 100644 index 0000000..f20a3f8 --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/splash_bubble.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "splash_bubble.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SodaLive/Resources/Assets.xcassets/splash_bubble.imageset/splash_bubble.png b/SodaLive/Resources/Assets.xcassets/splash_bubble.imageset/splash_bubble.png new file mode 100644 index 0000000..647356e Binary files /dev/null and b/SodaLive/Resources/Assets.xcassets/splash_bubble.imageset/splash_bubble.png differ diff --git a/SodaLive/Resources/Assets.xcassets/splash_logo.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/splash_logo.imageset/Contents.json new file mode 100644 index 0000000..ae9fc61 --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/splash_logo.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "splash_logo.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SodaLive/Resources/Assets.xcassets/splash_logo.imageset/splash_logo.png b/SodaLive/Resources/Assets.xcassets/splash_logo.imageset/splash_logo.png new file mode 100644 index 0000000..e56aed8 Binary files /dev/null and b/SodaLive/Resources/Assets.xcassets/splash_logo.imageset/splash_logo.png differ diff --git a/SodaLive/Resources/Assets.xcassets/splash_text.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/splash_text.imageset/Contents.json new file mode 100644 index 0000000..1214b3d --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/splash_text.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "splash_text.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SodaLive/Resources/Assets.xcassets/splash_text.imageset/splash_text.png b/SodaLive/Resources/Assets.xcassets/splash_text.imageset/splash_text.png new file mode 100644 index 0000000..98e27f8 Binary files /dev/null and b/SodaLive/Resources/Assets.xcassets/splash_text.imageset/splash_text.png differ diff --git a/SodaLive/Resources/Assets.xcassets/vividnext_logo.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/vividnext_logo.imageset/Contents.json new file mode 100644 index 0000000..2937c4b --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/vividnext_logo.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "vividnext_logo.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SodaLive/Resources/Assets.xcassets/vividnext_logo.imageset/vividnext_logo.png b/SodaLive/Resources/Assets.xcassets/vividnext_logo.imageset/vividnext_logo.png new file mode 100644 index 0000000..ce3f969 Binary files /dev/null and b/SodaLive/Resources/Assets.xcassets/vividnext_logo.imageset/vividnext_logo.png differ diff --git a/SodaLive/Resources/Debug/GoogleService-Info.plist b/SodaLive/Resources/Debug/GoogleService-Info.plist new file mode 100644 index 0000000..17ddffb --- /dev/null +++ b/SodaLive/Resources/Debug/GoogleService-Info.plist @@ -0,0 +1,34 @@ + + + + + CLIENT_ID + 758414412471-rgobklhtodvt7c2crp4cmh7q1kd89nha.apps.googleusercontent.com + REVERSED_CLIENT_ID + com.googleusercontent.apps.758414412471-rgobklhtodvt7c2crp4cmh7q1kd89nha + API_KEY + AIzaSyAYscsMZzW1m0btcs6c2zkI8pLtcDE_Eqg + GCM_SENDER_ID + 758414412471 + PLIST_VERSION + 1 + BUNDLE_ID + kr.co.vividnext.sodalive.dev + PROJECT_ID + sodalive-test + STORAGE_BUCKET + sodalive-test.appspot.com + IS_ADS_ENABLED + + IS_ANALYTICS_ENABLED + + IS_APPINVITE_ENABLED + + IS_GCM_ENABLED + + IS_SIGNIN_ENABLED + + GOOGLE_APP_ID + 1:758414412471:ios:866b0814ab94bdf77a5b32 + + \ No newline at end of file diff --git a/SodaLive/Sources/App/AppDelegate.swift b/SodaLive/Sources/App/AppDelegate.swift new file mode 100644 index 0000000..e04318b --- /dev/null +++ b/SodaLive/Sources/App/AppDelegate.swift @@ -0,0 +1,110 @@ +// +// AppDelegate.swift +// SodaLive +// +// Created by klaus on 2023/08/09. +// + +import UIKit + +import FirebaseCore +import FirebaseMessaging + +class AppDelegate: UIResponder, UIApplicationDelegate { + + private let gcmMessageIDKey = "gcm.message_id" + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { + FirebaseApp.configure() + + Messaging.messaging().delegate = self + + // For iOS 10 display notification (sent via APNS) + UNUserNotificationCenter.current().delegate = self + + let authOptions: UNAuthorizationOptions = [.alert, .badge, .sound] + UNUserNotificationCenter.current().requestAuthorization( + options: authOptions, + completionHandler: {_, _ in }) + + application.registerForRemoteNotifications() + UIApplication.shared.applicationIconBadgeNumber = 0 + + return true + } + + func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { + Messaging.messaging().appDidReceiveMessage(userInfo) + // Print message ID. + if let messageID = userInfo[gcmMessageIDKey] { + DEBUG_LOG("Message ID: \(messageID)") + } + + // Print full message. + DEBUG_LOG("userInfo: \(userInfo)") + + completionHandler(UIBackgroundFetchResult.newData) + } + + func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { + UserDefaults.set(deviceToken, forKey: .devicePushToken) + Messaging.messaging().apnsToken = deviceToken + } + + func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { + } +} + +extension AppDelegate: MessagingDelegate { + func messaging(_ messaging: Messaging, didReceiveRegistrationToken fcmToken: String?) { + if let fcmToken = fcmToken { + DEBUG_LOG("fcmToken: \(fcmToken)") + UserDefaults.set(fcmToken, forKey: .pushToken) + } + } +} + +extension AppDelegate : UNUserNotificationCenterDelegate { + + // Receive displayed notifications for iOS 10 devices. + func userNotificationCenter( + _ center: UNUserNotificationCenter, + willPresent notification: UNNotification, + withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void + ) { + let userInfo = notification.request.content.userInfo + + // With swizzling disabled you must let Messaging know about the message, for Analytics + Messaging.messaging().appDidReceiveMessage(userInfo) + + // ... + + // Print full message. + DEBUG_LOG("userInfo: \(userInfo)") + + // Change this to your preferred presentation option + completionHandler([.banner, .badge, .sound]) + } + + func userNotificationCenter(_ center: UNUserNotificationCenter, + didReceive response: UNNotificationResponse, + withCompletionHandler completionHandler: @escaping () -> Void) { + let userInfo = response.notification.request.content.userInfo + + // With swizzling disabled you must let Messaging know about the message, for Analytics + Messaging.messaging().appDidReceiveMessage(userInfo) + + let roomIdString = userInfo["suda_room_id"] as? String + let audioContentIdString = userInfo["audio_content_id"] as? String + + if let roomIdString = roomIdString, let roomId = Int(roomIdString), roomId > 0 { + AppState.shared.pushRoomId = roomId + } + + if let audioContentIdString = audioContentIdString, let audioContentId = Int(audioContentIdString), audioContentId > 0 { + AppState.shared.pushAudioContentId = audioContentId + } + + completionHandler() + } +} diff --git a/SodaLive/Sources/App/AppState.swift b/SodaLive/Sources/App/AppState.swift new file mode 100644 index 0000000..a762fc0 --- /dev/null +++ b/SodaLive/Sources/App/AppState.swift @@ -0,0 +1,58 @@ +// +// AppState.swift +// SodaLive +// +// Created by klaus on 2023/08/09. +// + +import Foundation + +class AppState: ObservableObject { + static let shared = AppState() + + private var appStepBackStack = [AppStep]() + + @Published private(set) var appStep: AppStep = .splash + + @Published var isShowPlayer = false { + didSet { + if isShowPlayer { + } + } + } + + @Published var isShowNotificationSettingsDialog = false + + @Published var pushRoomId = 0 + @Published var pushChannelId = 0 + @Published var pushAudioContentId = 0 + @Published var roomId = 0 { + didSet { + if roomId <= 0 { + isShowPlayer = false + } + } + } + + func setAppStep(step: AppStep) { + switch step { + case .splash, .main: + appStepBackStack.removeAll() + + default: + appStepBackStack.append(appStep) + } + + DispatchQueue.main.async { + self.appStep = step + } + } + + func back() { + if let step = appStepBackStack.popLast() { + self.appStep = step + } else { + self.appStep = .main + } + } +} diff --git a/SodaLive/Sources/App/AppStep.swift b/SodaLive/Sources/App/AppStep.swift new file mode 100644 index 0000000..14aa8b4 --- /dev/null +++ b/SodaLive/Sources/App/AppStep.swift @@ -0,0 +1,14 @@ +// +// AppStep.swift +// SodaLive +// +// Created by klaus on 2023/08/09. +// + +import Foundation + +enum AppStep { + case splash + + case main +} diff --git a/SodaLive/Sources/App/SodaLiveApp.swift b/SodaLive/Sources/App/SodaLiveApp.swift index fee81fd..2ec257f 100644 --- a/SodaLive/Sources/App/SodaLiveApp.swift +++ b/SodaLive/Sources/App/SodaLiveApp.swift @@ -6,12 +6,81 @@ // import SwiftUI +import AppTrackingTransparency + +import FirebaseDynamicLinks @main struct SodaLiveApp: App { + + @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + + func handleIncomingDynamicLink(_ dynamicLink: DynamicLink) { + guard let url = dynamicLink.url else { + DEBUG_LOG("That's weired. My dynamic link object has no url") + return + } + + DEBUG_LOG("incoming link parameter is \(url.absoluteString)") + + let queryItems = URLComponents(url: url, resolvingAgainstBaseURL: true)?.queryItems + let roomId = queryItems?.filter({$0.name == "room_id"}).first?.value + let channelId = queryItems?.filter({$0.name == "channel_id"}).first?.value + let audioContentId = queryItems?.filter({$0.name == "audio_content_id"}).first?.value + + if let roomId = roomId { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + AppState.shared.pushRoomId = Int(roomId) ?? 0 + } + } + + if let channelId = channelId { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + AppState.shared.pushChannelId = Int(channelId) ?? 0 + } + } + + if let audioContentId = audioContentId { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + AppState.shared.pushAudioContentId = Int(audioContentId) ?? 0 + } + } + } + var body: some Scene { WindowGroup { ContentView() + .onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in + UIApplication.shared.applicationIconBadgeNumber = 0 + + ATTrackingManager.requestTrackingAuthorization { _ in } + } + .onOpenURL { url in + DEBUG_LOG("I have received a URL through a custom scheme! \(url.absoluteString)") + if let scheme = url.scheme { + if scheme == "kr.co.vividnext.yozm" { + if let dynamicLink = DynamicLinks.dynamicLinks().dynamicLink(fromCustomSchemeURL: url) { + self.handleIncomingDynamicLink(dynamicLink) + } else { + DEBUG_LOG("dynamic link fail") + } + } else { + DynamicLinks.dynamicLinks().handleUniversalLink(url) { dynamicLink, error in + guard error == nil else { + DEBUG_LOG("Found an error! \(error!.localizedDescription)") + DEBUG_LOG("dynamic link fail") + return + } + + if let dynamicLink = dynamicLink { + self.handleIncomingDynamicLink(dynamicLink) + } else { + DEBUG_LOG("dynamic link fail") + } + } + } + } + } } } } diff --git a/SodaLive/Sources/ContentView.swift b/SodaLive/Sources/ContentView.swift index 6e72655..2b0475d 100644 --- a/SodaLive/Sources/ContentView.swift +++ b/SodaLive/Sources/ContentView.swift @@ -8,14 +8,21 @@ import SwiftUI struct ContentView: View { + @StateObject private var appState = AppState.shared + var body: some View { - VStack { - Image(systemName: "globe") - .imageScale(.large) - .foregroundColor(.accentColor) - Text("Hello, world!") + ZStack { + Color.black.ignoresSafeArea() + + switch appState.appStep { + case .splash: + SplashView() + + default: + EmptyView() + .frame(width: 0, height: 0, alignment: .topLeading) + } } - .padding() } } diff --git a/SodaLive/Sources/Debug/Utils/Constants.swift b/SodaLive/Sources/Debug/Utils/Constants.swift new file mode 100644 index 0000000..c4f5ca3 --- /dev/null +++ b/SodaLive/Sources/Debug/Utils/Constants.swift @@ -0,0 +1,16 @@ +// +// Constants.swift +// SodaLive-dev +// +// Created by klaus on 2023/08/09. +// + +import Foundation + +let BASE_URL = "https://test-api.sodalive.net" +let APPLY_YOZM_CREATOR = "https://forms.gle/mmhJKmijjVRnwtdZ7" + +let AGORA_APP_ID = "b96574e191a9430fa54c605528aa3ef7" +let AGORA_APP_CERTIFICATE = "ae18ade3afcf4086bd4397726eb0654c" + +let BOOTPAY_APP_ID = "6242a7772701800023f68b2f" diff --git a/SodaLive/Sources/Extensions/ColorExtension.swift b/SodaLive/Sources/Extensions/ColorExtension.swift new file mode 100644 index 0000000..742e4ad --- /dev/null +++ b/SodaLive/Sources/Extensions/ColorExtension.swift @@ -0,0 +1,40 @@ +// +// ColorExtension.swift +// SodaLive +// +// Created by klaus on 2023/08/09. +// +// https://seons-dev.tistory.com/174 +// + +import SwiftUI + +extension Color { + init(hex: String) { + let scanner = Scanner(string: hex) + _ = scanner.scanString("#") + + var rgb: UInt64 = 0 + scanner.scanHexInt64(&rgb) + + let r = Double((rgb >> 16) & 0xFF) / 255.0 + let g = Double((rgb >> 8) & 0xFF) / 255.0 + let b = Double((rgb >> 0) & 0xFF) / 255.0 + self.init(red: r, green: g, blue: b) + } +} + +extension UIColor { + convenience init(hex: String, alpha: CGFloat = 1.0) { + let scanner = Scanner(string: hex) + _ = scanner.scanString("#") + + var rgb: UInt64 = 0 + scanner.scanHexInt64(&rgb) + + let r = Double((rgb >> 16) & 0xFF) / 255.0 + let g = Double((rgb >> 8) & 0xFF) / 255.0 + let b = Double((rgb >> 0) & 0xFF) / 255.0 + self.init(red: r, green: g, blue: b, alpha: alpha) + } +} diff --git a/SodaLive/Sources/Extensions/DateExtension.swift b/SodaLive/Sources/Extensions/DateExtension.swift new file mode 100644 index 0000000..d983c3a --- /dev/null +++ b/SodaLive/Sources/Extensions/DateExtension.swift @@ -0,0 +1,21 @@ +// +// DateExtension.swift +// yozm +// +// Created by klaus on 2022/05/27. +// + +import Foundation + +extension Date { + func convertDateFormat(dateFormat: String = "yyyy.MM.dd") -> String { + let formatter = DateFormatter() + formatter.dateFormat = dateFormat + formatter.locale = Locale(identifier: "ko") + return formatter.string(from: self) + } + + func currentTimeMillis() -> Int64 { + return Int64(self.timeIntervalSince1970 * 1000) + } +} diff --git a/SodaLive/Sources/Extensions/IntExtension.swift b/SodaLive/Sources/Extensions/IntExtension.swift new file mode 100644 index 0000000..310a247 --- /dev/null +++ b/SodaLive/Sources/Extensions/IntExtension.swift @@ -0,0 +1,32 @@ +// +// IntExtension.swift +// yozm +// +// Created by klaus on 2022/06/21. +// + +import Foundation + +extension Int64 { + func durationText() -> String { + let duration = self + + let convertedTime = Int(duration / 1000) + let hour = Int(convertedTime / 3600) + let minute = Int(convertedTime / 60) % 60 + let second = Int(convertedTime % 60) + + // update UI + var timeText = [String]() + + if hour > 0 { + timeText.append(String(hour)) + timeText.append(String(format: "%02d", minute)) + } else { + timeText.append(String(format: "%02d", minute)) + timeText.append(String(format: "%02d", second)) + } + + return timeText.joined(separator: ":") + } +} diff --git a/SodaLive/Sources/Extensions/StringExtension.swift b/SodaLive/Sources/Extensions/StringExtension.swift new file mode 100644 index 0000000..d49fda3 --- /dev/null +++ b/SodaLive/Sources/Extensions/StringExtension.swift @@ -0,0 +1,43 @@ +// +// StringExtension.swift +// yozm +// +// Created by klaus on 2022/06/03. +// + +import Foundation + +extension Optional where Wrapped == String { + func isNullOrBlank() -> Bool { + return self == nil || self!.trimmingCharacters(in: .whitespaces).isEmpty + } +} + +extension String { + func convertDateFormat(from: String, to: String) -> String { + let fromFormatter = DateFormatter() + fromFormatter.dateFormat = from + fromFormatter.timeZone = TimeZone(identifier: TimeZone.current.identifier) + + if let date = fromFormatter.date(from: self) { + return date.convertDateFormat(dateFormat: to) + } else { + return self + } + } + + func substring(from: Int, to: Int) -> String { + guard from < count, to >= 0, to - from >= 0 else { + return "" + } + + // Index 값 획득 + let startIndex = index(self.startIndex, offsetBy: from) + let endIndex = index(self.startIndex, offsetBy: to + 1) // '+1'이 있는 이유: endIndex는 문자열의 마지막 그 다음을 가리키기 때문 + + // 파싱 + return String(self[startIndex ..< endIndex]) + + // 출처 - https://ios-development.tistory.com/379 + } +} diff --git a/SodaLive/Sources/Extensions/UserDefaultsExtension.swift b/SodaLive/Sources/Extensions/UserDefaultsExtension.swift new file mode 100644 index 0000000..b572a77 --- /dev/null +++ b/SodaLive/Sources/Extensions/UserDefaultsExtension.swift @@ -0,0 +1,72 @@ +// +// UserDefaultsExtension.swift +// yozm +// +// Created by klaus on 2022/05/20. +// + +import Foundation + +enum UserDefaultsKey: String, CaseIterable { + case auth + case role + case coin + case token + case email + case userId + case nickname + case pushToken + case profileImage + case voipPushToken + case devicePushToken + case isContentPlayLoop + case isFollowedCreatorLive + case isViewedOnboardingView + case notShowingEventPopupId +} + +extension UserDefaults { + static func set(_ value: Bool, forKey key: UserDefaultsKey) { + let key = key.rawValue + UserDefaults.standard.set(value, forKey: key) + } + + static func bool(forKey key: UserDefaultsKey) -> Bool { + let key = key.rawValue + return UserDefaults.standard.bool(forKey: key) + } + + static func set(_ value: String, forKey key: UserDefaultsKey) { + let key = key.rawValue + UserDefaults.standard.set(value, forKey: key) + } + + static func string(forKey key: UserDefaultsKey) -> String { + let key = key.rawValue + return UserDefaults.standard.string(forKey: key) ?? "" + } + + static func set(_ value: Int, forKey key: UserDefaultsKey) { + let key = key.rawValue + UserDefaults.standard.set(value, forKey: key) + } + + static func int(forKey key: UserDefaultsKey) -> Int { + let key = key.rawValue + return UserDefaults.standard.integer(forKey: key) + } + + static func set(_ value: Data, forKey key: UserDefaultsKey) { + let key = key.rawValue + UserDefaults.standard.set(value, forKey: key) + } + + static func data(forKey key: UserDefaultsKey) -> Data? { + let key = key.rawValue + return UserDefaults.standard.data(forKey: key) + } + + static func reset() { + UserDefaultsKey.allCases.forEach { UserDefaults.standard.removeObject(forKey: $0.rawValue) } + } +} diff --git a/SodaLive/Sources/Extensions/ViewExtension.swift b/SodaLive/Sources/Extensions/ViewExtension.swift new file mode 100644 index 0000000..df65c57 --- /dev/null +++ b/SodaLive/Sources/Extensions/ViewExtension.swift @@ -0,0 +1,49 @@ +// +// ViewExtension.swift +// yozm +// +// Created by klaus on 2022/05/19. +// + +import SwiftUI +import Combine + +extension View { + func screenSize() -> CGRect { + return UIScreen.main.bounds + } + + // 출처 - https://stackoverflow.com/a/58606176 + func cornerRadius(_ radius: CGFloat, corners: UIRectCorner) -> some View { + clipShape( RoundedCorner(radius: radius, corners: corners) ) + } + + func hideKeyboard() { + UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) + } + + /// A backwards compatible wrapper for iOS 14 `onChange` + @ViewBuilder func valueChanged(value: T, onChange: @escaping (T) -> Void) -> some View { + if #available(iOS 14.0, *) { + self.onChange(of: value, perform: onChange) + } else { + self.onReceive(Just(value)) { (value) in + onChange(value) + } + } + } + + @ViewBuilder func scrollContentBackgroundHidden() -> some View { + if #available(iOS 16.0, *) { + self.scrollContentBackground(.hidden) + } + } + + @ViewBuilder func progressColor(color: Color) -> some View { + if #available(iOS 16.0, *) { + self.tint(color) + } else { + self.accentColor(color) + } + } +} diff --git a/SodaLive/Sources/Shape/RoundedCorner.swift b/SodaLive/Sources/Shape/RoundedCorner.swift new file mode 100644 index 0000000..16587e3 --- /dev/null +++ b/SodaLive/Sources/Shape/RoundedCorner.swift @@ -0,0 +1,20 @@ +// +// RoundedCorner.swift +// SodaLive +// +// Created by klaus on 2023/08/09. +// + +import SwiftUI + +// 출처 - https://stackoverflow.com/a/58606176 + +struct RoundedCorner: Shape { + var radius: CGFloat = .infinity + var corners: UIRectCorner = .allCorners + + func path(in rect: CGRect) -> Path { + let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius)) + return Path(path.cgPath) + } +} diff --git a/SodaLive/Sources/Splash/SplashView.swift b/SodaLive/Sources/Splash/SplashView.swift new file mode 100644 index 0000000..2b02528 --- /dev/null +++ b/SodaLive/Sources/Splash/SplashView.swift @@ -0,0 +1,106 @@ +// +// SplashView.swift +// SodaLive +// +// Created by klaus on 2023/08/09. +// + +import SwiftUI +import FirebaseRemoteConfig + +struct SplashView: View { + + @State private var isShowForcedUpdatePopup = false + @State private var isShowUpdatePopup = false + + var body: some View { + ZStack(alignment: .top) { + LinearGradient( + gradient: Gradient(colors: [Color(hex: "a0e2ff"), Color(hex: "ecfaff")]), + startPoint: .bottom, + endPoint: .top + ).ignoresSafeArea() + + Image("splash_bubble") + .padding(.top, 11) + + VStack(spacing: 0) { + Image("splash_text") + .padding(.top, 111) + + Image("splash_logo") + .padding(.top, 77.3) + + Spacer() + + Image("vividnext_logo") + .padding(.bottom, 36) + } + } + .onAppear { + fetchLastestVersion() + } + } + + private func fetchLastestVersion() { + let remoteConfig = RemoteConfig.remoteConfig() + let configSettings = RemoteConfigSettings() + configSettings.minimumFetchInterval = 300 + remoteConfig.configSettings = configSettings + + remoteConfig.fetch { status, error in + if status == .success { + remoteConfig.activate { changed, error in + checkAppVersion(latestVersion: remoteConfig["ios_latest_version"].stringValue) + } + } else { + withAnimation { + DispatchQueue.main.asyncAfter(deadline: .now() + 2.5) { + AppState.shared.setAppStep(step: .main) + } + } + } + } + } + + private func checkAppVersion(latestVersion: String?) { + let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String + + if let latestVersion = latestVersion, let version = version { + let latestVersions = latestVersion.split(separator: ".") + let versions = version.split(separator: ".") + + let latestMajor = Int(latestVersions[0])! + let latestMinor = Int(latestVersions[1])! + let latestPatch = Int(latestVersions[2])! + + let major = Int(versions[0])! + let minor = Int(versions[1])! + let patch = Int(versions[2])! + + if latestMajor > major || (latestMajor == major && latestMinor > minor) { + self.isShowForcedUpdatePopup = true + } else if latestMajor == major && latestMinor == minor && latestPatch > patch { + self.isShowUpdatePopup = true + } else { + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + withAnimation { + AppState.shared.setAppStep(step: .main) + } + } + } + } else { + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + withAnimation { + AppState.shared.setAppStep(step: .main) + } + } + } + } +} + +struct SplashView_Previews: PreviewProvider { + static var previews: some View { + SplashView() + } +} diff --git a/SodaLive/Sources/Utils/Constants.swift b/SodaLive/Sources/Utils/Constants.swift new file mode 100644 index 0000000..857ff86 --- /dev/null +++ b/SodaLive/Sources/Utils/Constants.swift @@ -0,0 +1,16 @@ +// +// Constants.swift +// SodaLive +// +// Created by klaus on 2023/08/09. +// + +import Foundation + +let BASE_URL = "https://api.sodalive.net" +let APPLY_YOZM_CREATOR = "https://forms.gle/mmhJKmijjVRnwtdZ7" + +let AGORA_APP_ID = "e34e40046e9847baba3adfe2b8ffb4f6" +let AGORA_APP_CERTIFICATE = "15cadeea4ba94ff7b091c9a10f4bf4a6" + +let BOOTPAY_APP_ID = "64c35be1d25985001dc50c88" diff --git a/SodaLive/Sources/Utils/Log.swift b/SodaLive/Sources/Utils/Log.swift new file mode 100644 index 0000000..b2e8011 --- /dev/null +++ b/SodaLive/Sources/Utils/Log.swift @@ -0,0 +1,23 @@ +// +// Log.swift +// yozm +// +// Created by klaus on 2022/05/20. +// + +import Foundation + +func DEBUG_LOG(_ msg: String, file: String = #file, function: String = #function, line: Int = #line) { + #if DEBUG + let filename = file.split(separator: "/").last ?? "" + let funcName = function.split(separator: "(").first ?? "" + print("👻 [\(filename)] \(funcName)(\(line)): \(msg)") + #endif +} + +func ERROR_LOG(_ msg: String, file: String = #file, function: String = #function, line: Int = #line) { + let filename = file.split(separator: "/").last ?? "" + let funcName = function.split(separator: "(").first ?? "" + print("🤯😡 [\(filename)] \(funcName)(\(line)): \(msg)") +} +