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:
John Pinkyfloyd 2026-02-04 22:07:03 -08:00
parent 75c0698ec4
commit 7dfe8f593e
10 changed files with 162 additions and 95 deletions

View file

@ -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

View file

@ -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>

View file

@ -8,6 +8,7 @@ struct PayfritWorksApp: App {
WindowGroup { WindowGroup {
RootView() RootView()
.environmentObject(appState) .environmentObject(appState)
.devRibbon()
} }
} }
} }

View file

@ -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
} }

View file

@ -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) }
} }

View file

@ -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() {

View file

@ -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
} }

View 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())
}
}

View file

@ -0,0 +1,2 @@
CFBundleDisplayName = "Payfrit Works";
CFBundleName = "Payfrit Works";