SubmittedToTestflight

This commit is contained in:
2026-03-20 03:39:17 -05:00
parent 813031c823
commit 2ee3f37576
5 changed files with 60 additions and 56 deletions

View File

@@ -433,6 +433,7 @@
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = LabWise/Info.plist; INFOPLIST_FILE = LabWise/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = LabWise;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
INFOPLIST_KEY_NSCameraUsageDescription = "Camera Access needed for scanning chemical labels for adding to inventory."; INFOPLIST_KEY_NSCameraUsageDescription = "Camera Access needed for scanning chemical labels for adding to inventory.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
@@ -446,7 +447,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.adipu.LabWise; PRODUCT_BUNDLE_IDENTIFIER = com.adipu.LabWiseApp;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = YES; STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_APPROACHABLE_CONCURRENCY = YES;
@@ -469,6 +470,7 @@
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = LabWise/Info.plist; INFOPLIST_FILE = LabWise/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = LabWise;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
INFOPLIST_KEY_NSCameraUsageDescription = "Camera Access needed for scanning chemical labels for adding to inventory."; INFOPLIST_KEY_NSCameraUsageDescription = "Camera Access needed for scanning chemical labels for adding to inventory.";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
@@ -482,7 +484,7 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.0; MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.adipu.LabWise; PRODUCT_BUNDLE_IDENTIFIER = com.adipu.LabWiseApp;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = YES; STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_APPROACHABLE_CONCURRENCY = YES;

View File

@@ -1,6 +1,7 @@
{ {
"images" : [ "images" : [
{ {
"filename" : "LabWiseLogo1.png",
"idiom" : "universal", "idiom" : "universal",
"platform" : "ios", "platform" : "ios",
"size" : "1024x1024" "size" : "1024x1024"

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 KiB

View File

@@ -11,27 +11,17 @@ public struct SignInBody: Encodable, Sendable {
} }
} }
/// Better Auth wraps the sign-in response as { data: { user, session } } /// Server returns { token, user } directly (or { error: { message } } on failure)
public struct SignInResponse: Decodable, Sendable { public struct SignInResponse: Decodable, Sendable {
public let data: SignInData? public let token: String?
public let user: AuthUser?
public let error: SignInError? public let error: SignInError?
public struct SignInData: Decodable, Sendable {
public let user: AuthUser
public let session: AuthSession
}
public struct SignInError: Decodable, Sendable { public struct SignInError: Decodable, Sendable {
public let message: String? public let message: String?
} }
} }
public struct AuthSession: Decodable, Sendable {
public let id: String
public let token: String
public let userId: String
public let expiresAt: String
}
public struct AuthUser: Decodable, Sendable { public struct AuthUser: Decodable, Sendable {
public let id: String public let id: String
@@ -41,10 +31,9 @@ public struct AuthUser: Decodable, Sendable {
public let image: String? public let image: String?
} }
/// Thin wrapper around the /api/auth/session endpoint response. /// Thin wrapper around the /api/auth/get-session endpoint response.
private struct SessionResponse: Decodable, Sendable { private struct SessionResponse: Decodable, Sendable {
let user: AuthUser? let user: AuthUser?
let session: AuthSession?
} }
public final class AuthClient: Sendable { public final class AuthClient: Sendable {
@@ -64,6 +53,7 @@ public final class AuthClient: Sendable {
var req = URLRequest(url: URL(string: "\(baseURL)/api/auth/sign-in/email")!) var req = URLRequest(url: URL(string: "\(baseURL)/api/auth/sign-in/email")!)
req.httpMethod = "POST" req.httpMethod = "POST"
req.setValue("application/json", forHTTPHeaderField: "Content-Type") req.setValue("application/json", forHTTPHeaderField: "Content-Type")
req.setValue(baseURL, forHTTPHeaderField: "Origin")
req.httpBody = encoded req.httpBody = encoded
let session = sharedURLSession() let session = sharedURLSession()
@@ -73,19 +63,22 @@ public final class AuthClient: Sendable {
throw APIError.networkError(URLError(.badServerResponse)) throw APIError.networkError(URLError(.badServerResponse))
} }
// Better Auth may return 200 with an error body, or a non-200 status guard http.statusCode == 200 else {
throw APIError.httpError(statusCode: http.statusCode, data: data)
}
let signInResponse = try JSONDecoder.api.decode(SignInResponse.self, from: data) let signInResponse = try JSONDecoder.api.decode(SignInResponse.self, from: data)
if let errMsg = signInResponse.error?.message { if let errMsg = signInResponse.error?.message {
throw APIError.httpError(statusCode: http.statusCode, data: Data(errMsg.utf8)) throw APIError.httpError(statusCode: http.statusCode, data: Data(errMsg.utf8))
} }
guard http.statusCode == 200 else { guard let user = signInResponse.user else {
throw APIError.httpError(statusCode: http.statusCode, data: data)
}
guard let user = signInResponse.data?.user else {
throw APIError.decodingError(DecodingError.dataCorrupted( throw APIError.decodingError(DecodingError.dataCorrupted(
.init(codingPath: [], debugDescription: "Missing user in sign-in response") .init(codingPath: [], debugDescription: "Missing user in sign-in response")
)) ))
} }
if let token = signInResponse.token {
injectSessionCookie(token: token)
}
return user return user
} }
@@ -194,6 +187,7 @@ public final class AuthClient: Sendable {
public func fetchCurrentUser() async throws -> AuthUser { public func fetchCurrentUser() async throws -> AuthUser {
var req = URLRequest(url: URL(string: "\(baseURL)/api/auth/get-session")!) var req = URLRequest(url: URL(string: "\(baseURL)/api/auth/get-session")!)
req.httpMethod = "GET" req.httpMethod = "GET"
req.setValue(baseURL, forHTTPHeaderField: "Origin")
let session = sharedURLSession() let session = sharedURLSession()
let (data, response) = try await session.data(for: req) let (data, response) = try await session.data(for: req)
guard let http = response as? HTTPURLResponse, http.statusCode == 200 else { guard let http = response as? HTTPURLResponse, http.statusCode == 200 else {

View File

@@ -29,15 +29,6 @@ public struct Chemical: Identifiable, Sendable, Encodable {
public var createdAt: String? public var createdAt: String?
public var updatedAt: String? public var updatedAt: String?
enum CodingKeys: String, CodingKey {
case id, piFirstName, physicalState, chemicalName, bldgCode, lab
case storageLocation, storageDevice, numberOfContainers, amountPerContainer
case unitOfMeasure, casNumber, chemicalFormula, molecularWeight, vendor
case catalogNumber, lotNumber, expirationDate, concentration, percentageFull
case comments, barcode, contact, scannedImage, needsManualEntry
case createdAt, updatedAt
}
public init( public init(
id: String = "", id: String = "",
piFirstName: String = "", piFirstName: String = "",
@@ -100,34 +91,33 @@ public struct Chemical: Identifiable, Sendable, Encodable {
extension Chemical: Decodable { extension Chemical: Decodable {
public init(from decoder: Decoder) throws { public init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self) let c = try decoder.container(keyedBy: CodingKeys.self)
id = try c.decode(String.self, forKey: .id) id = try c.decode(String.self, forKey: .id)
piFirstName = try c.decode(String.self, forKey: .piFirstName) piFirstName = try c.decode(String.self, forKey: .piFirstName)
physicalState = try c.decode(String.self, forKey: .physicalState) physicalState = try c.decode(String.self, forKey: .physicalState)
chemicalName = try c.decode(String.self, forKey: .chemicalName) chemicalName = try c.decode(String.self, forKey: .chemicalName)
bldgCode = try c.decode(String.self, forKey: .bldgCode) bldgCode = try c.decode(String.self, forKey: .bldgCode)
lab = try c.decode(String.self, forKey: .lab) lab = try c.decode(String.self, forKey: .lab)
storageLocation = try c.decode(String.self, forKey: .storageLocation) storageLocation = try c.decode(String.self, forKey: .storageLocation)
storageDevice = try c.decode(String.self, forKey: .storageDevice) storageDevice = try c.decode(String.self, forKey: .storageDevice)
numberOfContainers = try c.decode(String.self, forKey: .numberOfContainers) numberOfContainers = try c.decode(String.self, forKey: .numberOfContainers)
amountPerContainer = try c.decode(String.self, forKey: .amountPerContainer) amountPerContainer = try c.decode(String.self, forKey: .amountPerContainer)
unitOfMeasure = try c.decode(String.self, forKey: .unitOfMeasure) unitOfMeasure = try c.decode(String.self, forKey: .unitOfMeasure)
casNumber = try c.decode(String.self, forKey: .casNumber) casNumber = try c.decode(String.self, forKey: .casNumber)
chemicalFormula = try c.decodeIfPresent(String.self, forKey: .chemicalFormula) chemicalFormula = try c.decodeIfPresent(String.self, forKey: .chemicalFormula)
molecularWeight = try c.decodeIfPresent(String.self, forKey: .molecularWeight) molecularWeight = try c.decodeIfPresent(String.self, forKey: .molecularWeight)
vendor = try c.decodeIfPresent(String.self, forKey: .vendor) vendor = try c.decodeIfPresent(String.self, forKey: .vendor)
catalogNumber = try c.decodeIfPresent(String.self, forKey: .catalogNumber) catalogNumber = try c.decodeIfPresent(String.self, forKey: .catalogNumber)
lotNumber = try c.decodeIfPresent(String.self, forKey: .lotNumber) lotNumber = try c.decodeIfPresent(String.self, forKey: .lotNumber)
expirationDate = try c.decodeIfPresent(String.self, forKey: .expirationDate) concentration = try c.decodeIfPresent(String.self, forKey: .concentration)
concentration = try c.decodeIfPresent(String.self, forKey: .concentration) comments = try c.decodeIfPresent(String.self, forKey: .comments)
comments = try c.decodeIfPresent(String.self, forKey: .comments) barcode = try c.decodeIfPresent(String.self, forKey: .barcode)
barcode = try c.decodeIfPresent(String.self, forKey: .barcode) contact = try c.decodeIfPresent(String.self, forKey: .contact)
contact = try c.decodeIfPresent(String.self, forKey: .contact) scannedImage = try c.decodeIfPresent(String.self, forKey: .scannedImage)
scannedImage = try c.decodeIfPresent(String.self, forKey: .scannedImage) needsManualEntry = try c.decodeIfPresent([String].self, forKey: .needsManualEntry)
needsManualEntry = try c.decodeIfPresent([String].self, forKey: .needsManualEntry) createdAt = try c.decodeIfPresent(String.self, forKey: .createdAt)
createdAt = try c.decodeIfPresent(String.self, forKey: .createdAt) updatedAt = try c.decodeIfPresent(String.self, forKey: .updatedAt)
updatedAt = try c.decodeIfPresent(String.self, forKey: .updatedAt)
// percentageFull arrives as a number OR a numeric string ("12.00") depending on the query path // percentageFull: GET returns Float (after ::float cast), POST/PATCH RETURNING still returns String
if let d = try? c.decodeIfPresent(Double.self, forKey: .percentageFull) { if let d = try? c.decodeIfPresent(Double.self, forKey: .percentageFull) {
percentageFull = d percentageFull = d
} else if let s = try? c.decodeIfPresent(String.self, forKey: .percentageFull) { } else if let s = try? c.decodeIfPresent(String.self, forKey: .percentageFull) {
@@ -135,6 +125,23 @@ extension Chemical: Decodable {
} else { } else {
percentageFull = nil percentageFull = nil
} }
// expirationDate: GET returns "YYYY-MM-DD", POST/PATCH RETURNING returns full ISO timestamp
// Normalise to "YYYY-MM-DD" in all cases
if let raw = try? c.decodeIfPresent(String.self, forKey: .expirationDate) {
expirationDate = String(raw.prefix(10))
} else {
expirationDate = nil
}
}
enum CodingKeys: String, CodingKey {
case id, piFirstName, physicalState, chemicalName, bldgCode, lab
case storageLocation, storageDevice, numberOfContainers, amountPerContainer
case unitOfMeasure, casNumber, chemicalFormula, molecularWeight, vendor
case catalogNumber, lotNumber, expirationDate, concentration, percentageFull
case comments, barcode, contact, scannedImage, needsManualEntry
case createdAt, updatedAt
} }
} }