feat(auth): 인증 후 콘텐츠 설정과 개발용 모의 인증을 추가한다
This commit is contained in:
@@ -7,6 +7,12 @@
|
|||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
|
AC5200013012000000A5BBA3 /* AuthVerificationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC5200003012000000A5BBA3 /* AuthVerificationView.swift */; };
|
||||||
|
AC5200023012000000A5BBA3 /* AuthVerificationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC5200003012000000A5BBA3 /* AuthVerificationView.swift */; };
|
||||||
|
AC5200043012000000A5BBA3 /* AuthVerificationMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC5200033012000000A5BBA3 /* AuthVerificationMock.swift */; };
|
||||||
|
AC5200053012000000A5BBA3 /* AuthVerificationMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC5200033012000000A5BBA3 /* AuthVerificationMock.swift */; };
|
||||||
|
AC5100013012000000A5BBA3 /* AuthContentSettingsSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC5100003012000000A5BBA3 /* AuthContentSettingsSheet.swift */; };
|
||||||
|
AC5100023012000000A5BBA3 /* AuthContentSettingsSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = AC5100003012000000A5BBA3 /* AuthContentSettingsSheet.swift */; };
|
||||||
00C796627A6758D271215EC5 /* CreatorProfileItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26881ED30292E28D2E5F84CD /* CreatorProfileItem.swift */; };
|
00C796627A6758D271215EC5 /* CreatorProfileItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26881ED30292E28D2E5F84CD /* CreatorProfileItem.swift */; };
|
||||||
042BEE0C973700FB5F4540CD /* I18n.swift in Sources */ = {isa = PBXBuildFile; fileRef = 826D626B7714E5C28ED31074 /* I18n.swift */; };
|
042BEE0C973700FB5F4540CD /* I18n.swift in Sources */ = {isa = PBXBuildFile; fileRef = 826D626B7714E5C28ED31074 /* I18n.swift */; };
|
||||||
0470E68C6F302A131264E3AF /* ChannelDonationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DF2BFD2E09F509C44C92214 /* ChannelDonationViewModel.swift */; };
|
0470E68C6F302A131264E3AF /* ChannelDonationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DF2BFD2E09F509C44C92214 /* ChannelDonationViewModel.swift */; };
|
||||||
@@ -2202,6 +2208,9 @@
|
|||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
|
AC5200003012000000A5BBA3 /* AuthVerificationView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AuthVerificationView.swift; sourceTree = "<group>"; };
|
||||||
|
AC5200033012000000A5BBA3 /* AuthVerificationMock.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AuthVerificationMock.swift; sourceTree = "<group>"; };
|
||||||
|
AC5100003012000000A5BBA3 /* AuthContentSettingsSheet.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AuthContentSettingsSheet.swift; sourceTree = "<group>"; };
|
||||||
002435A7145D59CCCA8F76D1 /* CreatorChannelFanTalkApi.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CreatorChannelFanTalkApi.swift; sourceTree = "<group>"; };
|
002435A7145D59CCCA8F76D1 /* CreatorChannelFanTalkApi.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CreatorChannelFanTalkApi.swift; sourceTree = "<group>"; };
|
||||||
0BBDC3ECBBE049EAA4057D3A /* PostChannelDonationRequest.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PostChannelDonationRequest.swift; sourceTree = "<group>"; };
|
0BBDC3ECBBE049EAA4057D3A /* PostChannelDonationRequest.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PostChannelDonationRequest.swift; sourceTree = "<group>"; };
|
||||||
0EC05859619BDC03056911E7 /* MainTab.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MainTab.swift; sourceTree = "<group>"; };
|
0EC05859619BDC03056911E7 /* MainTab.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MainTab.swift; sourceTree = "<group>"; };
|
||||||
@@ -3355,6 +3364,8 @@
|
|||||||
25C5D0734F54C0ED7B7BD462 /* Modal */ = {
|
25C5D0734F54C0ED7B7BD462 /* Modal */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
AC5200003012000000A5BBA3 /* AuthVerificationView.swift */,
|
||||||
|
AC5100003012000000A5BBA3 /* AuthContentSettingsSheet.swift */,
|
||||||
25C5D0714F54C0ED7B7BD462 /* SodaV2ActionModal.swift */,
|
25C5D0714F54C0ED7B7BD462 /* SodaV2ActionModal.swift */,
|
||||||
);
|
);
|
||||||
path = Modal;
|
path = Modal;
|
||||||
@@ -6631,6 +6642,7 @@
|
|||||||
F18179AD98929A1E018058E0 /* V2 */ = {
|
F18179AD98929A1E018058E0 /* V2 */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
AC5200063012000000A5BBA3 /* Auth */,
|
||||||
976A594BED288A98C0EEA03C /* Component */,
|
976A594BED288A98C0EEA03C /* Component */,
|
||||||
D979B1B9166521052893A897 /* Main */,
|
D979B1B9166521052893A897 /* Main */,
|
||||||
CC0100242FF0100000A5BBA3 /* CreatorChannel */,
|
CC0100242FF0100000A5BBA3 /* CreatorChannel */,
|
||||||
@@ -6639,6 +6651,14 @@
|
|||||||
path = V2;
|
path = V2;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
AC5200063012000000A5BBA3 /* Auth */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
AC5200033012000000A5BBA3 /* AuthVerificationMock.swift */,
|
||||||
|
);
|
||||||
|
path = Auth;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
F4F9FC33C494BBFD8CE96749 /* Models */ = {
|
F4F9FC33C494BBFD8CE96749 /* Models */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -7003,6 +7023,9 @@
|
|||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
AC5200013012000000A5BBA3 /* AuthVerificationView.swift in Sources */,
|
||||||
|
AC5200043012000000A5BBA3 /* AuthVerificationMock.swift in Sources */,
|
||||||
|
AC5100013012000000A5BBA3 /* AuthContentSettingsSheet.swift in Sources */,
|
||||||
A4F100000000000000000001 /* LottieRefreshIndicator.swift in Sources */,
|
A4F100000000000000000001 /* LottieRefreshIndicator.swift in Sources */,
|
||||||
A4F100000000000000000003 /* LottieRefreshableScrollView.swift in Sources */,
|
A4F100000000000000000003 /* LottieRefreshableScrollView.swift in Sources */,
|
||||||
400EDE592B31D2D7008E37B4 /* CreatorCommunityApi.swift in Sources */,
|
400EDE592B31D2D7008E37B4 /* CreatorCommunityApi.swift in Sources */,
|
||||||
@@ -8138,6 +8161,9 @@
|
|||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
AC5200023012000000A5BBA3 /* AuthVerificationView.swift in Sources */,
|
||||||
|
AC5200053012000000A5BBA3 /* AuthVerificationMock.swift in Sources */,
|
||||||
|
AC5100023012000000A5BBA3 /* AuthContentSettingsSheet.swift in Sources */,
|
||||||
A4F100000000000000000002 /* LottieRefreshIndicator.swift in Sources */,
|
A4F100000000000000000002 /* LottieRefreshIndicator.swift in Sources */,
|
||||||
A4F100000000000000000004 /* LottieRefreshableScrollView.swift in Sources */,
|
A4F100000000000000000004 /* LottieRefreshableScrollView.swift in Sources */,
|
||||||
400EDE5A2B31D2D7008E37B4 /* CreatorCommunityApi.swift in Sources */,
|
400EDE5A2B31D2D7008E37B4 /* CreatorCommunityApi.swift in Sources */,
|
||||||
@@ -9340,6 +9366,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = 959B90DBB4BB5911AB10C442 /* Pods-SodaLive-dev.debug.xcconfig */;
|
baseConfigurationReference = 959B90DBB4BB5911AB10C442 /* Pods-SodaLive-dev.debug.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) AUTH_VERIFICATION_MOCK";
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_ENTITLEMENTS = "SodaLive-dev.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "SodaLive-dev.entitlements";
|
||||||
@@ -9380,6 +9407,7 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = B108032F3DC7110CBFD04130 /* Pods-SodaLive-dev.release.xcconfig */;
|
baseConfigurationReference = B108032F3DC7110CBFD04130 /* Pods-SodaLive-dev.release.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) AUTH_VERIFICATION_MOCK";
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_ENTITLEMENTS = "SodaLive-dev.entitlements";
|
CODE_SIGN_ENTITLEMENTS = "SodaLive-dev.entitlements";
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Bootpay
|
import Bootpay
|
||||||
import BootpayUI
|
|
||||||
struct ChatTabView: View {
|
struct ChatTabView: View {
|
||||||
@AppStorage("token") private var token: String = UserDefaults.string(forKey: UserDefaultsKey.token)
|
@AppStorage("token") private var token: String = UserDefaults.string(forKey: UserDefaultsKey.token)
|
||||||
@AppStorage("auth") private var auth: Bool = UserDefaults.bool(forKey: UserDefaultsKey.auth)
|
@AppStorage("auth") private var auth: Bool = UserDefaults.bool(forKey: UserDefaultsKey.auth)
|
||||||
@@ -159,33 +158,30 @@ struct ChatTabView: View {
|
|||||||
payload.orderName = "본인인증"
|
payload.orderName = "본인인증"
|
||||||
payload.authenticationId = "\(UserDefaults.string(forKey: .nickname))__\(String(NSTimeIntervalSince1970))"
|
payload.authenticationId = "\(UserDefaults.string(forKey: .nickname))__\(String(NSTimeIntervalSince1970))"
|
||||||
}
|
}
|
||||||
.fullScreenCover(isPresented: $isShowAuthView) {
|
.fullScreenCover(isPresented: $isShowAuthView, onDismiss: mypageViewModel.presentAuthContentSettings) {
|
||||||
BootpayUI(payload: payload, requestType: BootpayRequest.TYPE_AUTHENTICATION)
|
AuthVerificationView(
|
||||||
.onConfirm { _ in
|
payload: payload,
|
||||||
true
|
onCancel: {
|
||||||
}
|
|
||||||
.onCancel { _ in
|
|
||||||
isShowAuthView = false
|
isShowAuthView = false
|
||||||
}
|
},
|
||||||
.onError { _ in
|
onError: {
|
||||||
AppState.shared.errorMessage = I18n.Chat.Auth.authenticationError
|
AppState.shared.errorMessage = I18n.Chat.Auth.authenticationError
|
||||||
AppState.shared.isShowErrorPopup = true
|
AppState.shared.isShowErrorPopup = true
|
||||||
isShowAuthView = false
|
isShowAuthView = false
|
||||||
}
|
},
|
||||||
.onDone {
|
onDone: {
|
||||||
DEBUG_LOG("onDone: \($0)")
|
let action = pendingAction
|
||||||
mypageViewModel.authVerify($0) {
|
mypageViewModel.authVerify($0) {
|
||||||
auth = true
|
auth = true
|
||||||
isShowAuthView = false
|
pendingAction = nil
|
||||||
if let action = pendingAction {
|
action?()
|
||||||
pendingAction = nil
|
|
||||||
action()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
isShowAuthView = false
|
||||||
.onClose {
|
},
|
||||||
|
onClose: {
|
||||||
isShowAuthView = false
|
isShowAuthView = false
|
||||||
}
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if isShowAuthConfirmView {
|
if isShowAuthConfirmView {
|
||||||
@@ -206,6 +202,7 @@ struct ChatTabView: View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.authContentSettings(viewModel: mypageViewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,14 @@ extension UserDefaults {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static func set(_ value: String, forKey key: UserDefaultsKey) {
|
static func set(_ value: String, forKey key: UserDefaultsKey) {
|
||||||
|
#if AUTH_VERIFICATION_MOCK
|
||||||
|
if key == .token {
|
||||||
|
AuthVerificationMock.shared.resetSession {
|
||||||
|
UserDefaults.standard.set(value, forKey: key.rawValue)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
#endif
|
||||||
let key = key.rawValue
|
let key = key.rawValue
|
||||||
UserDefaults.standard.set(value, forKey: key)
|
UserDefaults.standard.set(value, forKey: key)
|
||||||
}
|
}
|
||||||
@@ -78,6 +86,12 @@ extension UserDefaults {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static func reset() {
|
static func reset() {
|
||||||
|
#if AUTH_VERIFICATION_MOCK
|
||||||
|
AuthVerificationMock.shared.resetSession {
|
||||||
|
UserDefaultsKey.allCases.filter { $0 != .pushToken }.forEach { UserDefaults.standard.removeObject(forKey: $0.rawValue) }
|
||||||
|
}
|
||||||
|
#else
|
||||||
UserDefaultsKey.allCases.filter { $0 != .pushToken }.forEach { UserDefaults.standard.removeObject(forKey: $0.rawValue) }
|
UserDefaultsKey.allCases.filter { $0 != .pushToken }.forEach { UserDefaults.standard.removeObject(forKey: $0.rawValue) }
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Bootpay
|
import Bootpay
|
||||||
import BootpayUI
|
|
||||||
|
|
||||||
struct HomeTabView: View {
|
struct HomeTabView: View {
|
||||||
@StateObject var viewModel = HomeTabViewModel()
|
@StateObject var viewModel = HomeTabViewModel()
|
||||||
@@ -479,36 +478,34 @@ struct HomeTabView: View {
|
|||||||
|
|
||||||
viewModel.fetchData()
|
viewModel.fetchData()
|
||||||
}
|
}
|
||||||
.fullScreenCover(isPresented: $isShowAuthView) {
|
.fullScreenCover(isPresented: $isShowAuthView, onDismiss: mypageViewModel.presentAuthContentSettings) {
|
||||||
BootpayUI(payload: payload, requestType: BootpayRequest.TYPE_AUTHENTICATION)
|
AuthVerificationView(
|
||||||
.onConfirm { _ in
|
payload: payload,
|
||||||
true
|
onCancel: {
|
||||||
}
|
|
||||||
.onCancel { _ in
|
|
||||||
isShowAuthView = false
|
isShowAuthView = false
|
||||||
}
|
},
|
||||||
.onError { _ in
|
onError: {
|
||||||
AppState.shared.errorMessage = I18n.Chat.Auth.authenticationError
|
AppState.shared.errorMessage = I18n.Chat.Auth.authenticationError
|
||||||
AppState.shared.isShowErrorPopup = true
|
AppState.shared.isShowErrorPopup = true
|
||||||
isShowAuthView = false
|
isShowAuthView = false
|
||||||
}
|
},
|
||||||
.onDone {
|
onDone: {
|
||||||
DEBUG_LOG("onDone: \($0)")
|
let action = pendingAction
|
||||||
mypageViewModel.authVerify($0) {
|
mypageViewModel.authVerify($0) {
|
||||||
auth = true
|
auth = true
|
||||||
isShowAuthView = false
|
pendingAction = nil
|
||||||
if let action = pendingAction {
|
action?()
|
||||||
pendingAction = nil
|
|
||||||
action()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
isShowAuthView = false
|
||||||
.onClose {
|
},
|
||||||
|
onClose: {
|
||||||
isShowAuthView = false
|
isShowAuthView = false
|
||||||
}
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||||
|
.authContentSettings(viewModel: mypageViewModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1653,6 +1653,46 @@ enum I18n {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum Content {
|
enum Content {
|
||||||
|
static var authSensitiveContentOnDescription: String {
|
||||||
|
pick(
|
||||||
|
ko: "성인인증 완료 후 성인 콘텐츠를 볼 수 있어요",
|
||||||
|
en: "You can view adult content after age verification.",
|
||||||
|
ja: "年齢認証完了後、成人向けコンテンツを表示できます"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
static var authSensitiveContentOffDescription: String {
|
||||||
|
pick(
|
||||||
|
ko: "성인 콘텐츠가 목록과 검색에 노출되지 않아요",
|
||||||
|
en: "Adult content will not appear in lists or search.",
|
||||||
|
ja: "成人向けコンテンツは一覧や検索に表示されません"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
static var preferredContent: String {
|
||||||
|
pick(ko: "선호 콘텐츠", en: "Preferred content", ja: "好みのコンテンツ")
|
||||||
|
}
|
||||||
|
|
||||||
|
static var authSettingsGuide: String {
|
||||||
|
pick(
|
||||||
|
ko: "마이 > 설정 > 콘텐츠 보기 설정에서 언제든 바꿀 수 있어요",
|
||||||
|
en: "You can change this anytime in My > Settings > Content view settings.",
|
||||||
|
ja: "マイ > 設定 > コンテンツ表示設定でいつでも変更できます"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
static var confirmAuthSettings: String {
|
||||||
|
pick(ko: "설정 완료", en: "Confirm settings", ja: "設定を完了")
|
||||||
|
}
|
||||||
|
|
||||||
|
static var selectedPreference: String {
|
||||||
|
pick(ko: "선택됨", en: "Selected", ja: "選択済み")
|
||||||
|
}
|
||||||
|
|
||||||
|
static var unselectedPreference: String {
|
||||||
|
pick(ko: "선택 안 됨", en: "Not selected", ja: "未選択")
|
||||||
|
}
|
||||||
|
|
||||||
static var sensitiveContentTitle: String {
|
static var sensitiveContentTitle: String {
|
||||||
pick(ko: "민감한 콘텐츠 보기", en: "Show sensitive content", ja: "センシティブなコンテンツ表示")
|
pick(ko: "민감한 콘텐츠 보기", en: "Show sensitive content", ja: "センシティブなコンテンツ表示")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import RefreshableScrollView
|
import RefreshableScrollView
|
||||||
import Bootpay
|
import Bootpay
|
||||||
import BootpayUI
|
|
||||||
|
|
||||||
struct LiveNowAllView: View {
|
struct LiveNowAllView: View {
|
||||||
|
|
||||||
@@ -126,35 +125,33 @@ struct LiveNowAllView: View {
|
|||||||
|
|
||||||
viewModel.getLiveNowList()
|
viewModel.getLiveNowList()
|
||||||
}
|
}
|
||||||
.fullScreenCover(isPresented: $isShowAuthView) {
|
.fullScreenCover(isPresented: $isShowAuthView, onDismiss: mypageViewModel.presentAuthContentSettings) {
|
||||||
BootpayUI(payload: payload, requestType: BootpayRequest.TYPE_AUTHENTICATION)
|
AuthVerificationView(
|
||||||
.onConfirm { _ in
|
payload: payload,
|
||||||
true
|
onCancel: {
|
||||||
}
|
|
||||||
.onCancel { _ in
|
|
||||||
isShowAuthView = false
|
isShowAuthView = false
|
||||||
}
|
},
|
||||||
.onError { _ in
|
onError: {
|
||||||
AppState.shared.errorMessage = I18n.Main.Auth.authenticationError
|
AppState.shared.errorMessage = I18n.Main.Auth.authenticationError
|
||||||
AppState.shared.isShowErrorPopup = true
|
AppState.shared.isShowErrorPopup = true
|
||||||
isShowAuthView = false
|
isShowAuthView = false
|
||||||
}
|
},
|
||||||
.onDone {
|
onDone: {
|
||||||
DEBUG_LOG("onDone: \($0)")
|
let action = pendingAction
|
||||||
mypageViewModel.authVerify($0) {
|
mypageViewModel.authVerify($0) {
|
||||||
auth = true
|
auth = true
|
||||||
isShowAuthView = false
|
pendingAction = nil
|
||||||
if let action = pendingAction {
|
action?()
|
||||||
pendingAction = nil
|
|
||||||
action()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
isShowAuthView = false
|
||||||
.onClose {
|
},
|
||||||
|
onClose: {
|
||||||
isShowAuthView = false
|
isShowAuthView = false
|
||||||
}
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||||
|
.authContentSettings(viewModel: mypageViewModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handleLiveNowItemTap(roomId: Int, isAdult: Bool) {
|
private func handleLiveNowItemTap(roomId: Int, isAdult: Bool) {
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import SwiftUI
|
|||||||
import Firebase
|
import Firebase
|
||||||
import Kingfisher
|
import Kingfisher
|
||||||
import Bootpay
|
import Bootpay
|
||||||
import BootpayUI
|
|
||||||
|
|
||||||
struct HomeView: View {
|
struct HomeView: View {
|
||||||
@StateObject var viewModel = HomeViewModel()
|
@StateObject var viewModel = HomeViewModel()
|
||||||
@@ -296,33 +295,30 @@ struct HomeView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.edgesIgnoringSafeArea(.bottom)
|
.edgesIgnoringSafeArea(.bottom)
|
||||||
.fullScreenCover(isPresented: $isShowAuthView) {
|
.fullScreenCover(isPresented: $isShowAuthView, onDismiss: mypageViewModel.presentAuthContentSettings) {
|
||||||
BootpayUI(payload: payload, requestType: BootpayRequest.TYPE_AUTHENTICATION)
|
AuthVerificationView(
|
||||||
.onConfirm { _ in
|
payload: payload,
|
||||||
true
|
onCancel: {
|
||||||
}
|
|
||||||
.onCancel { _ in
|
|
||||||
isShowAuthView = false
|
isShowAuthView = false
|
||||||
}
|
},
|
||||||
.onError { _ in
|
onError: {
|
||||||
AppState.shared.errorMessage = I18n.Main.Auth.authenticationError
|
AppState.shared.errorMessage = I18n.Main.Auth.authenticationError
|
||||||
AppState.shared.isShowErrorPopup = true
|
AppState.shared.isShowErrorPopup = true
|
||||||
isShowAuthView = false
|
isShowAuthView = false
|
||||||
}
|
},
|
||||||
.onDone {
|
onDone: {
|
||||||
DEBUG_LOG("onDone: \($0)")
|
let action = pendingAction
|
||||||
mypageViewModel.authVerify($0) {
|
mypageViewModel.authVerify($0) {
|
||||||
auth = true
|
auth = true
|
||||||
isShowAuthView = false
|
pendingAction = nil
|
||||||
if let action = pendingAction {
|
action?()
|
||||||
pendingAction = nil
|
|
||||||
action()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
isShowAuthView = false
|
||||||
.onClose {
|
},
|
||||||
|
onClose: {
|
||||||
isShowAuthView = false
|
isShowAuthView = false
|
||||||
}
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
.valueChanged(value: appState.pushRoomId) { value in
|
.valueChanged(value: appState.pushRoomId) { value in
|
||||||
guard value > 0 else {
|
guard value > 0 else {
|
||||||
@@ -463,6 +459,7 @@ struct HomeView: View {
|
|||||||
autohideIn: 2
|
autohideIn: 2
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
.authContentSettings(viewModel: mypageViewModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handleLiveNowItemTap(roomId: Int, isAdult: Bool) {
|
private func handleLiveNowItemTap(roomId: Int, isAdult: Bool) {
|
||||||
|
|||||||
@@ -13,8 +13,18 @@ import Moya
|
|||||||
final class AuthRepository {
|
final class AuthRepository {
|
||||||
private let api = MoyaProvider<AuthApi>()
|
private let api = MoyaProvider<AuthApi>()
|
||||||
|
|
||||||
func authCertificate(receiptId: String) -> AnyPublisher<Response, MoyaError> {
|
func authCertificate(request: AuthVerifyRequest) -> AnyPublisher<Response, MoyaError> {
|
||||||
return api.requestPublisher(.auth(request: AuthVerifyRequest(receiptId: receiptId)))
|
#if AUTH_VERIFICATION_MOCK
|
||||||
|
let requestToken = UserDefaults.string(forKey: .token)
|
||||||
|
guard let data = AuthVerificationMock.shared.confirm(request: request, requestToken: requestToken) else {
|
||||||
|
return Fail(error: MoyaError.statusCode(Response(statusCode: 401, data: Data())))
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
return Just(Response(statusCode: 200, data: data))
|
||||||
|
.setFailureType(to: MoyaError.self)
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
#else
|
||||||
|
return api.requestPublisher(.auth(request: request))
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,12 @@ import Foundation
|
|||||||
|
|
||||||
struct AuthVerifyRequest: Encodable {
|
struct AuthVerifyRequest: Encodable {
|
||||||
let receiptId: String
|
let receiptId: String
|
||||||
|
let isAdultContentVisible: Bool
|
||||||
|
let contentType: ContentType
|
||||||
|
|
||||||
enum CodingKeys : String, CodingKey {
|
enum CodingKeys : String, CodingKey {
|
||||||
case receiptId = "receipt_id"
|
case receiptId = "receipt_id"
|
||||||
|
case isAdultContentVisible
|
||||||
|
case contentType
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
import Bootpay
|
import Bootpay
|
||||||
import BootpayUI
|
|
||||||
import Kingfisher
|
import Kingfisher
|
||||||
import RefreshableScrollView
|
import RefreshableScrollView
|
||||||
|
|
||||||
@@ -33,28 +32,23 @@ struct MyPageView: View {
|
|||||||
BaseView(isLoading: $viewModel.isLoading) {
|
BaseView(isLoading: $viewModel.isLoading) {
|
||||||
if !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty &&
|
if !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty &&
|
||||||
viewModel.isShowAuthView {
|
viewModel.isShowAuthView {
|
||||||
BootpayUI(payload: payload, requestType: BootpayRequest.TYPE_AUTHENTICATION)
|
AuthVerificationView(
|
||||||
.onConfirm {
|
payload: payload,
|
||||||
DEBUG_LOG("onConfirm: \($0)")
|
onCancel: {},
|
||||||
return true
|
onError: {
|
||||||
}
|
|
||||||
.onCancel {
|
|
||||||
DEBUG_LOG("onCancel: \($0)")
|
|
||||||
}
|
|
||||||
.onError {
|
|
||||||
DEBUG_LOG("onError: \($0)")
|
|
||||||
viewModel.errorMessage = I18n.Main.Auth.authenticationError
|
viewModel.errorMessage = I18n.Main.Auth.authenticationError
|
||||||
viewModel.isShowPopup = true
|
viewModel.isShowPopup = true
|
||||||
viewModel.isShowAuthView = false
|
viewModel.isShowAuthView = false
|
||||||
}
|
},
|
||||||
.onDone {
|
onDone: {
|
||||||
DEBUG_LOG("onDone: \($0)")
|
|
||||||
viewModel.authVerify($0)
|
viewModel.authVerify($0)
|
||||||
}
|
viewModel.isShowAuthView = false
|
||||||
.onClose {
|
viewModel.presentAuthContentSettings()
|
||||||
DEBUG_LOG("onClose")
|
},
|
||||||
|
onClose: {
|
||||||
viewModel.isShowAuthView = false
|
viewModel.isShowAuthView = false
|
||||||
}
|
}
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
// Header
|
// Header
|
||||||
@@ -180,7 +174,7 @@ struct MyPageView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
.authContentSettings(viewModel: viewModel)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
payload.applicationId = BOOTPAY_APP_ID
|
payload.applicationId = BOOTPAY_APP_ID
|
||||||
payload.price = 0
|
payload.price = 0
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ final class MyPageViewModel: ObservableObject {
|
|||||||
private let authRepository = AuthRepository()
|
private let authRepository = AuthRepository()
|
||||||
private let noticeRepository = NoticeRepository()
|
private let noticeRepository = NoticeRepository()
|
||||||
private var subscription = Set<AnyCancellable>()
|
private var subscription = Set<AnyCancellable>()
|
||||||
|
private var pendingAuthReceiptId: String?
|
||||||
|
private var pendingAuthRequest: AuthVerifyRequest?
|
||||||
|
private var pendingAuthSuccess: (() -> Void)?
|
||||||
|
|
||||||
@Published var latestNotice: NoticeItem? = nil
|
@Published var latestNotice: NoticeItem? = nil
|
||||||
|
|
||||||
@@ -28,6 +31,16 @@ final class MyPageViewModel: ObservableObject {
|
|||||||
@Published var isShowPopup = false
|
@Published var isShowPopup = false
|
||||||
@Published var isLoading = false
|
@Published var isLoading = false
|
||||||
@Published var isShowAuthView = false
|
@Published var isShowAuthView = false
|
||||||
|
@Published var isShowAuthContentSettings = false
|
||||||
|
@Published var authIsAdultContentVisible = true {
|
||||||
|
didSet {
|
||||||
|
if !authIsAdultContentVisible {
|
||||||
|
authContentType = .ALL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@Published var authContentType: ContentType = .ALL
|
||||||
|
@Published private(set) var isSubmittingAuth = false
|
||||||
|
|
||||||
func getMypage() {
|
func getMypage() {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
@@ -73,61 +86,164 @@ final class MyPageViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func authVerify(_ data: [String: Any], onSuccess: (() -> Void)? = nil) {
|
func authVerify(_ data: [String: Any], onSuccess: (() -> Void)? = nil) {
|
||||||
isLoading = true
|
guard Thread.isMainThread else {
|
||||||
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
self?.authVerify(data, onSuccess: onSuccess)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard pendingAuthReceiptId == nil, !isSubmittingAuth else { return }
|
||||||
|
|
||||||
let _data = data["data"] as? [String: Any]
|
guard let payload = data["data"] as? [String: Any],
|
||||||
|
let receiptId = payload["receipt_id"] as? String,
|
||||||
if let data = _data {
|
!receiptId.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else {
|
||||||
let receiptId = data["receipt_id"] as! String
|
|
||||||
|
|
||||||
authRepository.authCertificate(receiptId: receiptId)
|
|
||||||
.sink { result in
|
|
||||||
switch result {
|
|
||||||
case .finished:
|
|
||||||
DEBUG_LOG("finish")
|
|
||||||
case .failure(let error):
|
|
||||||
ERROR_LOG(error.localizedDescription)
|
|
||||||
}
|
|
||||||
} receiveValue: { [unowned self] response in
|
|
||||||
self.isLoading = false
|
|
||||||
let responseData = response.data
|
|
||||||
|
|
||||||
do {
|
|
||||||
let jsonDecoder = JSONDecoder()
|
|
||||||
let decoded = try jsonDecoder.decode(ApiResponse<AuthResponse>.self, from: responseData)
|
|
||||||
|
|
||||||
if let data = decoded.data, decoded.success {
|
|
||||||
if data.gender == 0 {
|
|
||||||
UserDefaults.set(ContentType.FEMALE.rawValue, forKey: .contentPreference)
|
|
||||||
} else {
|
|
||||||
UserDefaults.set(ContentType.MALE.rawValue, forKey: .contentPreference)
|
|
||||||
}
|
|
||||||
|
|
||||||
if let onSuccess = onSuccess {
|
|
||||||
onSuccess()
|
|
||||||
} else {
|
|
||||||
AppState.shared.isRestartApp = true
|
|
||||||
AppState.shared.setAppStep(step: .splash)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if let message = decoded.message {
|
|
||||||
self.errorMessage = message
|
|
||||||
} else {
|
|
||||||
self.errorMessage = I18n.MyPage.Auth.verificationErrorWithSupport
|
|
||||||
}
|
|
||||||
self.isShowPopup = true
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
self.errorMessage = I18n.MyPage.Auth.verificationErrorWithSupport
|
|
||||||
self.isShowPopup = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.store(in: &subscription)
|
|
||||||
} else {
|
|
||||||
isLoading = false
|
|
||||||
errorMessage = I18n.Main.Auth.authenticationError
|
errorMessage = I18n.Main.Auth.authenticationError
|
||||||
isShowPopup = true
|
isShowPopup = true
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pendingAuthReceiptId = receiptId
|
||||||
|
pendingAuthSuccess = onSuccess
|
||||||
|
authIsAdultContentVisible = true
|
||||||
|
authContentType = .ALL
|
||||||
|
}
|
||||||
|
|
||||||
|
func presentAuthContentSettings() {
|
||||||
|
guard Thread.isMainThread else {
|
||||||
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
self?.presentAuthContentSettings()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard pendingAuthReceiptId != nil, !isSubmittingAuth else { return }
|
||||||
|
isShowAuthContentSettings = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func confirmAuthContentSettings() {
|
||||||
|
guard Thread.isMainThread else {
|
||||||
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
self?.confirmAuthContentSettings()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let receiptId = pendingAuthReceiptId, !isSubmittingAuth else { return }
|
||||||
|
|
||||||
|
pendingAuthRequest = AuthVerifyRequest(
|
||||||
|
receiptId: receiptId,
|
||||||
|
isAdultContentVisible: authIsAdultContentVisible,
|
||||||
|
contentType: authIsAdultContentVisible ? authContentType : .ALL
|
||||||
|
)
|
||||||
|
isSubmittingAuth = true
|
||||||
|
isShowAuthContentSettings = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func submitPendingAuthVerification() {
|
||||||
|
guard Thread.isMainThread else {
|
||||||
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
self?.submitPendingAuthVerification()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard isSubmittingAuth, let request = pendingAuthRequest else { return }
|
||||||
|
pendingAuthRequest = nil
|
||||||
|
|
||||||
|
authRepository.authCertificate(request: request)
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] result in
|
||||||
|
if case .failure = result {
|
||||||
|
self?.handleAuthSubmissionFailure()
|
||||||
|
}
|
||||||
|
} receiveValue: { [weak self] response in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
guard (200..<300).contains(response.statusCode) else {
|
||||||
|
self.handleAuthSubmissionFailure()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
let decoded = try JSONDecoder().decode(ApiResponse<AuthResponse>.self, from: response.data)
|
||||||
|
guard decoded.success, decoded.data != nil else {
|
||||||
|
self.handleAuthSubmissionFailure()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.pendingAuthSuccess != nil {
|
||||||
|
self.synchronizeAuthenticatedMember()
|
||||||
|
} else {
|
||||||
|
UserDefaults.set(request.isAdultContentVisible, forKey: .isAdultContentVisible)
|
||||||
|
UserDefaults.set(request.contentType.rawValue, forKey: .contentPreference)
|
||||||
|
self.restartAfterAuthVerification()
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
self.handleAuthSubmissionFailure()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &subscription)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleAuthSubmissionFailure() {
|
||||||
|
pendingAuthRequest = nil
|
||||||
|
isSubmittingAuth = false
|
||||||
|
errorMessage = I18n.MyPage.Auth.verificationErrorWithSupport
|
||||||
|
isShowPopup = true
|
||||||
|
isShowAuthContentSettings = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private func synchronizeAuthenticatedMember() {
|
||||||
|
repository.getMemberInfo()
|
||||||
|
.receive(on: DispatchQueue.main)
|
||||||
|
.sink { [weak self] result in
|
||||||
|
if case .failure = result {
|
||||||
|
self?.restartAfterAuthVerification()
|
||||||
|
}
|
||||||
|
} receiveValue: { [weak self] response in
|
||||||
|
guard let self = self else { return }
|
||||||
|
|
||||||
|
guard (200..<300).contains(response.statusCode) else {
|
||||||
|
self.restartAfterAuthVerification()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
let decoded = try JSONDecoder().decode(ApiResponse<GetMemberInfoResponse>.self, from: response.data)
|
||||||
|
guard let data = decoded.data, decoded.success, data.isAuth else {
|
||||||
|
self.restartAfterAuthVerification()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
UserDefaults.set(data.can, forKey: .can)
|
||||||
|
UserDefaults.set(data.point, forKey: .point)
|
||||||
|
UserDefaults.set(data.isAuth, forKey: .auth)
|
||||||
|
UserDefaults.set(data.role.rawValue, forKey: .role)
|
||||||
|
UserDefaults.set(data.auditionNotice ?? false, forKey: .isAuditionNotification)
|
||||||
|
UserDefaults.set(data.countryCode, forKey: .countryCode)
|
||||||
|
UserDefaults.set(data.isAdultContentVisible, forKey: .isAdultContentVisible)
|
||||||
|
UserDefaults.set(data.isAdultContentVisible ? data.contentType : ContentType.ALL.rawValue, forKey: .contentPreference)
|
||||||
|
self.isAuth = data.isAuth
|
||||||
|
|
||||||
|
let onSuccess = self.pendingAuthSuccess
|
||||||
|
self.clearPendingAuthVerification()
|
||||||
|
onSuccess?()
|
||||||
|
} catch {
|
||||||
|
self.restartAfterAuthVerification()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.store(in: &subscription)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func clearPendingAuthVerification() {
|
||||||
|
pendingAuthReceiptId = nil
|
||||||
|
pendingAuthRequest = nil
|
||||||
|
pendingAuthSuccess = nil
|
||||||
|
isSubmittingAuth = false
|
||||||
|
isShowAuthContentSettings = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private func restartAfterAuthVerification() {
|
||||||
|
clearPendingAuthVerification()
|
||||||
|
AppState.shared.isRestartApp = true
|
||||||
|
AppState.shared.setAppStep(step: .splash)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getLatestNotice() {
|
func getLatestNotice() {
|
||||||
|
|||||||
@@ -46,19 +46,45 @@ final class UserRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getMypage() -> AnyPublisher<Response, MoyaError> {
|
func getMypage() -> AnyPublisher<Response, MoyaError> {
|
||||||
|
#if AUTH_VERIFICATION_MOCK
|
||||||
|
return selfMemberPublisher(.getMypage, as: MyPageResponse.self)
|
||||||
|
#else
|
||||||
return api.requestPublisher(.getMypage)
|
return api.requestPublisher(.getMypage)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
func getMemberInfo() -> AnyPublisher<Response, MoyaError> {
|
func getMemberInfo() -> AnyPublisher<Response, MoyaError> {
|
||||||
|
#if AUTH_VERIFICATION_MOCK
|
||||||
|
return selfMemberPublisher(.getMemberInfo, as: GetMemberInfoResponse.self)
|
||||||
|
#else
|
||||||
return api.requestPublisher(.getMemberInfo)
|
return api.requestPublisher(.getMemberInfo)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateContentPreference(request: UpdateContentPreferenceRequest) -> AnyPublisher<Response, MoyaError> {
|
func updateContentPreference(request: UpdateContentPreferenceRequest) -> AnyPublisher<Response, MoyaError> {
|
||||||
|
#if AUTH_VERIFICATION_MOCK
|
||||||
|
let requestToken = UserDefaults.string(forKey: .token)
|
||||||
|
let session = AuthVerificationMock.shared.session(for: requestToken)
|
||||||
return api.requestPublisher(.updateContentPreference(request: request))
|
return api.requestPublisher(.updateContentPreference(request: request))
|
||||||
|
.map { response in
|
||||||
|
AuthVerificationMock.shared.updateContentPreference(from: response.data,
|
||||||
|
statusCode: response.statusCode,
|
||||||
|
requestToken: requestToken,
|
||||||
|
session: session)
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
#else
|
||||||
|
return api.requestPublisher(.updateContentPreference(request: request))
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
func getMemberCan() -> AnyPublisher<Response, MoyaError> {
|
func getMemberCan() -> AnyPublisher<Response, MoyaError> {
|
||||||
|
#if AUTH_VERIFICATION_MOCK
|
||||||
|
return getMemberInfo()
|
||||||
|
#else
|
||||||
return api.requestPublisher(.getMemberInfo)
|
return api.requestPublisher(.getMemberInfo)
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateNotificationSettings(live: Bool? = nil, uploadContent: Bool? = nil, message: Bool? = nil, audition: Bool? = nil) -> AnyPublisher<Response, MoyaError> {
|
func updateNotificationSettings(live: Bool? = nil, uploadContent: Bool? = nil, message: Bool? = nil, audition: Bool? = nil) -> AnyPublisher<Response, MoyaError> {
|
||||||
@@ -149,4 +175,23 @@ final class UserRepository {
|
|||||||
func getMemberProfile(memberId: Int) -> AnyPublisher<Response, MoyaError> {
|
func getMemberProfile(memberId: Int) -> AnyPublisher<Response, MoyaError> {
|
||||||
return api.requestPublisher(.getMemberProfile(memberId: memberId))
|
return api.requestPublisher(.getMemberProfile(memberId: memberId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if AUTH_VERIFICATION_MOCK
|
||||||
|
private func selfMemberPublisher<Member: Decodable>(_ target: UserApi, as type: Member.Type) -> AnyPublisher<Response, MoyaError> {
|
||||||
|
let requestToken = UserDefaults.string(forKey: .token)
|
||||||
|
let session = AuthVerificationMock.shared.session(for: requestToken)
|
||||||
|
return api.requestPublisher(target)
|
||||||
|
.map { response in
|
||||||
|
let data = AuthVerificationMock.shared.overlayMemberResponse(response.data,
|
||||||
|
statusCode: response.statusCode,
|
||||||
|
requestToken: requestToken,
|
||||||
|
session: session,
|
||||||
|
as: type)
|
||||||
|
guard data != response.data else { return response }
|
||||||
|
return Response(statusCode: response.statusCode, data: data,
|
||||||
|
request: response.request, response: response.response)
|
||||||
|
}
|
||||||
|
.eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
#if AUTH_VERIFICATION_MOCK
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
final class AuthVerificationMock {
|
||||||
|
static let shared = AuthVerificationMock()
|
||||||
|
|
||||||
|
final class Session {
|
||||||
|
fileprivate let token: String
|
||||||
|
fileprivate var preference: (isAdultContentVisible: Bool, contentType: ContentType)?
|
||||||
|
|
||||||
|
fileprivate init(token: String) {
|
||||||
|
self.token = token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private let lock = NSLock()
|
||||||
|
private var loginSession: Session?
|
||||||
|
|
||||||
|
private init() {}
|
||||||
|
|
||||||
|
func confirm(request: AuthVerifyRequest, requestToken: String) -> Data? {
|
||||||
|
lock.lock()
|
||||||
|
defer { lock.unlock() }
|
||||||
|
guard let session = currentSession(for: requestToken) else { return nil }
|
||||||
|
|
||||||
|
session.preference = (request.isAdultContentVisible, request.isAdultContentVisible ? request.contentType : .ALL)
|
||||||
|
return Data(#"{"success":true,"data":{"gender":0}}"#.utf8)
|
||||||
|
}
|
||||||
|
|
||||||
|
func session(for requestToken: String) -> Session? {
|
||||||
|
lock.lock()
|
||||||
|
defer { lock.unlock() }
|
||||||
|
return currentSession(for: requestToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resetSession(_ changes: () -> Void) {
|
||||||
|
lock.lock()
|
||||||
|
defer { lock.unlock() }
|
||||||
|
loginSession = nil
|
||||||
|
changes()
|
||||||
|
}
|
||||||
|
|
||||||
|
func overlayMemberResponse<Member: Decodable>(_ data: Data, statusCode: Int, requestToken: String,
|
||||||
|
session: Session?, as type: Member.Type) -> Data {
|
||||||
|
lock.lock()
|
||||||
|
defer { lock.unlock() }
|
||||||
|
guard isActive(session, requestToken: requestToken), let preference = session?.preference,
|
||||||
|
(200..<300).contains(statusCode),
|
||||||
|
let decoded = try? JSONDecoder().decode(ApiResponse<Member>.self, from: data),
|
||||||
|
decoded.success, decoded.data != nil,
|
||||||
|
var envelope = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any],
|
||||||
|
var member = envelope["data"] as? [String: Any] else { return data }
|
||||||
|
|
||||||
|
member["isAuth"] = true
|
||||||
|
member["isAdultContentVisible"] = preference.isAdultContentVisible
|
||||||
|
member["contentType"] = preference.contentType.rawValue
|
||||||
|
envelope["data"] = member
|
||||||
|
return (try? JSONSerialization.data(withJSONObject: envelope)) ?? data
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateContentPreference(from data: Data, statusCode: Int, requestToken: String, session: Session?) {
|
||||||
|
lock.lock()
|
||||||
|
defer { lock.unlock() }
|
||||||
|
guard isActive(session, requestToken: requestToken), let session = session, session.preference != nil,
|
||||||
|
(200..<300).contains(statusCode),
|
||||||
|
let decoded = try? JSONDecoder().decode(ApiResponse<UpdateContentPreferenceResponse>.self, from: data),
|
||||||
|
decoded.success, let preference = decoded.data else { return }
|
||||||
|
|
||||||
|
session.preference = (preference.isAdultContentVisible, preference.isAdultContentVisible ? preference.contentType : .ALL)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func currentSession(for requestToken: String) -> Session? {
|
||||||
|
guard !requestToken.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
|
||||||
|
requestToken == UserDefaults.string(forKey: .token) else { return nil }
|
||||||
|
if loginSession?.token != requestToken {
|
||||||
|
loginSession = Session(token: requestToken)
|
||||||
|
}
|
||||||
|
return loginSession
|
||||||
|
}
|
||||||
|
|
||||||
|
private func isActive(_ session: Session?, requestToken: String) -> Bool {
|
||||||
|
guard let session = session, session === loginSession else { return false }
|
||||||
|
return session.token == requestToken && requestToken == UserDefaults.string(forKey: .token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,349 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
private enum AuthContentSettingsLayout {
|
||||||
|
static let topInset: CGFloat = 26
|
||||||
|
static let headerHeight: CGFloat = 54
|
||||||
|
static let touchHeight: CGFloat = 44
|
||||||
|
static let buttonHeight: CGFloat = 52
|
||||||
|
static let titleLineHeight: CGFloat = 23
|
||||||
|
static let captionLineHeight: CGFloat = 17
|
||||||
|
static let radioSize: CGFloat = 22
|
||||||
|
static let radioDotSize: CGFloat = 12
|
||||||
|
static let radioRowGap: CGFloat = 1
|
||||||
|
static let radioBottomInset: CGFloat = 10
|
||||||
|
static let switchWidth: CGFloat = 44
|
||||||
|
static let switchHeight: CGFloat = 24
|
||||||
|
static let switchInset: CGFloat = 2
|
||||||
|
static let strokeWidth: CGFloat = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AuthContentSettingsSheet: View {
|
||||||
|
@Binding var isAdultContentVisible: Bool
|
||||||
|
@Binding var contentType: ContentType
|
||||||
|
let onConfirm: () -> Void
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
ViewThatFits(in: .vertical) {
|
||||||
|
settingsContent
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
|
||||||
|
ScrollView(.vertical, showsIndicators: false) {
|
||||||
|
settingsContent
|
||||||
|
}
|
||||||
|
.scrollBounceBehavior(.basedOnSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(action: onConfirm) {
|
||||||
|
Text(I18n.Settings.Content.confirmAuthSettings)
|
||||||
|
.appFont(size: SodaTypography.heading4.size, weight: .medium)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.frame(maxWidth: .infinity, minHeight: AuthContentSettingsLayout.buttonHeight)
|
||||||
|
.background(Color.soda400)
|
||||||
|
.clipShape(Capsule())
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.accessibilityIdentifier("authContentSettings.confirm")
|
||||||
|
.padding(.horizontal, SodaSpacing.s14)
|
||||||
|
.padding(.vertical, SodaSpacing.s8)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
}
|
||||||
|
.background(Color.gray900)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var settingsContent: some View {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
Text(I18n.Settings.contentViewSettings)
|
||||||
|
.appFont(size: SodaTypography.heading4.size, weight: .medium)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.frame(maxWidth: .infinity, minHeight: AuthContentSettingsLayout.headerHeight)
|
||||||
|
.accessibilityAddTraits(.isHeader)
|
||||||
|
|
||||||
|
sensitiveContentRow
|
||||||
|
.padding(.top, SodaSpacing.s8)
|
||||||
|
.padding(.bottom, isAdultContentVisible ? SodaSpacing.s14 : SodaSpacing.s8)
|
||||||
|
|
||||||
|
if isAdultContentVisible {
|
||||||
|
preferenceSection
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack(alignment: .top, spacing: SodaSpacing.s6) {
|
||||||
|
Image(systemName: "info.circle")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: SodaSpacing.s16, height: SodaSpacing.s16)
|
||||||
|
.accessibilityHidden(true)
|
||||||
|
|
||||||
|
Text(I18n.Settings.Content.authSettingsGuide)
|
||||||
|
.appFont(.caption3)
|
||||||
|
.frame(maxWidth: .infinity, minHeight: AuthContentSettingsLayout.captionLineHeight, alignment: .leading)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
}
|
||||||
|
.foregroundColor(.gray400)
|
||||||
|
.padding(.top, isAdultContentVisible ? 0 : SodaSpacing.s8)
|
||||||
|
.padding(.bottom, SodaSpacing.s20)
|
||||||
|
}
|
||||||
|
.padding(.top, AuthContentSettingsLayout.topInset)
|
||||||
|
.padding(.horizontal, SodaSpacing.s14)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var sensitiveContentRow: some View {
|
||||||
|
HStack(spacing: SodaSpacing.s12) {
|
||||||
|
VStack(alignment: .leading, spacing: SodaSpacing.s4) {
|
||||||
|
Text(I18n.Settings.Content.sensitiveContentTitle)
|
||||||
|
.appFont(.body2)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.frame(minHeight: AuthContentSettingsLayout.titleLineHeight, alignment: .leading)
|
||||||
|
|
||||||
|
Text(isAdultContentVisible ? I18n.Settings.Content.authSensitiveContentOnDescription : I18n.Settings.Content.authSensitiveContentOffDescription)
|
||||||
|
.appFont(.caption3)
|
||||||
|
.foregroundColor(.gray400)
|
||||||
|
.frame(minHeight: AuthContentSettingsLayout.captionLineHeight, alignment: .leading)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
|
||||||
|
Toggle(I18n.Settings.Content.sensitiveContentTitle, isOn: $isAdultContentVisible)
|
||||||
|
.toggleStyle(AuthContentSettingsToggleStyle())
|
||||||
|
.accessibilityIdentifier("authContentSettings.toggle")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var preferenceSection: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
|
Rectangle()
|
||||||
|
.fill(Color.gray800)
|
||||||
|
.frame(height: AuthContentSettingsLayout.strokeWidth)
|
||||||
|
|
||||||
|
Text(I18n.Settings.Content.preferredContent)
|
||||||
|
.appFont(.caption2)
|
||||||
|
.foregroundColor(.gray400)
|
||||||
|
.frame(minHeight: AuthContentSettingsLayout.captionLineHeight)
|
||||||
|
.padding(.top, SodaSpacing.s14)
|
||||||
|
|
||||||
|
HStack(spacing: SodaSpacing.s12) {
|
||||||
|
preferenceButton(.ALL, title: I18n.Settings.Content.all)
|
||||||
|
preferenceButton(.MALE, title: I18n.Settings.Content.maleOriented)
|
||||||
|
preferenceButton(.FEMALE, title: I18n.Settings.Content.femaleOriented)
|
||||||
|
}
|
||||||
|
.padding(.top, AuthContentSettingsLayout.radioRowGap)
|
||||||
|
.padding(.bottom, AuthContentSettingsLayout.radioBottomInset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func preferenceButton(_ type: ContentType, title: String) -> some View {
|
||||||
|
Button {
|
||||||
|
contentType = type
|
||||||
|
} label: {
|
||||||
|
HStack(spacing: SodaSpacing.s6) {
|
||||||
|
Circle()
|
||||||
|
.stroke(Color.gray700, lineWidth: AuthContentSettingsLayout.strokeWidth)
|
||||||
|
.frame(width: AuthContentSettingsLayout.radioSize, height: AuthContentSettingsLayout.radioSize)
|
||||||
|
.overlay {
|
||||||
|
if contentType == type {
|
||||||
|
Circle()
|
||||||
|
.fill(Color.soda400)
|
||||||
|
.frame(width: AuthContentSettingsLayout.radioDotSize, height: AuthContentSettingsLayout.radioDotSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(title)
|
||||||
|
.appFont(.body2)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, minHeight: AuthContentSettingsLayout.touchHeight, alignment: .leading)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.accessibilityLabel(title)
|
||||||
|
.accessibilityValue(contentType == type ? I18n.Settings.Content.selectedPreference : I18n.Settings.Content.unselectedPreference)
|
||||||
|
.accessibilityAddTraits(contentType == type ? .isSelected : [])
|
||||||
|
.accessibilityIdentifier("authContentSettings.preference.\(type.rawValue)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct AuthContentSettingsToggleStyle: ToggleStyle {
|
||||||
|
func makeBody(configuration: Configuration) -> some View {
|
||||||
|
Button {
|
||||||
|
configuration.isOn.toggle()
|
||||||
|
} label: {
|
||||||
|
Capsule()
|
||||||
|
.fill(configuration.isOn ? Color.soda400 : Color.gray500)
|
||||||
|
.frame(width: AuthContentSettingsLayout.switchWidth, height: AuthContentSettingsLayout.switchHeight)
|
||||||
|
.overlay(alignment: configuration.isOn ? .trailing : .leading) {
|
||||||
|
Circle()
|
||||||
|
.fill(Color.white)
|
||||||
|
.frame(width: SodaSpacing.s20, height: SodaSpacing.s20)
|
||||||
|
.padding(AuthContentSettingsLayout.switchInset)
|
||||||
|
}
|
||||||
|
.frame(minHeight: AuthContentSettingsLayout.touchHeight)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
.accessibilityRepresentation {
|
||||||
|
Toggle(isOn: configuration.$isOn) {
|
||||||
|
configuration.label
|
||||||
|
}
|
||||||
|
.toggleStyle(.switch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct AuthContentSettingsModifier: ViewModifier {
|
||||||
|
@ObservedObject var viewModel: MyPageViewModel
|
||||||
|
@Environment(\.accessibilityReduceMotion) private var reduceMotion
|
||||||
|
@State private var isHostPresented = false
|
||||||
|
@State private var panelProgress: CGFloat = 0
|
||||||
|
|
||||||
|
private var callerErrorIsPresented: Binding<Bool> {
|
||||||
|
Binding(
|
||||||
|
get: { viewModel.isShowPopup && !viewModel.isShowAuthContentSettings },
|
||||||
|
set: { viewModel.isShowPopup = $0 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func body(content: Content) -> some View {
|
||||||
|
content
|
||||||
|
.disabled(viewModel.isSubmittingAuth)
|
||||||
|
.overlay {
|
||||||
|
if viewModel.isSubmittingAuth {
|
||||||
|
LoadingView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sodaToast(isPresented: callerErrorIsPresented, message: viewModel.errorMessage, autohideIn: 2)
|
||||||
|
.fullScreenCover(isPresented: $isHostPresented, onDismiss: {
|
||||||
|
panelProgress = 0
|
||||||
|
viewModel.submitPendingAuthVerification()
|
||||||
|
}) {
|
||||||
|
GeometryReader { proxy in
|
||||||
|
ZStack(alignment: .bottom) {
|
||||||
|
Color.black.opacity(0.6)
|
||||||
|
.ignoresSafeArea()
|
||||||
|
|
||||||
|
AuthContentSettingsSheet(
|
||||||
|
isAdultContentVisible: $viewModel.authIsAdultContentVisible,
|
||||||
|
contentType: $viewModel.authContentType,
|
||||||
|
onConfirm: viewModel.confirmAuthContentSettings
|
||||||
|
)
|
||||||
|
.padding(.leading, proxy.safeAreaInsets.leading)
|
||||||
|
.padding(.trailing, proxy.safeAreaInsets.trailing)
|
||||||
|
.padding(.bottom, proxy.safeAreaInsets.bottom)
|
||||||
|
.background(Color.gray900)
|
||||||
|
.clipShape(RoundedCorner(radius: SodaRadius.r14, corners: [.topLeft, .topRight]))
|
||||||
|
.accessibilityElement(children: .contain)
|
||||||
|
.accessibilityIdentifier("authContentSettings.panel")
|
||||||
|
.disabled(viewModel.isSubmittingAuth)
|
||||||
|
.overlay {
|
||||||
|
if viewModel.isSubmittingAuth {
|
||||||
|
LoadingView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||||
|
.modifier(AuthContentSettingsPanelMotion(
|
||||||
|
progress: panelProgress,
|
||||||
|
reduceMotion: reduceMotion,
|
||||||
|
onHidden: closeHostIfHidden
|
||||||
|
))
|
||||||
|
}
|
||||||
|
.ignoresSafeArea(.container, edges: [.horizontal, .bottom])
|
||||||
|
}
|
||||||
|
.interactiveDismissDisabled(true)
|
||||||
|
.presentationBackground(.clear)
|
||||||
|
.onAppear {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
updatePanel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
if viewModel.isShowAuthContentSettings {
|
||||||
|
setHostPresented(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onChange(of: viewModel.isShowAuthContentSettings) { isPresented in
|
||||||
|
if isPresented && !isHostPresented {
|
||||||
|
setHostPresented(true)
|
||||||
|
} else if isHostPresented {
|
||||||
|
updatePanel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onChange(of: reduceMotion) { _ in
|
||||||
|
if isHostPresented {
|
||||||
|
updatePanel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setHostPresented(_ isPresented: Bool) {
|
||||||
|
var transaction = Transaction()
|
||||||
|
transaction.disablesAnimations = true
|
||||||
|
withTransaction(transaction) {
|
||||||
|
isHostPresented = isPresented
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updatePanel() {
|
||||||
|
let progress: CGFloat = viewModel.isShowAuthContentSettings ? 1 : 0
|
||||||
|
if reduceMotion || panelProgress == progress {
|
||||||
|
panelProgress = progress
|
||||||
|
closeHostIfHidden()
|
||||||
|
} else {
|
||||||
|
withAnimation(.easeInOut(duration: 0.25)) {
|
||||||
|
panelProgress = progress
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func closeHostIfHidden() {
|
||||||
|
guard isHostPresented, panelProgress == 0, !viewModel.isShowAuthContentSettings else { return }
|
||||||
|
setHostPresented(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct AuthContentSettingsPanelMotion: GeometryEffect {
|
||||||
|
var progress: CGFloat
|
||||||
|
let reduceMotion: Bool
|
||||||
|
let onHidden: () -> Void
|
||||||
|
|
||||||
|
var animatableData: CGFloat {
|
||||||
|
get { progress }
|
||||||
|
set {
|
||||||
|
progress = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func effectValue(size: CGSize) -> ProjectionTransform {
|
||||||
|
if progress == 0 {
|
||||||
|
DispatchQueue.main.async(execute: onHidden)
|
||||||
|
}
|
||||||
|
return ProjectionTransform(CGAffineTransform(translationX: 0, y: reduceMotion ? 0 : size.height * (1 - progress)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension View {
|
||||||
|
func authContentSettings(viewModel: MyPageViewModel) -> some View {
|
||||||
|
modifier(AuthContentSettingsModifier(viewModel: viewModel))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AuthContentSettingsSheet_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
Group {
|
||||||
|
AuthContentSettingsSheet(isAdultContentVisible: .constant(true), contentType: .constant(.ALL), onConfirm: {})
|
||||||
|
.frame(width: 402)
|
||||||
|
.previewDisplayName("ON / ALL")
|
||||||
|
|
||||||
|
AuthContentSettingsSheet(isAdultContentVisible: .constant(false), contentType: .constant(.ALL), onConfirm: {})
|
||||||
|
.frame(width: 402)
|
||||||
|
.previewDisplayName("OFF / ALL")
|
||||||
|
}
|
||||||
|
.padding(.bottom, 34)
|
||||||
|
.background(Color.gray900)
|
||||||
|
.clipShape(RoundedCorner(radius: SodaRadius.r14, corners: [.topLeft, .topRight]))
|
||||||
|
.environment(\.locale, Locale(identifier: "ko"))
|
||||||
|
.preferredColorScheme(.dark)
|
||||||
|
.previewLayout(.sizeThatFits)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
import Bootpay
|
||||||
|
#if !AUTH_VERIFICATION_MOCK
|
||||||
|
import BootpayUI
|
||||||
|
#endif
|
||||||
|
|
||||||
|
struct AuthVerificationView: View {
|
||||||
|
let payload: Payload
|
||||||
|
let onCancel: () -> Void
|
||||||
|
let onError: () -> Void
|
||||||
|
let onDone: ([String: Any]) -> Void
|
||||||
|
let onClose: () -> Void
|
||||||
|
|
||||||
|
#if AUTH_VERIFICATION_MOCK
|
||||||
|
@State private var hasCompleted = false
|
||||||
|
#endif
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
#if AUTH_VERIFICATION_MOCK
|
||||||
|
LoadingView()
|
||||||
|
.background(Color.white.ignoresSafeArea())
|
||||||
|
.task {
|
||||||
|
guard !hasCompleted else { return }
|
||||||
|
do {
|
||||||
|
try await Task.sleep(nanoseconds: 500_000_000)
|
||||||
|
} catch is CancellationError {
|
||||||
|
return
|
||||||
|
} catch {
|
||||||
|
onError()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard !Task.isCancelled, !hasCompleted else { return }
|
||||||
|
hasCompleted = true
|
||||||
|
onDone(["data": ["receipt_id": "dev-mock-auth-receipt"]])
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
BootpayUI(payload: payload, requestType: BootpayRequest.TYPE_AUTHENTICATION)
|
||||||
|
.onConfirm { _ in true }
|
||||||
|
.onCancel { _ in onCancel() }
|
||||||
|
.onError { _ in onError() }
|
||||||
|
.onDone(onDone)
|
||||||
|
.onClose(onClose)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import Bootpay
|
import Bootpay
|
||||||
import BootpayUI
|
|
||||||
|
|
||||||
struct CreatorChannelView: View {
|
struct CreatorChannelView: View {
|
||||||
let creatorId: Int
|
let creatorId: Int
|
||||||
@@ -256,9 +255,10 @@ struct CreatorChannelView: View {
|
|||||||
ActivityViewController(activityItems: [viewModel.shareMessage])
|
ActivityViewController(activityItems: [viewModel.shareMessage])
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.fullScreenCover(isPresented: $isShowAuthView) {
|
.fullScreenCover(isPresented: $isShowAuthView, onDismiss: mypageViewModel.presentAuthContentSettings) {
|
||||||
authView
|
authView
|
||||||
}
|
}
|
||||||
|
.authContentSettings(viewModel: mypageViewModel)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
configurePayload()
|
configurePayload()
|
||||||
if viewModel.hasLoaded == false {
|
if viewModel.hasLoaded == false {
|
||||||
@@ -490,26 +490,25 @@ struct CreatorChannelView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var authView: some View {
|
private var authView: some View {
|
||||||
BootpayUI(payload: payload, requestType: BootpayRequest.TYPE_AUTHENTICATION)
|
AuthVerificationView(
|
||||||
.onConfirm { _ in true }
|
payload: payload,
|
||||||
.onCancel { _ in isShowAuthView = false }
|
onCancel: { isShowAuthView = false },
|
||||||
.onError { _ in
|
onError: {
|
||||||
AppState.shared.errorMessage = I18n.Chat.Auth.authenticationError
|
AppState.shared.errorMessage = I18n.Chat.Auth.authenticationError
|
||||||
AppState.shared.isShowErrorPopup = true
|
AppState.shared.isShowErrorPopup = true
|
||||||
isShowAuthView = false
|
isShowAuthView = false
|
||||||
}
|
},
|
||||||
.onDone {
|
onDone: {
|
||||||
DEBUG_LOG("onDone: \($0)")
|
let action = pendingAction
|
||||||
mypageViewModel.authVerify($0) {
|
mypageViewModel.authVerify($0) {
|
||||||
auth = true
|
auth = true
|
||||||
isShowAuthView = false
|
pendingAction = nil
|
||||||
if let action = pendingAction {
|
action?()
|
||||||
pendingAction = nil
|
|
||||||
action()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
isShowAuthView = false
|
||||||
.onClose { isShowAuthView = false }
|
},
|
||||||
|
onClose: { isShowAuthView = false }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func configurePayload() {
|
private func configurePayload() {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
import Bootpay
|
import Bootpay
|
||||||
import BootpayUI
|
|
||||||
import Kingfisher
|
import Kingfisher
|
||||||
|
|
||||||
struct MainView: View {
|
struct MainView: View {
|
||||||
@@ -148,7 +147,7 @@ struct MainView: View {
|
|||||||
ContentPlayerView(isShowing: $isShowPlayer, playlist: [])
|
ContentPlayerView(isShowing: $isShowPlayer, playlist: [])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.fullScreenCover(isPresented: $isShowAuthView) {
|
.fullScreenCover(isPresented: $isShowAuthView, onDismiss: mypageViewModel.presentAuthContentSettings) {
|
||||||
authView
|
authView
|
||||||
}
|
}
|
||||||
.valueChanged(value: appState.pushRoomId) { handlePushRoomId($0) }
|
.valueChanged(value: appState.pushRoomId) { handlePushRoomId($0) }
|
||||||
@@ -175,6 +174,7 @@ struct MainView: View {
|
|||||||
autohideIn: 2
|
autohideIn: 2
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
.authContentSettings(viewModel: mypageViewModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var shouldShowCreatorActionMenu: Bool {
|
private var shouldShowCreatorActionMenu: Bool {
|
||||||
@@ -383,26 +383,25 @@ struct MainView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var authView: some View {
|
private var authView: some View {
|
||||||
BootpayUI(payload: payload, requestType: BootpayRequest.TYPE_AUTHENTICATION)
|
AuthVerificationView(
|
||||||
.onConfirm { _ in true }
|
payload: payload,
|
||||||
.onCancel { _ in isShowAuthView = false }
|
onCancel: { isShowAuthView = false },
|
||||||
.onError { _ in
|
onError: {
|
||||||
appState.errorMessage = I18n.Main.Auth.authenticationError
|
appState.errorMessage = I18n.Main.Auth.authenticationError
|
||||||
appState.isShowErrorPopup = true
|
appState.isShowErrorPopup = true
|
||||||
isShowAuthView = false
|
isShowAuthView = false
|
||||||
}
|
},
|
||||||
.onDone {
|
onDone: {
|
||||||
DEBUG_LOG("onDone: \($0)")
|
let action = pendingAction
|
||||||
mypageViewModel.authVerify($0) {
|
mypageViewModel.authVerify($0) {
|
||||||
auth = true
|
auth = true
|
||||||
isShowAuthView = false
|
pendingAction = nil
|
||||||
if let action = pendingAction {
|
action?()
|
||||||
pendingAction = nil
|
|
||||||
action()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
isShowAuthView = false
|
||||||
.onClose { isShowAuthView = false }
|
},
|
||||||
|
onClose: { isShowAuthView = false }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func configurePayload() {
|
private func configurePayload() {
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import Foundation
|
||||||
|
import CoreFoundation
|
||||||
|
|
||||||
|
@main
|
||||||
|
struct AuthContentSettingsCheck {
|
||||||
|
static func main() throws {
|
||||||
|
let receiptId = "synthetic-auth-receipt"
|
||||||
|
let cases: [(Bool, ContentType)] = [
|
||||||
|
(true, .ALL),
|
||||||
|
(true, .MALE),
|
||||||
|
(true, .FEMALE),
|
||||||
|
(false, .ALL)
|
||||||
|
]
|
||||||
|
|
||||||
|
for (isAdultContentVisible, contentType) in cases {
|
||||||
|
let request = AuthVerifyRequest(
|
||||||
|
receiptId: receiptId,
|
||||||
|
isAdultContentVisible: isAdultContentVisible,
|
||||||
|
contentType: contentType
|
||||||
|
)
|
||||||
|
let encoded = try JSONEncoder().encode(request)
|
||||||
|
guard let json = try JSONSerialization.jsonObject(with: encoded) as? [String: Any],
|
||||||
|
let encodedReceipt = json["receipt_id"] as? String,
|
||||||
|
let encodedVisibility = json["isAdultContentVisible"] as? NSNumber,
|
||||||
|
let encodedContentType = json["contentType"] as? String else {
|
||||||
|
assertionFailure("Expected receipt string, visibility bool and content type string")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(Set(json.keys) == Set(["receipt_id", "isAdultContentVisible", "contentType"]))
|
||||||
|
assert(encodedReceipt == receiptId)
|
||||||
|
assert(CFGetTypeID(encodedVisibility) == CFBooleanGetTypeID())
|
||||||
|
assert(encodedVisibility.boolValue == isAdultContentVisible)
|
||||||
|
assert(encodedContentType == contentType.rawValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
print("PASS: AuthVerifyRequest exact JSON keys/types and ON ALL/MALE/FEMALE, OFF ALL (4 cases)")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
import Foundation
|
||||||
|
import CoreFoundation
|
||||||
|
|
||||||
|
@main
|
||||||
|
struct AuthVerificationMockCheck {
|
||||||
|
static let mock = AuthVerificationMock.shared
|
||||||
|
static let token = "synthetic-session-a"
|
||||||
|
static let otherToken = "synthetic-session-b"
|
||||||
|
static let member = Data(#"{"success":true,"message":null,"extra":{"trace":17},"data":{"can":137,"point":29,"isAuth":false,"gender":"FEMALE","signupDate":"synthetic-date","chargeCount":3,"role":"CREATOR","messageNotice":true,"followingChannelLiveNotice":false,"followingChannelUploadContentNotice":true,"auditionNotice":false,"countryCode":" kr ","isAdultContentVisible":false,"contentType":"MALE","extra":{"nested":[1,true,null,"keep"]}}}"#.utf8)
|
||||||
|
static let mypage = Data(#"{"success":true,"data":{"nickname":"synthetic-name","profileUrl":"synthetic-profile","chargeCan":71,"rewardCan":42,"point":19,"youtubeUrl":null,"instagramUrl":"keep","liveReservationCount":4,"isAuth":false,"extra":{"untouched":true}}}"#.utf8)
|
||||||
|
|
||||||
|
static func request(visible: Bool = true, type: ContentType = .FEMALE) -> AuthVerifyRequest {
|
||||||
|
AuthVerifyRequest(receiptId: "synthetic-check-receipt", isAdultContentVisible: visible, contentType: type)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func object(_ data: Data) -> NSDictionary {
|
||||||
|
try! JSONSerialization.jsonObject(with: data) as! NSDictionary
|
||||||
|
}
|
||||||
|
|
||||||
|
static func overlay(_ data: Data = member, status: Int = 200, requestToken: String = token) -> Data {
|
||||||
|
mock.overlayMemberResponse(data, statusCode: status, requestToken: requestToken,
|
||||||
|
session: mock.session(for: requestToken), as: GetMemberInfoResponse.self)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func begin() {
|
||||||
|
UserDefaults.reset()
|
||||||
|
UserDefaults.set(token, forKey: .token)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func confirm(visible: Bool = true, type: ContentType = .FEMALE) {
|
||||||
|
assert(mock.confirm(request: request(visible: visible, type: type), requestToken: token) != nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func checkLatePreConfirmationResponse() {
|
||||||
|
begin()
|
||||||
|
let beforeConfirmation = mock.session(for: token)
|
||||||
|
assert(mock.overlayMemberResponse(member, statusCode: 200, requestToken: token,
|
||||||
|
session: beforeConfirmation, as: GetMemberInfoResponse.self) == member)
|
||||||
|
confirm()
|
||||||
|
let delayed = mock.overlayMemberResponse(member, statusCode: 200, requestToken: token,
|
||||||
|
session: beforeConfirmation, as: GetMemberInfoResponse.self)
|
||||||
|
let decoded = try! JSONDecoder().decode(ApiResponse<GetMemberInfoResponse>.self, from: delayed).data!
|
||||||
|
assert(decoded.isAuth && decoded.isAdultContentVisible && decoded.contentType == "FEMALE",
|
||||||
|
"Late pre-confirmation GET must use true/FEMALE, got \(decoded.isAuth)/\(decoded.contentType)")
|
||||||
|
assert(decoded.can == 137 && decoded.point == 29 && decoded.role == .CREATOR)
|
||||||
|
confirm(visible: false, type: .MALE)
|
||||||
|
let updated = mock.overlayMemberResponse(member, statusCode: 200, requestToken: token,
|
||||||
|
session: beforeConfirmation, as: GetMemberInfoResponse.self)
|
||||||
|
let latest = try! JSONDecoder().decode(ApiResponse<GetMemberInfoResponse>.self, from: updated).data!
|
||||||
|
assert(latest.isAuth && !latest.isAdultContentVisible && latest.contentType == "ALL",
|
||||||
|
"Confirmation must not replace the login epoch; delayed GET uses latest confirmed choice")
|
||||||
|
print("PASS: late pre-confirmation GET uses current confirmed choice within the same login session")
|
||||||
|
}
|
||||||
|
|
||||||
|
static func checkConfirmationAndThreeFieldOverlay() {
|
||||||
|
begin()
|
||||||
|
UserDefaults.set(false, forKey: .auth)
|
||||||
|
UserDefaults.set(false, forKey: .isAdultContentVisible)
|
||||||
|
UserDefaults.set("MALE", forKey: .contentPreference)
|
||||||
|
assert(overlay() == member, "No override before confirmation")
|
||||||
|
let response = mock.confirm(request: request(), requestToken: token)!
|
||||||
|
let auth = try! JSONDecoder().decode(ApiResponse<AuthResponse>.self, from: response)
|
||||||
|
assert(auth.success && auth.data?.gender == 0)
|
||||||
|
assert(!UserDefaults.bool(forKey: .auth))
|
||||||
|
assert(!UserDefaults.bool(forKey: .isAdultContentVisible))
|
||||||
|
assert(UserDefaults.string(forKey: .contentPreference) == "MALE")
|
||||||
|
|
||||||
|
let result = overlay()
|
||||||
|
let envelope = object(result)
|
||||||
|
let data = envelope["data"] as! NSDictionary
|
||||||
|
assert(data["isAuth"] as! Bool && data["isAdultContentVisible"] as! Bool)
|
||||||
|
assert(data["contentType"] as! String == "FEMALE")
|
||||||
|
assert(CFGetTypeID(data["isAuth"] as! NSNumber) == CFBooleanGetTypeID())
|
||||||
|
assert(CFGetTypeID(data["isAdultContentVisible"] as! NSNumber) == CFBooleanGetTypeID())
|
||||||
|
let unchanged = data.mutableCopy() as! NSMutableDictionary
|
||||||
|
unchanged["isAuth"] = false
|
||||||
|
unchanged["isAdultContentVisible"] = false
|
||||||
|
unchanged["contentType"] = "MALE"
|
||||||
|
let restored = envelope.mutableCopy() as! NSMutableDictionary
|
||||||
|
restored["data"] = unchanged
|
||||||
|
assert(restored == object(member), "Only three fields may change, including unknown fields")
|
||||||
|
let decoded = try! JSONDecoder().decode(ApiResponse<GetMemberInfoResponse>.self, from: result).data!
|
||||||
|
assert(decoded.can == 137 && decoded.point == 29 && decoded.role == .CREATOR)
|
||||||
|
assert(decoded.gender == "FEMALE" && decoded.countryCode == "KR" && decoded.auditionNotice == false)
|
||||||
|
|
||||||
|
let mypageResult = mock.overlayMemberResponse(mypage, statusCode: 200, requestToken: token,
|
||||||
|
session: mock.session(for: token), as: MyPageResponse.self)
|
||||||
|
let page = try! JSONDecoder().decode(ApiResponse<MyPageResponse>.self, from: mypageResult).data!
|
||||||
|
assert(page.isAuth && page.chargeCan == 71 && page.rewardCan == 42 && page.point == 19)
|
||||||
|
let originalPage = (object(mypage)["data"] as! NSDictionary).mutableCopy() as! NSMutableDictionary
|
||||||
|
originalPage["isAuth"] = true
|
||||||
|
originalPage["isAdultContentVisible"] = true
|
||||||
|
originalPage["contentType"] = "FEMALE"
|
||||||
|
assert(originalPage == object(mypageResult)["data"] as! NSDictionary)
|
||||||
|
|
||||||
|
for (visible, type, expected) in [(true, ContentType.ALL, "ALL"), (true, .MALE, "MALE"),
|
||||||
|
(true, .FEMALE, "FEMALE"), (false, .FEMALE, "ALL")] {
|
||||||
|
confirm(visible: visible, type: type)
|
||||||
|
let data = object(overlay())["data"] as! NSDictionary
|
||||||
|
assert(data["isAdultContentVisible"] as! Bool == visible)
|
||||||
|
assert(data["contentType"] as! String == expected)
|
||||||
|
}
|
||||||
|
print("PASS: confirmation-only state, existing auth DTO, three-field self-member/mypage overlay, enum and OFF normalization")
|
||||||
|
}
|
||||||
|
|
||||||
|
static func checkInvalidResponses() {
|
||||||
|
begin()
|
||||||
|
confirm()
|
||||||
|
let invalid = ["not-json", "[]", #"{"success":false,"data":null,"message":"keep-error"}"#,
|
||||||
|
#"{"success":true,"data":null}"#, #"{"success":true,"data":{}}"#,
|
||||||
|
#"{"success":"true","data":{}}"#]
|
||||||
|
for json in invalid {
|
||||||
|
let data = Data(json.utf8)
|
||||||
|
assert(overlay(data) == data, "Invalid envelope must stay byte-identical")
|
||||||
|
assert(mock.overlayMemberResponse(data, statusCode: 200, requestToken: token,
|
||||||
|
session: mock.session(for: token), as: MyPageResponse.self) == data)
|
||||||
|
}
|
||||||
|
let unsuccessful = object(member).mutableCopy() as! NSMutableDictionary
|
||||||
|
unsuccessful["success"] = false
|
||||||
|
let unsuccessfulData = try! JSONSerialization.data(withJSONObject: unsuccessful)
|
||||||
|
assert(overlay(unsuccessfulData) == unsuccessfulData)
|
||||||
|
for status in [199, 300, 401, 403, 500] {
|
||||||
|
assert(overlay(status: status) == member, "HTTP failure must not be repaired")
|
||||||
|
}
|
||||||
|
for (key, value) in [("isAuth", "false" as Any), ("can", "137" as Any),
|
||||||
|
("role", "UNKNOWN" as Any), ("isAdultContentVisible", "false" as Any),
|
||||||
|
("contentType", 7 as Any)] {
|
||||||
|
let envelope = object(member).mutableCopy() as! NSMutableDictionary
|
||||||
|
let data = (envelope["data"] as! NSDictionary).mutableCopy() as! NSMutableDictionary
|
||||||
|
data[key] = value
|
||||||
|
envelope["data"] = data
|
||||||
|
let malformed = try! JSONSerialization.data(withJSONObject: envelope)
|
||||||
|
assert(overlay(malformed) == malformed, "DTO type failures must not be repaired")
|
||||||
|
}
|
||||||
|
print("PASS: malformed, non-success, null/missing data, DTO type and non-2xx failures are byte-identical")
|
||||||
|
}
|
||||||
|
|
||||||
|
static func checkSessionLifetimeAndIsolation() {
|
||||||
|
UserDefaults.reset()
|
||||||
|
for blank in ["", " \n "] {
|
||||||
|
UserDefaults.set(blank, forKey: .token)
|
||||||
|
assert(mock.confirm(request: request(), requestToken: blank) == nil)
|
||||||
|
assert(overlay(requestToken: blank) == member)
|
||||||
|
}
|
||||||
|
begin()
|
||||||
|
assert(mock.confirm(request: request(), requestToken: otherToken) == nil)
|
||||||
|
confirm()
|
||||||
|
let oldSession = mock.session(for: token)
|
||||||
|
assert(overlay(requestToken: otherToken) == member)
|
||||||
|
assert(mock.overlayMemberResponse(member, statusCode: 200, requestToken: token,
|
||||||
|
session: nil, as: GetMemberInfoResponse.self) == member)
|
||||||
|
UserDefaults.set("synthetic-other-name", forKey: .nickname)
|
||||||
|
assert((object(overlay())["data"] as! NSDictionary)["isAuth"] as! Bool)
|
||||||
|
assert(AuthVerificationMock.shared === mock, "Process-wide state survives owner reconstruction/Splash")
|
||||||
|
UserDefaults.reset()
|
||||||
|
assert(overlay() == member)
|
||||||
|
UserDefaults.set(token, forKey: .token)
|
||||||
|
assert(overlay() == member, "Reset must not resurrect old state")
|
||||||
|
confirm()
|
||||||
|
UserDefaults.set(token, forKey: .token)
|
||||||
|
assert(overlay() == member, "Same-token relogin must reset state")
|
||||||
|
confirm()
|
||||||
|
assert(mock.overlayMemberResponse(member, statusCode: 200, requestToken: token,
|
||||||
|
session: oldSession, as: GetMemberInfoResponse.self) == member)
|
||||||
|
let activeSession = mock.session(for: token)
|
||||||
|
UserDefaults.set(otherToken, forKey: .token)
|
||||||
|
assert(overlay(requestToken: otherToken) == member)
|
||||||
|
assert(mock.confirm(request: request(type: .MALE), requestToken: otherToken) != nil)
|
||||||
|
assert(mock.overlayMemberResponse(member, statusCode: 200, requestToken: token,
|
||||||
|
session: activeSession, as: GetMemberInfoResponse.self) == member)
|
||||||
|
assert(mock.confirm(request: request(), requestToken: token) == nil)
|
||||||
|
assert((object(overlay(requestToken: otherToken))["data"] as! NSDictionary)["contentType"] as! String == "MALE")
|
||||||
|
print("PASS: blank/mismatch/stale tokens, reset/replacement/same-token relogin and in-flight account isolation")
|
||||||
|
}
|
||||||
|
|
||||||
|
static func checkLaterPreferences() {
|
||||||
|
begin()
|
||||||
|
let update = Data(#"{"success":true,"data":{"isAdultContentVisible":true,"contentType":" male "}}"#.utf8)
|
||||||
|
mock.updateContentPreference(from: update, statusCode: 200, requestToken: token, session: nil)
|
||||||
|
mock.updateContentPreference(from: update, statusCode: 200, requestToken: token, session: mock.session(for: token))
|
||||||
|
assert(overlay() == member, "Real preference success alone must not activate mock auth")
|
||||||
|
confirm()
|
||||||
|
let session = mock.session(for: token)
|
||||||
|
mock.updateContentPreference(from: update, statusCode: 200, requestToken: token, session: session)
|
||||||
|
assert((object(overlay())["data"] as! NSDictionary)["contentType"] as! String == "MALE")
|
||||||
|
let failures = [("bad-json", 200), (#"{"success":false,"data":{"isAdultContentVisible":false,"contentType":"ALL"}}"#, 200),
|
||||||
|
(#"{"success":true,"data":null}"#, 200),
|
||||||
|
(#"{"success":true,"data":{"isAdultContentVisible":"false","contentType":"ALL"}}"#, 200),
|
||||||
|
(#"{"success":true,"data":{"isAdultContentVisible":false,"contentType":"ALL"}}"#, 401)]
|
||||||
|
for (json, status) in failures {
|
||||||
|
mock.updateContentPreference(from: Data(json.utf8), statusCode: status, requestToken: token, session: session)
|
||||||
|
assert((object(overlay())["data"] as! NSDictionary)["contentType"] as! String == "MALE")
|
||||||
|
}
|
||||||
|
let off = Data(#"{"success":true,"data":{"isAdultContentVisible":false,"contentType":"FEMALE"}}"#.utf8)
|
||||||
|
mock.updateContentPreference(from: off, statusCode: 200, requestToken: token, session: session)
|
||||||
|
let data = object(overlay())["data"] as! NSDictionary
|
||||||
|
assert(!(data["isAdultContentVisible"] as! Bool) && data["contentType"] as! String == "ALL")
|
||||||
|
UserDefaults.set(token, forKey: .token)
|
||||||
|
confirm()
|
||||||
|
mock.updateContentPreference(from: update, statusCode: 200, requestToken: token, session: session)
|
||||||
|
assert((object(overlay())["data"] as! NSDictionary)["contentType"] as! String == "FEMALE")
|
||||||
|
print("PASS: real later preference success updates active snapshot; failure/inactive/stale requests do not")
|
||||||
|
}
|
||||||
|
|
||||||
|
static func checkConcurrentCallbacks() {
|
||||||
|
begin()
|
||||||
|
confirm()
|
||||||
|
DispatchQueue.concurrentPerform(iterations: 100) { _ in
|
||||||
|
let session = mock.session(for: token)
|
||||||
|
let data = mock.overlayMemberResponse(member, statusCode: 200, requestToken: token,
|
||||||
|
session: session, as: GetMemberInfoResponse.self)
|
||||||
|
assert((object(data)["data"] as! NSDictionary)["isAuth"] as! Bool)
|
||||||
|
let update = Data(#"{"success":true,"data":{"isAdultContentVisible":true,"contentType":"FEMALE"}}"#.utf8)
|
||||||
|
mock.updateContentPreference(from: update, statusCode: 200, requestToken: token, session: session)
|
||||||
|
}
|
||||||
|
print("PASS: concurrent member/preference callbacks")
|
||||||
|
}
|
||||||
|
|
||||||
|
static func main() {
|
||||||
|
defer { UserDefaults.reset() }
|
||||||
|
checkLatePreConfirmationResponse()
|
||||||
|
checkConfirmationAndThreeFieldOverlay()
|
||||||
|
checkInvalidResponses()
|
||||||
|
checkSessionLifetimeAndIsolation()
|
||||||
|
checkLaterPreferences()
|
||||||
|
checkConcurrentCallbacks()
|
||||||
|
print("PASS: actual Foundation-only AuthVerificationMock/DTO/UserDefaults sources")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
TEMP_DIR="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$TEMP_DIR"' EXIT
|
||||||
|
|
||||||
|
swiftc -swift-version 5 -D AUTH_VERIFICATION_MOCK -module-cache-path "$TEMP_DIR/module-cache" \
|
||||||
|
"$ROOT/SodaLive/Sources/V2/Auth/AuthVerificationMock.swift" \
|
||||||
|
"$ROOT/SodaLive/Sources/Extensions/UserDefaultsExtension.swift" \
|
||||||
|
"$ROOT/SodaLive/Sources/Common/ApiResponse.swift" \
|
||||||
|
"$ROOT/SodaLive/Sources/MyPage/Auth/AuthVerifyRequest.swift" \
|
||||||
|
"$ROOT/SodaLive/Sources/MyPage/Auth/AuthResponse.swift" \
|
||||||
|
"$ROOT/SodaLive/Sources/MyPage/MyPageResponse.swift" \
|
||||||
|
"$ROOT/SodaLive/Sources/Settings/Content/ContentType.swift" \
|
||||||
|
"$ROOT/SodaLive/Sources/Settings/Content/UpdateContentPreferenceResponse.swift" \
|
||||||
|
"$ROOT/SodaLive/Sources/Settings/Notification/GetMemberInfoResponse.swift" \
|
||||||
|
"$ROOT/work/tests/AuthVerificationMockCheck.swift" \
|
||||||
|
-o "$TEMP_DIR/check-auth-verification-mock"
|
||||||
|
"$TEMP_DIR/check-auth-verification-mock"
|
||||||
|
|
||||||
|
for configuration in Debug Release; do
|
||||||
|
FLAGS=()
|
||||||
|
if [[ "$configuration" == Debug ]]; then FLAGS=(-D DEBUG); fi
|
||||||
|
swiftc -swift-version 5 -module-cache-path "$TEMP_DIR/module-cache" "${FLAGS[@]}" \
|
||||||
|
-emit-library -module-name "AuthMockStandard$configuration" \
|
||||||
|
"$ROOT/SodaLive/Sources/V2/Auth/AuthVerificationMock.swift" \
|
||||||
|
"$ROOT/SodaLive/Sources/Extensions/UserDefaultsExtension.swift" \
|
||||||
|
-o "$TEMP_DIR/standard-$configuration.dylib"
|
||||||
|
nm -j "$TEMP_DIR/standard-$configuration.dylib" > "$TEMP_DIR/symbols-$configuration"
|
||||||
|
if rg -q 'AuthVerificationMock' "$TEMP_DIR/symbols-$configuration"; then
|
||||||
|
printf 'FAIL: mock symbols in standard %s\n' "$configuration" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
printf 'PASS: standard %s contains no mock symbols\n' "$configuration"
|
||||||
|
done
|
||||||
Reference in New Issue
Block a user