Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,23 @@
}
}
},
"account_connected" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Connected"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "연결됨"
}
}
}
},
"account_current_section" : {
"extractionState" : "manual",
"localizations" : {
Expand Down Expand Up @@ -196,6 +213,40 @@
}
}
},
"account_not_connected" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Not Connected"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "연결되지 않음"
}
}
}
},
"account_signed_in" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "Signed In"
}
},
"ko" : {
"stringUnit" : {
"state" : "translated",
"value" : "로그인 중"
}
}
}
},
"account_social_section" : {
"extractionState" : "manual",
"localizations" : {
Expand Down
228 changes: 173 additions & 55 deletions Application/Presentation/ProfileTab/Sources/Settings/AccountView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,60 +11,76 @@ import PresentationShared
import Domain

struct AccountView: View {
@Environment(\.dismiss) private var dismiss
@State var store: StoreOf<AccountFeature>

var body: some View {
List {
Section(String(localized: "account_current_section", bundle: PresentationResources.bundle)) {
HStack {
if let provider = store.currentProvider {
providerContent(provider)
ScrollView {
LazyVStack(alignment: .leading, spacing: 24) {
if let currentProvider = store.currentProvider {
VStack(alignment: .leading, spacing: 12) {
Text("account_current_section", bundle: PresentationResources.bundle)
.font(.title3.weight(.semibold))

HStack(spacing: 12) {
ProviderIdentity(
provider: currentProvider,
status: nil
)

Spacer(minLength: 8)

Text("account_signed_in", bundle: PresentationResources.bundle)
.font(.caption.weight(.semibold))
.foregroundStyle(Color.accent)
.padding(.horizontal, 12)
.padding(.vertical, 7)
.background(Color.accent.opacity(0.1), in: .capsule)
}
.padding(16)
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color.surface, in: .rect(cornerRadius: 16))
}
}
}
Section(String(localized: "account_social_section", bundle: PresentationResources.bundle)) {

let providers = AuthProvider.allCases.filter { $0 != store.currentProvider }
ForEach(providers, id: \.self) { provider in
let isConnected = store.connectedProviders.contains(provider)
let showProgressView = store.isLoading && store.activeLoadingProvider == provider
HStack {
providerContent(provider)
Spacer()
Button {
if isConnected {
store.send(.unlinkFromProvider(provider))
} else {
store.send(.linkWithProvider(provider))
}
} label: {
Text(isConnected
? String(localized: "account_disconnect", bundle: PresentationResources.bundle)
: String(localized: "account_connect", bundle: PresentationResources.bundle))
.font(.caption.weight(.semibold))
.foregroundStyle(.white)
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(isConnected ? Color.red : .blue)
.clipShape(.capsule)
}
.buttonStyle(.plain)
.disabled(
store.isLoading ||
(!isConnected && store.presentationContext == nil)
)
.opacity(showProgressView ? 0 : 1)
.overlay(alignment: .trailing) {
if showProgressView {
ProgressView()
VStack(alignment: .leading, spacing: 12) {
Text("account_social_section", bundle: PresentationResources.bundle)
.font(.title3.weight(.semibold))

VStack(spacing: 0) {
ForEach(providers, id: \.self) { provider in
let isConnected = store.connectedProviders.contains(provider)
ProviderRow(
provider: provider,
isConnected: isConnected,
showsProgress: store.isLoading && store.activeLoadingProvider == provider,
isDisabled: store.isLoading || (!isConnected && store.presentationContext == nil),
action: {
if isConnected {
store.send(.unlinkFromProvider(provider))
} else {
store.send(.linkWithProvider(provider))
}
}
)

if provider != providers.last {
Divider()
.padding(.leading, 64)
.padding(.trailing, 16)
}
}
}
.background(Color.surface, in: .rect(cornerRadius: 16))
}
}
.padding(.horizontal, 16)
.padding(.bottom, 24)
}
.scrollDisabled(true)
.listStyle(.insetGrouped)
.navigationTitle(String(localized: "nav_account", bundle: PresentationResources.bundle))
.safeAreaInset(edge: .top, spacing: 0) { topBar }
.background(Color.appBackground)
.toolbarVisibility(.hidden, for: .navigationBar)
.onAppear { store.send(.onAppear) }
.prominentAlert(store, state: \.alert, action: \.alert)
.background {
Expand All @@ -75,20 +91,122 @@ struct AccountView: View {
}
}
}

private func formattedProviderName(_ provider: AuthProvider) -> String {
let rawValue = provider.rawValue
let providerPrefix = rawValue.prefix(1).uppercased()
let providerSuffix = rawValue.dropFirst().prefix(while: { $0 != "." })
return providerPrefix + providerSuffix

private var topBar: some View {
ZStack {
Text(String(localized: "nav_account", bundle: PresentationResources.bundle))
.font(.headline)

HStack {
NavigationBackButton(action: { dismiss() })
Spacer()
}
}
.padding(.horizontal, 16)
.padding(.bottom, 12)
.background(Color.appBackground)
.toolbarBackground(Color.appBackground)
}
}

private struct ProviderRow: View {
let provider: AuthProvider
let isConnected: Bool
let showsProgress: Bool
let isDisabled: Bool
let action: () -> Void

var body: some View {
HStack(spacing: 12) {
ProviderIdentity(
provider: provider,
status: String(
localized: isConnected ? "account_connected" : "account_not_connected",
bundle: PresentationResources.bundle
)
)

Spacer(minLength: 8)

Button(action: action) {
ZStack {
Text(isConnected
? String(localized: "account_disconnect", bundle: PresentationResources.bundle)
: String(localized: "account_connect", bundle: PresentationResources.bundle))
.opacity(showsProgress ? 0 : 1)

if showsProgress {
ProgressView()
.tint(isConnected ? Color.danger : Color.white)
}
}
.font(.caption.weight(.semibold))
.foregroundStyle(isConnected ? Color.danger : Color.white)
.padding(.horizontal, 12)
.padding(.vertical, 7)
.background(isConnected ? Color.clear : Color.accent, in: .capsule)
.overlay {
Capsule()
.stroke(isConnected ? Color.danger : Color.clear)
}
}
.buttonStyle(.plain)
.disabled(isDisabled)
.opacity(isDisabled && !showsProgress ? 0.5 : 1)
}
.padding(16)
.frame(maxWidth: .infinity, alignment: .leading)
}
}

private struct ProviderIdentity: View {
let provider: AuthProvider
let status: String?

var body: some View {
HStack(spacing: 12) {
Image(provider.imageName, bundle: PresentationResources.bundle)
.resizable()
.scaledToFit()
.frame(width: 20, height: 20)
.frame(width: 36, height: 36)
.background(Color.surfaceSecondary, in: Circle())

VStack(alignment: .leading, spacing: 2) {
Text(provider.displayName)
.font(.headline)
.foregroundStyle(Color.primary)

if let status {
Text(status)
.font(.footnote)
.foregroundStyle(Color.textSecondary)
}
}
}
}
}

@ViewBuilder
private func providerContent(_ provider: AuthProvider) -> some View {
Image(formattedProviderName(provider))
.resizable()
.scaledToFit()
.frame(width: UIFont.labelFontSize)
Text(formattedProviderName(provider))
private extension AuthProvider {
var displayName: String {
switch self {
case .apple:
return "Apple"
case .google:
return "Google"
case .github:
return "GitHub"
}
}

var imageName: String {
switch self {
case .apple:
return "Apple"
case .google:
return "Google"
case .github:
return "Github"
}
}
}
Loading