From 334d66c91fbc29941e8643b90549d914d9fbd73d Mon Sep 17 00:00:00 2001 From: Ho Ngoc Hai Date: Fri, 16 Jan 2026 10:42:18 +0700 Subject: [PATCH] feat: Reimplement HomeView with a Super App layout, introducing WalletCard, ServiceGrid, PromoCarousel, and ActivityFeed components. --- .../UserInterfaceState.xcuserstate | Bin 49753 -> 50051 bytes .../Views/Home/ActivityFeed.swift | 193 ++++++++++ .../Views/Home/PromoCarousel.swift | 187 ++++++++++ .../Views/Home/ServiceGrid.swift | 119 ++++++ .../Views/Home/WalletCard.swift | 169 +++++++++ .../Views/Screens/HomeView.swift | 340 ++++++++---------- note.md | 2 +- 7 files changed, 828 insertions(+), 182 deletions(-) create mode 100644 apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Views/Home/ActivityFeed.swift create mode 100644 apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Views/Home/PromoCarousel.swift create mode 100644 apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Views/Home/ServiceGrid.swift create mode 100644 apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Views/Home/WalletCard.swift diff --git a/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift.xcodeproj/project.xcworkspace/xcuserdata/velikho.xcuserdatad/UserInterfaceState.xcuserstate b/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift.xcodeproj/project.xcworkspace/xcuserdata/velikho.xcuserdatad/UserInterfaceState.xcuserstate index 6555ef613a66b224430b84eefcc4049822aab0c3..15da0371c7f1e5bc5d603ce20651cf75b648c159 100644 GIT binary patch delta 18563 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 diff --git a/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Views/Home/ActivityFeed.swift b/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Views/Home/ActivityFeed.swift new file mode 100644 index 00000000..d3b00513 --- /dev/null +++ b/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Views/Home/ActivityFeed.swift @@ -0,0 +1,193 @@ +// +// ActivityFeed.swift +// AppClientBaseSwift +// +// Recent activity/transaction feed +// Feed hoạt động/giao dịch gần đây +// + +import SwiftUI + +// MARK: - Activity Item +// Item Activity + +/// Activity/Transaction item model +/// Model item hoạt động/giao dịch +struct ActivityItem: Identifiable { + let id = UUID() + let title: String + let subtitle: String + let amount: Double + let icon: String + let color: Color + let time: String + + /// Whether amount is positive (received) + /// Amount có dương (nhận tiền) không + var isPositive: Bool { + amount >= 0 + } + + static let samples: [ActivityItem] = [ + ActivityItem( + title: "Grab - Đặt xe", + subtitle: "Quận 1 → Quận 7", + amount: -45000, + icon: "car.fill", + color: .green, + time: "10:30" + ), + ActivityItem( + title: "Nhận tiền", + subtitle: "Từ Nguyễn Văn A", + amount: 200000, + icon: "arrow.down.circle.fill", + color: .blue, + time: "Hôm qua" + ), + ActivityItem( + title: "Thanh toán điện", + subtitle: "EVN HCM", + amount: -350000, + icon: "bolt.fill", + color: .yellow, + time: "20/01" + ), + ActivityItem( + title: "Hoàn tiền", + subtitle: "Khuyến mãi đặt xe", + amount: 15000, + icon: "gift.fill", + color: .pink, + time: "19/01" + ) + ] +} + +// MARK: - Activity Feed +// Feed Activity + +/// Activity/Transaction feed list +/// List feed hoạt động/giao dịch +struct ActivityFeed: View { + + // MARK: - Properties + + /// Activity items + /// Các items hoạt động + let activities: [ActivityItem] + + /// Item tap callback + /// Callback tap item + var onActivityTap: ((ActivityItem) -> Void)? + + // MARK: - Init + + init(activities: [ActivityItem] = ActivityItem.samples, onActivityTap: ((ActivityItem) -> Void)? = nil) { + self.activities = activities + self.onActivityTap = onActivityTap + } + + // MARK: - Body + + var body: some View { + VStack(alignment: .leading, spacing: 16) { + // Header + HStack { + Text("Hoạt động gần đây") + .font(.headline) + .fontWeight(.semibold) + + Spacer() + + Button("Xem tất cả") { + // Show all activities + } + .font(.subheadline) + .foregroundStyle(.blue) + } + + // Activity list + // Danh sách hoạt động + VStack(spacing: 0) { + ForEach(activities) { activity in + ActivityRow(activity: activity) + .onTapGesture { + onActivityTap?(activity) + } + + if activity.id != activities.last?.id { + Divider() + .padding(.leading, 56) + } + } + } + .background(Color.white) + .cornerRadius(16) + .shadow(color: .black.opacity(0.05), radius: 10, x: 0, y: 5) + } + } +} + +// MARK: - Activity Row +// Row Activity + +/// Individual activity row +/// Row hoạt động riêng lẻ +struct ActivityRow: View { + let activity: ActivityItem + + var body: some View { + HStack(spacing: 14) { + // Icon + ZStack { + Circle() + .fill(activity.color.opacity(0.15)) + .frame(width: 44, height: 44) + + Image(systemName: activity.icon) + .font(.system(size: 18)) + .foregroundStyle(activity.color) + } + + // Content + // Nội dung + VStack(alignment: .leading, spacing: 4) { + Text(activity.title) + .font(.subheadline) + .fontWeight(.medium) + .foregroundStyle(.primary) + + Text(activity.subtitle) + .font(.caption) + .foregroundStyle(.secondary) + } + + Spacer() + + // Amount + Time + VStack(alignment: .trailing, spacing: 4) { + Text(activity.isPositive ? "+\(String.formatVND(activity.amount))" : String.formatVND(activity.amount)) + .font(.subheadline) + .fontWeight(.semibold) + .foregroundStyle(activity.isPositive ? .green : .primary) + + Text(activity.time) + .font(.caption) + .foregroundStyle(.secondary) + } + } + .padding(.horizontal, 16) + .padding(.vertical, 14) + } +} + +// MARK: - Preview + +#Preview { + ZStack { + Color.gray.opacity(0.1).ignoresSafeArea() + ActivityFeed() + .padding() + } +} diff --git a/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Views/Home/PromoCarousel.swift b/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Views/Home/PromoCarousel.swift new file mode 100644 index 00000000..7137b092 --- /dev/null +++ b/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Views/Home/PromoCarousel.swift @@ -0,0 +1,187 @@ +// +// PromoCarousel.swift +// AppClientBaseSwift +// +// Promotional banners carousel +// Carousel banners khuyến mãi +// + +import SwiftUI + +// MARK: - Promo Item +// Item Promo + +/// Promotional banner item +/// Item banner khuyến mãi +struct PromoItem: Identifiable { + let id = UUID() + let title: String + let subtitle: String + let gradientColors: [Color] + let icon: String + + static let samples: [PromoItem] = [ + PromoItem( + title: "Giảm 50% Đặt xe", + subtitle: "Áp dụng đến 31/01", + gradientColors: [.orange, .red], + icon: "car.fill" + ), + PromoItem( + title: "Freeship Đồ ăn", + subtitle: "Đơn từ 50K", + gradientColors: [.green, .mint], + icon: "takeoutbag.and.cup.and.straw.fill" + ), + PromoItem( + title: "Hoàn tiền 20%", + subtitle: "Thanh toán hóa đơn", + gradientColors: [.blue, .purple], + icon: "creditcard.fill" + ) + ] +} + +// MARK: - Promo Carousel +// Carousel Promo + +/// Promotional banners carousel +/// Carousel banners khuyến mãi +struct PromoCarousel: View { + + // MARK: - Properties + + /// Promo items to display + /// Các items promo hiển thị + let items: [PromoItem] + + /// Current page index + /// Index trang hiện tại + @State private var currentIndex = 0 + + /// Auto-scroll timer + /// Timer auto-scroll + @State private var timer: Timer? + + // MARK: - Init + + init(items: [PromoItem] = PromoItem.samples) { + self.items = items + } + + // MARK: - Body + + var body: some View { + VStack(spacing: 12) { + // Carousel + TabView(selection: $currentIndex) { + ForEach(Array(items.enumerated()), id: \.element.id) { index, item in + PromoCard(item: item) + .tag(index) + } + } + .tabViewStyle(.page(indexDisplayMode: .never)) + .frame(height: 140) + + // Page indicators + // Indicators trang + HStack(spacing: 6) { + ForEach(0.. Void)? + + /// Grid columns + /// Các cột grid + private let columns = [ + GridItem(.flexible()), + GridItem(.flexible()), + GridItem(.flexible()), + GridItem(.flexible()) + ] + + // MARK: - Init + + init(services: [ServiceItem] = ServiceItem.defaultServices, onServiceTap: ((ServiceItem) -> Void)? = nil) { + self.services = services + self.onServiceTap = onServiceTap + } + + // MARK: - Body + + var body: some View { + LazyVGrid(columns: columns, spacing: 20) { + ForEach(services) { service in + ServiceGridItem(service: service) + .onTapGesture { + onServiceTap?(service) + } + } + } + } +} + +// MARK: - Service Grid Item +// Item Grid Service + +/// Individual service grid item +/// Item grid service riêng lẻ +struct ServiceGridItem: View { + let service: ServiceItem + + var body: some View { + VStack(spacing: 10) { + // Icon container + // Container icon + ZStack { + RoundedRectangle(cornerRadius: 16) + .fill(service.color.opacity(0.15)) + .frame(width: 56, height: 56) + + Image(systemName: service.icon) + .font(.system(size: 24)) + .foregroundStyle(service.color) + } + + // Label + Text(service.title) + .font(.caption) + .fontWeight(.medium) + .foregroundStyle(.primary) + .lineLimit(1) + } + } +} + +// MARK: - Preview + +#Preview { + ServiceGrid() + .padding() +} diff --git a/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Views/Home/WalletCard.swift b/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Views/Home/WalletCard.swift new file mode 100644 index 00000000..56fae324 --- /dev/null +++ b/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Views/Home/WalletCard.swift @@ -0,0 +1,169 @@ +// +// WalletCard.swift +// AppClientBaseSwift +// +// Wallet card with balance and quick actions +// Card ví với số dư và quick actions +// + +import SwiftUI + +// MARK: - Wallet Card +// Card Ví + +/// Wallet card displaying balance and quick actions +/// Card ví hiển thị số dư và quick actions +struct WalletCard: View { + + // MARK: - Properties + + /// Current balance + /// Số dư hiện tại + let balance: Double + + /// Whether balance is hidden + /// Số dư có bị ẩn không + @State private var isBalanceHidden = false + + /// Quick action callback + /// Callback quick action + var onQuickAction: ((QuickAction) -> Void)? + + // MARK: - Quick Actions Enum + + enum QuickAction: String, CaseIterable { + case topUp = "Nạp tiền" + case transfer = "Chuyển tiền" + case payment = "Thanh toán" + + var icon: String { + switch self { + case .topUp: return "plus.circle.fill" + case .transfer: return "arrow.left.arrow.right.circle.fill" + case .payment: return "qrcode" + } + } + } + + // MARK: - Body + + var body: some View { + VStack(spacing: 20) { + // Balance section + // Phần số dư + balanceSection + + // Quick actions + // Quick actions + quickActionsSection + } + .padding(20) + .background( + LinearGradient( + colors: [ + Color(red: 0.4, green: 0.5, blue: 0.92), + Color(red: 0.47, green: 0.3, blue: 0.64) + ], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + .cornerRadius(20) + .shadow(color: Color(red: 0.4, green: 0.3, blue: 0.7).opacity(0.4), radius: 15, x: 0, y: 10) + } + + // MARK: - Subviews + + /// Balance display section + /// Phần hiển thị số dư + private var balanceSection: some View { + HStack { + VStack(alignment: .leading, spacing: 6) { + Text("Số dư khả dụng") + .font(.subheadline) + .foregroundStyle(.white.opacity(0.8)) + + HStack(spacing: 8) { + if isBalanceHidden { + Text("••••••••") + .font(.system(size: 28, weight: .bold, design: .rounded)) + .foregroundStyle(.white) + } else { + Text(String.formatVND(balance)) + .font(.system(size: 28, weight: .bold, design: .rounded)) + .foregroundStyle(.white) + } + + Button { + withAnimation(.easeInOut(duration: 0.2)) { + isBalanceHidden.toggle() + } + } label: { + Image(systemName: isBalanceHidden ? "eye.slash.fill" : "eye.fill") + .font(.subheadline) + .foregroundStyle(.white.opacity(0.8)) + } + } + } + + Spacer() + + // Points badge + // Badge điểm + VStack(alignment: .trailing, spacing: 4) { + HStack(spacing: 4) { + Image(systemName: "star.circle.fill") + .foregroundStyle(.yellow) + Text("1,250") + .fontWeight(.semibold) + } + .font(.subheadline) + .foregroundStyle(.white) + + Text("điểm thưởng") + .font(.caption) + .foregroundStyle(.white.opacity(0.7)) + } + } + } + + /// Quick actions buttons section + /// Phần buttons quick actions + private var quickActionsSection: some View { + HStack(spacing: 0) { + ForEach(QuickAction.allCases, id: \.self) { action in + Button { + onQuickAction?(action) + } label: { + VStack(spacing: 8) { + ZStack { + Circle() + .fill(.white.opacity(0.2)) + .frame(width: 48, height: 48) + + Image(systemName: action.icon) + .font(.title2) + .foregroundStyle(.white) + } + + Text(action.rawValue) + .font(.caption) + .fontWeight(.medium) + .foregroundStyle(.white) + } + } + .frame(maxWidth: .infinity) + } + } + } +} + +// MARK: - Preview + +#Preview { + ZStack { + Color.gray.opacity(0.1).ignoresSafeArea() + WalletCard(balance: 1_250_000) + .padding() + } +} diff --git a/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Views/Screens/HomeView.swift b/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Views/Screens/HomeView.swift index 32d8f8f6..63fc689b 100644 --- a/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Views/Screens/HomeView.swift +++ b/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Views/Screens/HomeView.swift @@ -2,8 +2,8 @@ // HomeView.swift // AppClientBaseSwift // -// Home screen view with MVVM binding -// View màn hình Home với MVVM binding +// Super App style home screen +// Màn hình home phong cách Super App // import SwiftUI @@ -11,233 +11,211 @@ import SwiftUI // MARK: - Home View // View Home -/// Home tab main view -/// View chính của tab Home +/// Super App style home view with wallet, services, promos +/// View home phong cách Super App với ví, services, promos struct HomeView: View { // MARK: - Properties - /// ViewModel for home screen - /// ViewModel cho màn hình home - @StateObject private var viewModel = HomeViewModel() + /// Auth manager for user info + /// Auth manager cho thông tin user + @StateObject private var authManager = AuthManager.shared + + /// Mock balance + /// Số dư mock + private let balance: Double = 1_250_000 // MARK: - Body var body: some View { NavigationStack { - ScrollView { - VStack(spacing: DesignSystem.spacingLG) { - // Greeting section - // Phần lời chào - greetingSection + ScrollView(showsIndicators: false) { + VStack(spacing: 24) { + // Header + headerSection + .padding(.horizontal, 20) - // Featured carousel - // Carousel nổi bật - if !viewModel.featuredItems.isEmpty { - featuredSection + // Wallet Card + // Card Ví + WalletCard(balance: balance) { action in + handleQuickAction(action) + } + .padding(.horizontal, 20) + + // Services Grid + // Grid Services + VStack(alignment: .leading, spacing: 16) { + Text("Dịch vụ") + .font(.headline) + .fontWeight(.semibold) + .padding(.horizontal, 20) + + ServiceGrid { service in + handleServiceTap(service) + } + .padding(.horizontal, 20) } - // Main content - // Nội dung chính - contentSection + // Promo Carousel + // Carousel Promo + VStack(alignment: .leading, spacing: 16) { + HStack { + Text("Ưu đãi hấp dẫn") + .font(.headline) + .fontWeight(.semibold) + + Spacer() + + Button("Xem tất cả") { + // Show all promos + } + .font(.subheadline) + .foregroundStyle(.blue) + } + .padding(.horizontal, 20) + + PromoCarousel() + .padding(.horizontal, 16) + } + + // Activity Feed + // Feed Hoạt động + ActivityFeed { activity in + handleActivityTap(activity) + } + .padding(.horizontal, 20) + + // Bottom spacing + Spacer(minLength: 20) } - .padding(.horizontal, DesignSystem.spacingMD) - .padding(.vertical, DesignSystem.spacingSM) + .padding(.top, 10) } - .refreshable { - await viewModel.refresh() - } - .navigationTitle("tab_home".localized) - .navigationBarTitleDisplayMode(.large) - .loadingOverlay(viewModel.isLoading) - .task { - await viewModel.loadData() - } - .alert("error_title".localized, isPresented: .constant(viewModel.errorMessage != nil)) { - Button("common_ok".localized) { - viewModel.errorMessage = nil + .background(Color(red: 0.96, green: 0.97, blue: 0.98)) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + HStack(spacing: 16) { + // QR Code + Button { + // Show QR scanner + } label: { + Image(systemName: "qrcode.viewfinder") + .font(.title3) + .foregroundStyle(.primary) + } + + // Notifications + Button { + // Show notifications + } label: { + Image(systemName: "bell.badge") + .font(.title3) + .foregroundStyle(.primary) + } + } } - } message: { - Text(viewModel.errorMessage ?? "") } } } // MARK: - Subviews - /// Greeting section at top - /// Phần lời chào ở đầu - private var greetingSection: some View { - HStack { - VStack(alignment: .leading, spacing: DesignSystem.spacingXS) { - Text(viewModel.greeting) - .font(.title2) - .fontWeight(.bold) + /// Header with user info + /// Header với thông tin user + private var headerSection: some View { + HStack(spacing: 14) { + // Avatar + avatarView - Text("home_subtitle".localized) + // Greeting + VStack(alignment: .leading, spacing: 4) { + Text(greetingText) .font(.subheadline) .foregroundStyle(.secondary) + + Text(authManager.currentUser?.name ?? "Người dùng") + .font(.title3) + .fontWeight(.bold) } Spacer() - - // Notification button - // Nút thông báo - Button { - // Handle notification tap - // Xử lý tap thông báo - } label: { - Image(systemName: "bell.badge") - .font(.title2) - .foregroundStyle(.primary) - } } - .padding(.vertical, DesignSystem.spacingSM) } - /// Featured items carousel section - /// Phần carousel items nổi bật - private var featuredSection: some View { - VStack(alignment: .leading, spacing: DesignSystem.spacingSM) { - Text("home_featured".localized) - .font(.headline) - - ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: DesignSystem.spacingMD) { - ForEach(viewModel.featuredItems) { item in - FeaturedCard(item: item) - .onTapGesture { - viewModel.handleItemTap(item) - } - } + /// User avatar view + /// View avatar user + private var avatarView: some View { + Group { + if let avatarUrl = authManager.currentUser?.avatarUrl, + let url = URL(string: avatarUrl) { + AsyncImage(url: url) { image in + image + .resizable() + .aspectRatio(contentMode: .fill) + } placeholder: { + avatarPlaceholder } + } else { + avatarPlaceholder } } + .frame(width: 50, height: 50) + .clipShape(Circle()) } - /// Main content section with items - /// Phần nội dung chính với các items - private var contentSection: some View { - VStack(alignment: .leading, spacing: DesignSystem.spacingSM) { - Text("home_explore".localized) - .font(.headline) - - LazyVStack(spacing: DesignSystem.spacingMD) { - ForEach(viewModel.items) { item in - HomeItemRow(item: item) - .onTapGesture { - viewModel.handleItemTap(item) - } - } - } - } - } -} - -// MARK: - Featured Card -// Card nổi bật - -/// Featured item card view -/// View card item nổi bật -struct FeaturedCard: View { - let item: HomeItem - - var body: some View { - VStack(alignment: .leading, spacing: DesignSystem.spacingSM) { - // Placeholder image - // Ảnh placeholder - RoundedRectangle(cornerRadius: DesignSystem.cornerRadiusMD) + /// Avatar placeholder + /// Placeholder avatar + private var avatarPlaceholder: some View { + ZStack { + Circle() .fill( LinearGradient( - colors: [.blue.opacity(0.8), .purple.opacity(0.8)], + colors: [.blue, .purple], startPoint: .topLeading, endPoint: .bottomTrailing ) ) - .frame(width: 280, height: 140) - .overlay( - Image(systemName: "star.fill") - .font(.largeTitle) - .foregroundStyle(.white) - ) - VStack(alignment: .leading, spacing: DesignSystem.spacingXS) { - Text(item.title) - .font(.subheadline) - .fontWeight(.semibold) - .lineLimit(1) - - Text(item.subtitle) - .font(.caption) - .foregroundStyle(.secondary) - .lineLimit(2) - } - .frame(width: 280, alignment: .leading) + Text(authManager.currentUser?.initials ?? "U") + .font(.headline) + .fontWeight(.bold) + .foregroundStyle(.white) } } -} -// MARK: - Home Item Row -// Row item Home - -/// Home item row view -/// View row item home -struct HomeItemRow: View { - let item: HomeItem - - var body: some View { - HStack(spacing: DesignSystem.spacingMD) { - // Icon - RoundedRectangle(cornerRadius: DesignSystem.cornerRadiusSM) - .fill(Color.accentColor.opacity(0.1)) - .frame(width: 56, height: 56) - .overlay( - Image(systemName: iconForCategory(item.category)) - .font(.title2) - .foregroundStyle(Color.accentColor) - ) - - // Content - // Nội dung - VStack(alignment: .leading, spacing: DesignSystem.spacingXS) { - Text(item.title) - .font(.subheadline) - .fontWeight(.medium) - - Text(item.subtitle) - .font(.caption) - .foregroundStyle(.secondary) - } - - Spacer() - - // Arrow - Image(systemName: "chevron.right") - .font(.caption) - .foregroundStyle(.tertiary) - } - .padding(DesignSystem.spacingMD) - .cardStyle() - } - - /// Get SF Symbol icon for category - /// Lấy icon SF Symbol cho category - /// - Parameter category: Item category / Category của item - /// - Returns: SF Symbol name / Tên SF Symbol - private func iconForCategory(_ category: String) -> String { - switch category { - case "explore": - return "map" - case "promo": - return "tag.fill" - case "points": - return "star.circle.fill" - case "recommend": - return "heart.fill" + /// Greeting text based on time of day + /// Text lời chào dựa trên thời gian trong ngày + private var greetingText: String { + let hour = Calendar.current.component(.hour, from: Date()) + switch hour { + case 5..<12: + return "Chào buổi sáng 👋" + case 12..<18: + return "Chào buổi chiều ☀️" default: - return "circle.fill" + return "Chào buổi tối 🌙" } } + + // MARK: - Methods + + /// Handle quick action tap + /// Xử lý tap quick action + private func handleQuickAction(_ action: WalletCard.QuickAction) { + print("Quick action: \(action.rawValue)") + } + + /// Handle service tap + /// Xử lý tap service + private func handleServiceTap(_ service: ServiceItem) { + print("Service: \(service.title)") + } + + /// Handle activity tap + /// Xử lý tap activity + private func handleActivityTap(_ activity: ActivityItem) { + print("Activity: \(activity.title)") + } } // MARK: - Preview diff --git a/note.md b/note.md index 2446f0ee..00b2854b 100644 --- a/note.md +++ b/note.md @@ -2,7 +2,7 @@ Test Account Tài khoản: hongochai10@icloud.com Mật Khẩu: Velik@2026 -demo@goodgo.vn / Demo@123. +admin@goodgo.com / 123456 dotnet build -c Debug -f net10.0-ios -t:Run -p:_DeviceName=:v2:udid=D8A27496-0AFB-4314-96EC-E8B685575330 curl -s -X POST "http