diff --git a/PayfritWorks.xcodeproj/project.pbxproj b/PayfritWorks.xcodeproj/project.pbxproj index 8d72368..4424436 100644 --- a/PayfritWorks.xcodeproj/project.pbxproj +++ b/PayfritWorks.xcodeproj/project.pbxproj @@ -28,6 +28,9 @@ B01000000032 /* BeaconScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = B02000000032; }; B01000000033 /* ChatService.swift in Sources */ = {isa = PBXBuildFile; fileRef = B02000000033; }; + /* Widgets */ + B01000000050 /* DevRibbon.swift in Sources */ = {isa = PBXBuildFile; fileRef = B02000000050; }; + /* Views */ B01000000040 /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B02000000040; }; B01000000041 /* LoginScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = B02000000041; }; @@ -40,6 +43,7 @@ /* Resources */ B01000000060 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B02000000060; }; + B01000000070 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = B05000000008; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -68,6 +72,9 @@ B02000000032 /* BeaconScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BeaconScanner.swift; sourceTree = ""; }; B02000000033 /* ChatService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatService.swift; sourceTree = ""; }; + /* Widgets */ + B02000000050 /* DevRibbon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DevRibbon.swift; sourceTree = ""; }; + /* Views */ B02000000040 /* RootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = ""; }; B02000000041 /* LoginScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreen.swift; sourceTree = ""; }; @@ -80,6 +87,7 @@ /* Resources */ B02000000060 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + B02000000070 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -106,9 +114,11 @@ children = ( B02000000001 /* PayfritWorksApp.swift */, B02000000002 /* Info.plist */, + B05000000008 /* InfoPlist.strings */, B05000000003 /* Models */, B05000000004 /* ViewModels */, B05000000005 /* Services */, + B05000000011 /* Widgets */, B05000000006 /* Views */, B05000000007 /* Resources */, ); @@ -148,6 +158,14 @@ path = Services; sourceTree = ""; }; + B05000000011 /* Widgets */ = { + isa = PBXGroup; + children = ( + B02000000050 /* DevRibbon.swift */, + ); + path = Widgets; + sourceTree = ""; + }; B05000000006 /* Views */ = { isa = PBXGroup; children = ( @@ -181,6 +199,17 @@ }; /* End PBXGroup section */ +/* Begin PBXVariantGroup section */ + B05000000008 /* InfoPlist.strings */ = { + isa = PBXVariantGroup; + children = ( + B02000000070 /* en */, + ); + name = InfoPlist.strings; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + /* Begin PBXNativeTarget section */ B06000000001 /* PayfritWorks */ = { isa = PBXNativeTarget; @@ -238,6 +267,7 @@ buildActionMask = 2147483647; files = ( B01000000060 /* Assets.xcassets in Resources */, + B01000000070 /* InfoPlist.strings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -261,6 +291,7 @@ B01000000031 /* AuthStorage.swift in Sources */, B01000000032 /* BeaconScanner.swift in Sources */, B01000000033 /* ChatService.swift in Sources */, + B01000000050 /* DevRibbon.swift in Sources */, B01000000040 /* RootView.swift in Sources */, B01000000041 /* LoginScreen.swift in Sources */, B01000000042 /* BusinessSelectionScreen.swift in Sources */, @@ -398,7 +429,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = U83YL8VRF3; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = PayfritWorks/Info.plist; @@ -415,7 +446,7 @@ ); MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.payfrit.works; - PRODUCT_NAME = "$(TARGET_NAME)"; + PRODUCT_NAME = "Payfrit Works"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SWIFT_EMIT_LOC_STRINGS = YES; @@ -430,7 +461,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = U83YL8VRF3; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = PayfritWorks/Info.plist; @@ -447,7 +478,7 @@ ); MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.payfrit.works; - PRODUCT_NAME = "$(TARGET_NAME)"; + PRODUCT_NAME = "Payfrit Works"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SWIFT_EMIT_LOC_STRINGS = YES; diff --git a/PayfritWorks/Assets.xcassets/AppIcon.appiconset/appicon.png b/PayfritWorks/Assets.xcassets/AppIcon.appiconset/appicon.png index e1aa381..603b024 100644 Binary files a/PayfritWorks/Assets.xcassets/AppIcon.appiconset/appicon.png and b/PayfritWorks/Assets.xcassets/AppIcon.appiconset/appicon.png differ diff --git a/PayfritWorks/Info.plist b/PayfritWorks/Info.plist index 3f6383c..2c3c0ab 100644 --- a/PayfritWorks/Info.plist +++ b/PayfritWorks/Info.plist @@ -13,7 +13,7 @@ CFBundleDisplayName Payfrit Works CFBundleName - $(PRODUCT_NAME) + Payfrit Works CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString @@ -28,6 +28,11 @@ Payfrit Works uses Bluetooth to scan for nearby beacons. NSFaceIDUsageDescription Payfrit Works uses Face ID for quick sign-in. + UILaunchScreen + + UIColorName + + UIApplicationSceneManifest UIApplicationSupportsMultipleScenes diff --git a/PayfritWorks/PayfritWorksApp.swift b/PayfritWorks/PayfritWorksApp.swift index 98c3e87..28da002 100644 --- a/PayfritWorks/PayfritWorksApp.swift +++ b/PayfritWorks/PayfritWorksApp.swift @@ -8,6 +8,7 @@ struct PayfritWorksApp: App { WindowGroup { RootView() .environmentObject(appState) + .devRibbon() } } } diff --git a/PayfritWorks/Services/APIService.swift b/PayfritWorks/Services/APIService.swift index 36ae6af..57be562 100644 --- a/PayfritWorks/Services/APIService.swift +++ b/PayfritWorks/Services/APIService.swift @@ -1,5 +1,11 @@ import Foundation +// MARK: - Dev Flag + +/// Master flag: flip to `false` for production builds. +/// Controls API endpoint, dev banner, and magic OTPs. +let IS_DEV = true + // MARK: - API Errors enum APIError: LocalizedError { @@ -43,24 +49,14 @@ struct ChatMessagesResult { actor APIService { static let shared = APIService() - private enum Environment { - case development, production + private static let devBaseURL = "https://dev.payfrit.com/api" + private static let prodBaseURL = "https://biz.payfrit.com/api" - var baseURL: String { - switch self { - case .development: return "https://dev.payfrit.com/api" - case .production: return "https://biz.payfrit.com/api" - } - } - } - - private let environment: Environment = .development - var isDev: Bool { environment == .development } private var userToken: String? private var userId: Int? private var businessId: Int = 0 - var baseURL: String { environment.baseURL } + var baseURL: String { IS_DEV ? Self.devBaseURL : Self.prodBaseURL } // MARK: - Configuration @@ -80,7 +76,7 @@ actor APIService { // MARK: - Core Request private func postJSON(_ path: String, payload: [String: Any]) async throws -> [String: Any] { - let urlString = environment.baseURL + (path.hasPrefix("/") ? path : "/\(path)") + let urlString = baseURL + (path.hasPrefix("/") ? path : "/\(path)") guard let url = URL(string: urlString) else { throw APIError.invalidURL } var request = URLRequest(url: url) @@ -482,7 +478,7 @@ actor APIService { /// Returns raw JSON string for a given endpoint (for debugging key issues) func debugRawJSON(_ path: String, payload: [String: Any]) async -> String { - let urlString = environment.baseURL + (path.hasPrefix("/") ? path : "/\(path)") + let urlString = baseURL + (path.hasPrefix("/") ? path : "/\(path)") guard let url = URL(string: urlString) else { return "Invalid URL" } var request = URLRequest(url: url) request.httpMethod = "POST" @@ -509,8 +505,7 @@ actor APIService { let trimmed = rawUrl.trimmingCharacters(in: .whitespacesAndNewlines) if trimmed.isEmpty { return "" } if trimmed.hasPrefix("http://") || trimmed.hasPrefix("https://") { return trimmed } - // Relative URL — prepend base domain - let baseDomain = "https://dev.payfrit.com" + let baseDomain = IS_DEV ? "https://dev.payfrit.com" : "https://biz.payfrit.com" if trimmed.hasPrefix("/") { return baseDomain + trimmed } return baseDomain + "/" + trimmed } diff --git a/PayfritWorks/Views/BusinessSelectionScreen.swift b/PayfritWorks/Views/BusinessSelectionScreen.swift index 8307b7c..f9737bf 100644 --- a/PayfritWorks/Views/BusinessSelectionScreen.swift +++ b/PayfritWorks/Views/BusinessSelectionScreen.swift @@ -108,8 +108,15 @@ struct BusinessSelectionScreen: View { ScrollView { LazyVStack(spacing: 12) { ForEach(businesses) { emp in - businessCard(emp) - .onTapGesture { selectBusiness(emp) } + if emp.pendingTaskCount > 0 { + Button { selectBusiness(emp) } label: { + businessCard(emp) + } + .buttonStyle(.plain) + } else { + businessCard(emp) + .opacity(0.5) + } } } .padding(.horizontal, 16) @@ -120,67 +127,72 @@ struct BusinessSelectionScreen: View { } private func businessCard(_ emp: Employment) -> some View { - VStack(spacing: 0) { - // Header image with brand color background - BusinessHeaderImage(businessId: emp.businessId) + HStack(spacing: 14) { + // Initial letter + Text(String(emp.businessName.prefix(1)).uppercased()) + .font(.system(size: 32, weight: .bold)) + .foregroundColor(.primary.opacity(0.7)) + .frame(width: 50) - // Info bar - HStack(spacing: 12) { - VStack(alignment: .leading, spacing: 2) { - Text(emp.businessName) - .font(.subheadline.weight(.semibold)) - .foregroundColor(.primary) - .lineLimit(1) + // Name + status + VStack(alignment: .leading, spacing: 4) { + Text(emp.businessName) + .font(.body.weight(.semibold)) + .foregroundColor(.primary) + .lineLimit(1) - Text(emp.statusName) - .font(.caption2) - .fontWeight(.medium) - .foregroundColor(statusColor(emp.employeeStatusId)) - } - - Spacer() - - // Task count badge - if emp.pendingTaskCount > 0 { - Text("\(emp.pendingTaskCount)") - .font(.caption.bold()) - .foregroundColor(.white) - .padding(.horizontal, 8) - .padding(.vertical, 3) - .background(Color.payfritGreen) - .clipShape(Capsule()) - } else { - Image(systemName: "checkmark.circle.fill") - .foregroundColor(.green) - .font(.body) - } - - Image(systemName: "chevron.right") - .font(.caption) - .foregroundColor(.secondary) + Text(emp.statusName) + .font(.caption.weight(.medium)) + .foregroundColor(.white) + .padding(.horizontal, 8) + .padding(.vertical, 2) + .background(statusColor(emp.employeeStatusId)) + .clipShape(Capsule()) } - .padding(.horizontal, 12) - .padding(.vertical, 8) + + Spacer() + + // Task count or clear checkmark + if emp.pendingTaskCount > 0 { + Text("\(emp.pendingTaskCount)") + .font(.title3.bold()) + .foregroundColor(.payfritGreen) + } else { + VStack(spacing: 2) { + Image(systemName: "checkmark.circle.fill") + .font(.title2) + .foregroundColor(.payfritGreen) + Text("clear") + .font(.caption2) + .foregroundColor(.secondary) + } + } + + Image(systemName: "chevron.right") + .font(.caption.weight(.semibold)) + .foregroundColor(.secondary) } - .background(Color(.systemBackground)) + .padding(.horizontal, 16) + .padding(.vertical, 14) + .background(Color.payfritGreen.opacity(0.08)) .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous)) - .overlay( - RoundedRectangle(cornerRadius: 12, style: .continuous) - .stroke(Color(.systemGray4), lineWidth: 0.5) - ) - .shadow(color: .black.opacity(0.06), radius: 4, x: 0, y: 2) .contentShape(Rectangle()) } private var myTasksFAB: some View { Button { showingMyTasks = true } label: { - Image(systemName: "checkmark.circle.fill") - .font(.title3) - .padding(12) - .background(Color.payfritGreen) - .foregroundColor(.white) - .clipShape(Circle()) - .shadow(radius: 4) + HStack(spacing: 8) { + Image(systemName: "checkmark.square.fill") + .font(.title3) + Text("My Tasks") + .font(.body.weight(.semibold)) + } + .padding(.horizontal, 20) + .padding(.vertical, 14) + .background(Color.payfritGreen) + .foregroundColor(.white) + .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous)) + .shadow(color: .black.opacity(0.15), radius: 6, x: 0, y: 3) } .padding(.trailing, 16) .padding(.bottom, 16) @@ -294,9 +306,10 @@ struct BusinessHeaderImage: View { @State private var isLoading = true private var imageURLs: [URL] { - [ - "https://dev.payfrit.com/uploads/headers/\(businessId).png", - "https://dev.payfrit.com/uploads/headers/\(businessId).jpg", + let domain = IS_DEV ? "https://dev.payfrit.com" : "https://biz.payfrit.com" + return [ + "\(domain)/uploads/headers/\(businessId).png", + "\(domain)/uploads/headers/\(businessId).jpg", ].compactMap { URL(string: $0) } } diff --git a/PayfritWorks/Views/LoginScreen.swift b/PayfritWorks/Views/LoginScreen.swift index dfdb7c7..e069a9c 100644 --- a/PayfritWorks/Views/LoginScreen.swift +++ b/PayfritWorks/Views/LoginScreen.swift @@ -7,7 +7,6 @@ struct LoginScreen: View { @State private var showPassword = false @State private var isLoading = false @State private var error: String? - @State private var isDev = false var body: some View { GeometryReader { geo in @@ -25,7 +24,7 @@ struct LoginScreen: View { Text("Sign in to view and claim tasks") .foregroundColor(.secondary) - if isDev { + if IS_DEV { Text("DEV MODE — password: 123456") .font(.caption) .foregroundColor(.red) @@ -99,7 +98,6 @@ struct LoginScreen: View { } } .background(Color(.systemGroupedBackground)) - .task { isDev = await APIService.shared.isDev } } private func login() { diff --git a/PayfritWorks/Views/RootView.swift b/PayfritWorks/Views/RootView.swift index cfc7223..423234f 100644 --- a/PayfritWorks/Views/RootView.swift +++ b/PayfritWorks/Views/RootView.swift @@ -4,7 +4,6 @@ import LocalAuthentication struct RootView: View { @EnvironmentObject var appState: AppState @State private var isCheckingAuth = true - @State private var isDev = false var body: some View { Group { @@ -17,20 +16,7 @@ struct RootView: View { } } .animation(.easeInOut(duration: 0.3), value: appState.isAuthenticated) - .overlay(alignment: .bottomLeading) { - if isDev { - Text("DEV") - .font(.caption.bold()) - .foregroundColor(.white) - .frame(width: 80, height: 20) - .background(Color.orange) - .rotationEffect(.degrees(45)) - .offset(x: -20, y: -6) - .allowsHitTesting(false) - } - } .task { - isDev = await APIService.shared.isDev await checkAuthWithBiometrics() isCheckingAuth = false } diff --git a/PayfritWorks/Widgets/DevRibbon.swift b/PayfritWorks/Widgets/DevRibbon.swift new file mode 100644 index 0000000..da3f337 --- /dev/null +++ b/PayfritWorks/Widgets/DevRibbon.swift @@ -0,0 +1,36 @@ +import SwiftUI + +// MARK: - DEV Ribbon Overlay + +/// Diagonal orange "DEV" ribbon in the lower-left corner. +/// Only visible when IS_DEV is true. +/// Taps pass through to content below. +struct DevRibbonModifier: ViewModifier { + func body(content: Content) -> some View { + content + .overlay(alignment: .bottomLeading) { + if IS_DEV { + ribbon + .allowsHitTesting(false) + } + } + } + + private var ribbon: some View { + Text("DEV") + .font(.system(size: 11, weight: .bold)) + .tracking(2) + .foregroundColor(.white) + .frame(width: 130, height: 24) + .background(Color(red: 1.0, green: 0.596, blue: 0.0)) // #FF9800 + .rotationEffect(.degrees(45)) + .offset(x: -28, y: -28) + .shadow(color: .black.opacity(0.25), radius: 2, x: 0, y: 1) + } +} + +extension View { + func devRibbon() -> some View { + modifier(DevRibbonModifier()) + } +} diff --git a/PayfritWorks/en.lproj/InfoPlist.strings b/PayfritWorks/en.lproj/InfoPlist.strings new file mode 100644 index 0000000..fc1d95f --- /dev/null +++ b/PayfritWorks/en.lproj/InfoPlist.strings @@ -0,0 +1,2 @@ +CFBundleDisplayName = "Payfrit Works"; +CFBundleName = "Payfrit Works";