Add IS_DEV flag, dev ribbon, new app icon, and UI improvements
- Add global IS_DEV flag controlling API endpoint (dev vs biz.payfrit.com), dev ribbon banner, and magic OTP hints - Add diagonal orange DEV ribbon overlay (Widgets/DevRibbon.swift) - Replace app icon with properly centered dark-outline SVG on white background - Fix display name with InfoPlist.strings localization - Redesign business selection cards with initial letter, status pill, task count - Make businesses only tappable when pending tasks > 0 (dimmed otherwise) - Simplify LoginScreen and RootView to use IS_DEV directly - Fix hardcoded dev URLs to respect IS_DEV flag Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
75c0698ec4
commit
7dfe8f593e
10 changed files with 162 additions and 95 deletions
|
|
@ -28,6 +28,9 @@
|
||||||
B01000000032 /* BeaconScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = B02000000032; };
|
B01000000032 /* BeaconScanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = B02000000032; };
|
||||||
B01000000033 /* ChatService.swift in Sources */ = {isa = PBXBuildFile; fileRef = B02000000033; };
|
B01000000033 /* ChatService.swift in Sources */ = {isa = PBXBuildFile; fileRef = B02000000033; };
|
||||||
|
|
||||||
|
/* Widgets */
|
||||||
|
B01000000050 /* DevRibbon.swift in Sources */ = {isa = PBXBuildFile; fileRef = B02000000050; };
|
||||||
|
|
||||||
/* Views */
|
/* Views */
|
||||||
B01000000040 /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B02000000040; };
|
B01000000040 /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B02000000040; };
|
||||||
B01000000041 /* LoginScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = B02000000041; };
|
B01000000041 /* LoginScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = B02000000041; };
|
||||||
|
|
@ -40,6 +43,7 @@
|
||||||
|
|
||||||
/* Resources */
|
/* Resources */
|
||||||
B01000000060 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B02000000060; };
|
B01000000060 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B02000000060; };
|
||||||
|
B01000000070 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = B05000000008; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
|
|
@ -68,6 +72,9 @@
|
||||||
B02000000032 /* BeaconScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BeaconScanner.swift; sourceTree = "<group>"; };
|
B02000000032 /* BeaconScanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BeaconScanner.swift; sourceTree = "<group>"; };
|
||||||
B02000000033 /* ChatService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatService.swift; sourceTree = "<group>"; };
|
B02000000033 /* ChatService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatService.swift; sourceTree = "<group>"; };
|
||||||
|
|
||||||
|
/* Widgets */
|
||||||
|
B02000000050 /* DevRibbon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DevRibbon.swift; sourceTree = "<group>"; };
|
||||||
|
|
||||||
/* Views */
|
/* Views */
|
||||||
B02000000040 /* RootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = "<group>"; };
|
B02000000040 /* RootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = "<group>"; };
|
||||||
B02000000041 /* LoginScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreen.swift; sourceTree = "<group>"; };
|
B02000000041 /* LoginScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginScreen.swift; sourceTree = "<group>"; };
|
||||||
|
|
@ -80,6 +87,7 @@
|
||||||
|
|
||||||
/* Resources */
|
/* Resources */
|
||||||
B02000000060 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
B02000000060 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
|
B02000000070 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
|
@ -106,9 +114,11 @@
|
||||||
children = (
|
children = (
|
||||||
B02000000001 /* PayfritWorksApp.swift */,
|
B02000000001 /* PayfritWorksApp.swift */,
|
||||||
B02000000002 /* Info.plist */,
|
B02000000002 /* Info.plist */,
|
||||||
|
B05000000008 /* InfoPlist.strings */,
|
||||||
B05000000003 /* Models */,
|
B05000000003 /* Models */,
|
||||||
B05000000004 /* ViewModels */,
|
B05000000004 /* ViewModels */,
|
||||||
B05000000005 /* Services */,
|
B05000000005 /* Services */,
|
||||||
|
B05000000011 /* Widgets */,
|
||||||
B05000000006 /* Views */,
|
B05000000006 /* Views */,
|
||||||
B05000000007 /* Resources */,
|
B05000000007 /* Resources */,
|
||||||
);
|
);
|
||||||
|
|
@ -148,6 +158,14 @@
|
||||||
path = Services;
|
path = Services;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
B05000000011 /* Widgets */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
B02000000050 /* DevRibbon.swift */,
|
||||||
|
);
|
||||||
|
path = Widgets;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
B05000000006 /* Views */ = {
|
B05000000006 /* Views */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
|
@ -181,6 +199,17 @@
|
||||||
};
|
};
|
||||||
/* End PBXGroup section */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
|
/* Begin PBXVariantGroup section */
|
||||||
|
B05000000008 /* InfoPlist.strings */ = {
|
||||||
|
isa = PBXVariantGroup;
|
||||||
|
children = (
|
||||||
|
B02000000070 /* en */,
|
||||||
|
);
|
||||||
|
name = InfoPlist.strings;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* End PBXVariantGroup section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
/* Begin PBXNativeTarget section */
|
||||||
B06000000001 /* PayfritWorks */ = {
|
B06000000001 /* PayfritWorks */ = {
|
||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
|
|
@ -238,6 +267,7 @@
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
B01000000060 /* Assets.xcassets in Resources */,
|
B01000000060 /* Assets.xcassets in Resources */,
|
||||||
|
B01000000070 /* InfoPlist.strings in Resources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
|
@ -261,6 +291,7 @@
|
||||||
B01000000031 /* AuthStorage.swift in Sources */,
|
B01000000031 /* AuthStorage.swift in Sources */,
|
||||||
B01000000032 /* BeaconScanner.swift in Sources */,
|
B01000000032 /* BeaconScanner.swift in Sources */,
|
||||||
B01000000033 /* ChatService.swift in Sources */,
|
B01000000033 /* ChatService.swift in Sources */,
|
||||||
|
B01000000050 /* DevRibbon.swift in Sources */,
|
||||||
B01000000040 /* RootView.swift in Sources */,
|
B01000000040 /* RootView.swift in Sources */,
|
||||||
B01000000041 /* LoginScreen.swift in Sources */,
|
B01000000041 /* LoginScreen.swift in Sources */,
|
||||||
B01000000042 /* BusinessSelectionScreen.swift in Sources */,
|
B01000000042 /* BusinessSelectionScreen.swift in Sources */,
|
||||||
|
|
@ -398,7 +429,7 @@
|
||||||
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_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 2;
|
||||||
DEVELOPMENT_TEAM = U83YL8VRF3;
|
DEVELOPMENT_TEAM = U83YL8VRF3;
|
||||||
GENERATE_INFOPLIST_FILE = NO;
|
GENERATE_INFOPLIST_FILE = NO;
|
||||||
INFOPLIST_FILE = PayfritWorks/Info.plist;
|
INFOPLIST_FILE = PayfritWorks/Info.plist;
|
||||||
|
|
@ -415,7 +446,7 @@
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.payfrit.works;
|
PRODUCT_BUNDLE_IDENTIFIER = com.payfrit.works;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "Payfrit Works";
|
||||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
SUPPORTS_MACCATALYST = NO;
|
SUPPORTS_MACCATALYST = NO;
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
|
@ -430,7 +461,7 @@
|
||||||
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_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 1;
|
CURRENT_PROJECT_VERSION = 2;
|
||||||
DEVELOPMENT_TEAM = U83YL8VRF3;
|
DEVELOPMENT_TEAM = U83YL8VRF3;
|
||||||
GENERATE_INFOPLIST_FILE = NO;
|
GENERATE_INFOPLIST_FILE = NO;
|
||||||
INFOPLIST_FILE = PayfritWorks/Info.plist;
|
INFOPLIST_FILE = PayfritWorks/Info.plist;
|
||||||
|
|
@ -447,7 +478,7 @@
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.payfrit.works;
|
PRODUCT_BUNDLE_IDENTIFIER = com.payfrit.works;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "Payfrit Works";
|
||||||
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
|
||||||
SUPPORTS_MACCATALYST = NO;
|
SUPPORTS_MACCATALYST = NO;
|
||||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 41 KiB |
|
|
@ -13,7 +13,7 @@
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>Payfrit Works</string>
|
<string>Payfrit Works</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>$(PRODUCT_NAME)</string>
|
<string>Payfrit Works</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
|
|
@ -28,6 +28,11 @@
|
||||||
<string>Payfrit Works uses Bluetooth to scan for nearby beacons.</string>
|
<string>Payfrit Works uses Bluetooth to scan for nearby beacons.</string>
|
||||||
<key>NSFaceIDUsageDescription</key>
|
<key>NSFaceIDUsageDescription</key>
|
||||||
<string>Payfrit Works uses Face ID for quick sign-in.</string>
|
<string>Payfrit Works uses Face ID for quick sign-in.</string>
|
||||||
|
<key>UILaunchScreen</key>
|
||||||
|
<dict>
|
||||||
|
<key>UIColorName</key>
|
||||||
|
<string></string>
|
||||||
|
</dict>
|
||||||
<key>UIApplicationSceneManifest</key>
|
<key>UIApplicationSceneManifest</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>UIApplicationSupportsMultipleScenes</key>
|
<key>UIApplicationSupportsMultipleScenes</key>
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ struct PayfritWorksApp: App {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
RootView()
|
RootView()
|
||||||
.environmentObject(appState)
|
.environmentObject(appState)
|
||||||
|
.devRibbon()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,11 @@
|
||||||
import Foundation
|
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
|
// MARK: - API Errors
|
||||||
|
|
||||||
enum APIError: LocalizedError {
|
enum APIError: LocalizedError {
|
||||||
|
|
@ -43,24 +49,14 @@ struct ChatMessagesResult {
|
||||||
actor APIService {
|
actor APIService {
|
||||||
static let shared = APIService()
|
static let shared = APIService()
|
||||||
|
|
||||||
private enum Environment {
|
private static let devBaseURL = "https://dev.payfrit.com/api"
|
||||||
case development, production
|
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 userToken: String?
|
||||||
private var userId: Int?
|
private var userId: Int?
|
||||||
private var businessId: Int = 0
|
private var businessId: Int = 0
|
||||||
|
|
||||||
var baseURL: String { environment.baseURL }
|
var baseURL: String { IS_DEV ? Self.devBaseURL : Self.prodBaseURL }
|
||||||
|
|
||||||
// MARK: - Configuration
|
// MARK: - Configuration
|
||||||
|
|
||||||
|
|
@ -80,7 +76,7 @@ actor APIService {
|
||||||
// MARK: - Core Request
|
// MARK: - Core Request
|
||||||
|
|
||||||
private func postJSON(_ path: String, payload: [String: Any]) async throws -> [String: Any] {
|
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 }
|
guard let url = URL(string: urlString) else { throw APIError.invalidURL }
|
||||||
|
|
||||||
var request = URLRequest(url: url)
|
var request = URLRequest(url: url)
|
||||||
|
|
@ -482,7 +478,7 @@ actor APIService {
|
||||||
|
|
||||||
/// Returns raw JSON string for a given endpoint (for debugging key issues)
|
/// Returns raw JSON string for a given endpoint (for debugging key issues)
|
||||||
func debugRawJSON(_ path: String, payload: [String: Any]) async -> String {
|
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" }
|
guard let url = URL(string: urlString) else { return "Invalid URL" }
|
||||||
var request = URLRequest(url: url)
|
var request = URLRequest(url: url)
|
||||||
request.httpMethod = "POST"
|
request.httpMethod = "POST"
|
||||||
|
|
@ -509,8 +505,7 @@ actor APIService {
|
||||||
let trimmed = rawUrl.trimmingCharacters(in: .whitespacesAndNewlines)
|
let trimmed = rawUrl.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
if trimmed.isEmpty { return "" }
|
if trimmed.isEmpty { return "" }
|
||||||
if trimmed.hasPrefix("http://") || trimmed.hasPrefix("https://") { return trimmed }
|
if trimmed.hasPrefix("http://") || trimmed.hasPrefix("https://") { return trimmed }
|
||||||
// Relative URL — prepend base domain
|
let baseDomain = IS_DEV ? "https://dev.payfrit.com" : "https://biz.payfrit.com"
|
||||||
let baseDomain = "https://dev.payfrit.com"
|
|
||||||
if trimmed.hasPrefix("/") { return baseDomain + trimmed }
|
if trimmed.hasPrefix("/") { return baseDomain + trimmed }
|
||||||
return baseDomain + "/" + trimmed
|
return baseDomain + "/" + trimmed
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -108,8 +108,15 @@ struct BusinessSelectionScreen: View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
LazyVStack(spacing: 12) {
|
LazyVStack(spacing: 12) {
|
||||||
ForEach(businesses) { emp in
|
ForEach(businesses) { emp in
|
||||||
businessCard(emp)
|
if emp.pendingTaskCount > 0 {
|
||||||
.onTapGesture { selectBusiness(emp) }
|
Button { selectBusiness(emp) } label: {
|
||||||
|
businessCard(emp)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
} else {
|
||||||
|
businessCard(emp)
|
||||||
|
.opacity(0.5)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
|
|
@ -120,67 +127,72 @@ struct BusinessSelectionScreen: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func businessCard(_ emp: Employment) -> some View {
|
private func businessCard(_ emp: Employment) -> some View {
|
||||||
VStack(spacing: 0) {
|
HStack(spacing: 14) {
|
||||||
// Header image with brand color background
|
// Initial letter
|
||||||
BusinessHeaderImage(businessId: emp.businessId)
|
Text(String(emp.businessName.prefix(1)).uppercased())
|
||||||
|
.font(.system(size: 32, weight: .bold))
|
||||||
|
.foregroundColor(.primary.opacity(0.7))
|
||||||
|
.frame(width: 50)
|
||||||
|
|
||||||
// Info bar
|
// Name + status
|
||||||
HStack(spacing: 12) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
Text(emp.businessName)
|
||||||
Text(emp.businessName)
|
.font(.body.weight(.semibold))
|
||||||
.font(.subheadline.weight(.semibold))
|
.foregroundColor(.primary)
|
||||||
.foregroundColor(.primary)
|
.lineLimit(1)
|
||||||
.lineLimit(1)
|
|
||||||
|
|
||||||
Text(emp.statusName)
|
Text(emp.statusName)
|
||||||
.font(.caption2)
|
.font(.caption.weight(.medium))
|
||||||
.fontWeight(.medium)
|
.foregroundColor(.white)
|
||||||
.foregroundColor(statusColor(emp.employeeStatusId))
|
.padding(.horizontal, 8)
|
||||||
}
|
.padding(.vertical, 2)
|
||||||
|
.background(statusColor(emp.employeeStatusId))
|
||||||
Spacer()
|
.clipShape(Capsule())
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
.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))
|
.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())
|
.contentShape(Rectangle())
|
||||||
}
|
}
|
||||||
|
|
||||||
private var myTasksFAB: some View {
|
private var myTasksFAB: some View {
|
||||||
Button { showingMyTasks = true } label: {
|
Button { showingMyTasks = true } label: {
|
||||||
Image(systemName: "checkmark.circle.fill")
|
HStack(spacing: 8) {
|
||||||
.font(.title3)
|
Image(systemName: "checkmark.square.fill")
|
||||||
.padding(12)
|
.font(.title3)
|
||||||
.background(Color.payfritGreen)
|
Text("My Tasks")
|
||||||
.foregroundColor(.white)
|
.font(.body.weight(.semibold))
|
||||||
.clipShape(Circle())
|
}
|
||||||
.shadow(radius: 4)
|
.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(.trailing, 16)
|
||||||
.padding(.bottom, 16)
|
.padding(.bottom, 16)
|
||||||
|
|
@ -294,9 +306,10 @@ struct BusinessHeaderImage: View {
|
||||||
@State private var isLoading = true
|
@State private var isLoading = true
|
||||||
|
|
||||||
private var imageURLs: [URL] {
|
private var imageURLs: [URL] {
|
||||||
[
|
let domain = IS_DEV ? "https://dev.payfrit.com" : "https://biz.payfrit.com"
|
||||||
"https://dev.payfrit.com/uploads/headers/\(businessId).png",
|
return [
|
||||||
"https://dev.payfrit.com/uploads/headers/\(businessId).jpg",
|
"\(domain)/uploads/headers/\(businessId).png",
|
||||||
|
"\(domain)/uploads/headers/\(businessId).jpg",
|
||||||
].compactMap { URL(string: $0) }
|
].compactMap { URL(string: $0) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ struct LoginScreen: View {
|
||||||
@State private var showPassword = false
|
@State private var showPassword = false
|
||||||
@State private var isLoading = false
|
@State private var isLoading = false
|
||||||
@State private var error: String?
|
@State private var error: String?
|
||||||
@State private var isDev = false
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
GeometryReader { geo in
|
GeometryReader { geo in
|
||||||
|
|
@ -25,7 +24,7 @@ struct LoginScreen: View {
|
||||||
Text("Sign in to view and claim tasks")
|
Text("Sign in to view and claim tasks")
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
|
|
||||||
if isDev {
|
if IS_DEV {
|
||||||
Text("DEV MODE — password: 123456")
|
Text("DEV MODE — password: 123456")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(.red)
|
.foregroundColor(.red)
|
||||||
|
|
@ -99,7 +98,6 @@ struct LoginScreen: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.background(Color(.systemGroupedBackground))
|
.background(Color(.systemGroupedBackground))
|
||||||
.task { isDev = await APIService.shared.isDev }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func login() {
|
private func login() {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import LocalAuthentication
|
||||||
struct RootView: View {
|
struct RootView: View {
|
||||||
@EnvironmentObject var appState: AppState
|
@EnvironmentObject var appState: AppState
|
||||||
@State private var isCheckingAuth = true
|
@State private var isCheckingAuth = true
|
||||||
@State private var isDev = false
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
Group {
|
||||||
|
|
@ -17,20 +16,7 @@ struct RootView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.animation(.easeInOut(duration: 0.3), value: appState.isAuthenticated)
|
.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 {
|
.task {
|
||||||
isDev = await APIService.shared.isDev
|
|
||||||
await checkAuthWithBiometrics()
|
await checkAuthWithBiometrics()
|
||||||
isCheckingAuth = false
|
isCheckingAuth = false
|
||||||
}
|
}
|
||||||
|
|
|
||||||
36
PayfritWorks/Widgets/DevRibbon.swift
Normal file
36
PayfritWorks/Widgets/DevRibbon.swift
Normal file
|
|
@ -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())
|
||||||
|
}
|
||||||
|
}
|
||||||
2
PayfritWorks/en.lproj/InfoPlist.strings
Normal file
2
PayfritWorks/en.lproj/InfoPlist.strings
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
CFBundleDisplayName = "Payfrit Works";
|
||||||
|
CFBundleName = "Payfrit Works";
|
||||||
Loading…
Add table
Reference in a new issue