Rebrand from Casera/MyCrib to honeyDue

Total rebrand across KMM project:
- Kotlin package: com.example.casera -> com.tt.honeyDue (dirs + declarations)
- Gradle: rootProject.name, namespace, applicationId
- Android: manifest, strings.xml (all languages), widget resources
- iOS: pbxproj bundle IDs, Info.plist, entitlements, xcconfig
- iOS directories: Casera/ -> HoneyDue/, CaseraTests/ -> HoneyDueTests/, etc.
- Swift source: all class/struct/enum renames
- Deep links: casera:// -> honeydue://, .casera -> .honeydue
- App icons replaced with honeyDue honeycomb icon
- Domains: casera.treytartt.com -> honeyDue.treytartt.com
- Bundle IDs: com.tt.casera -> com.tt.honeyDue
- Database table names preserved

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-03-07 06:33:57 -06:00
parent 9c574c4343
commit 1e2adf7660
450 changed files with 1730 additions and 1788 deletions

View File

@@ -0,0 +1,24 @@
package com.tt.honeyDue
import androidx.compose.ui.window.ComposeUIViewController
import com.tt.honeyDue.storage.TokenManager
import com.tt.honeyDue.storage.TokenStorage
import com.tt.honeyDue.storage.TaskCacheManager
import com.tt.honeyDue.storage.TaskCacheStorage
import com.tt.honeyDue.storage.ThemeStorage
import com.tt.honeyDue.storage.ThemeStorageManager
import com.tt.honeyDue.ui.theme.ThemeManager
fun MainViewController() = ComposeUIViewController {
// Initialize TokenStorage with iOS TokenManager
TokenStorage.initialize(TokenManager.getInstance())
// Initialize TaskCacheStorage for offline task caching
TaskCacheStorage.initialize(TaskCacheManager.getInstance())
// Initialize ThemeStorage and ThemeManager
ThemeStorage.initialize(ThemeStorageManager.getInstance())
ThemeManager.initialize()
App()
}

View File

@@ -0,0 +1,9 @@
package com.tt.honeyDue
import platform.UIKit.UIDevice
class IOSPlatform: Platform {
override val name: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion
}
actual fun getPlatform(): Platform = IOSPlatform()

View File

@@ -0,0 +1,32 @@
package com.tt.honeyDue.analytics
/**
* iOS implementation of PostHog Analytics.
* Note: The actual PostHog SDK calls are made from Swift code.
* This is a stub that does nothing - iOS uses the native Swift wrapper directly.
*/
actual object PostHogAnalytics {
actual fun initialize() {
// iOS initialization is done in Swift via PostHogAnalytics.swift
}
actual fun identify(userId: String, properties: Map<String, Any>?) {
// iOS uses Swift PostHogAnalytics.shared.identify() directly
}
actual fun capture(event: String, properties: Map<String, Any>?) {
// iOS uses Swift PostHogAnalytics.shared.capture() directly
}
actual fun screen(screenName: String, properties: Map<String, Any>?) {
// iOS uses Swift PostHogAnalytics.shared.screen() directly
}
actual fun reset() {
// iOS uses Swift PostHogAnalytics.shared.reset() directly
}
actual fun flush() {
// iOS uses Swift PostHogAnalytics.shared.flush() directly
}
}

View File

@@ -0,0 +1,42 @@
package com.tt.honeyDue.data
import platform.Foundation.NSUserDefaults
/**
* iOS implementation of PersistenceManager using NSUserDefaults.
*/
actual class PersistenceManager {
private val defaults = NSUserDefaults.standardUserDefaults
actual fun save(key: String, value: String) {
defaults.setObject(value, forKey = key)
defaults.synchronize()
}
actual fun load(key: String): String? {
return defaults.stringForKey(key)
}
actual fun remove(key: String) {
defaults.removeObjectForKey(key)
defaults.synchronize()
}
actual fun clear() {
// Get all keys with our prefix and remove them
val dict = defaults.dictionaryRepresentation()
dict.keys.forEach { key ->
val keyStr = key as? String ?: return@forEach
if (keyStr.startsWith("dm_")) {
defaults.removeObjectForKey(keyStr)
}
}
defaults.synchronize()
}
companion object {
private val instance by lazy { PersistenceManager() }
fun getInstance(): PersistenceManager = instance
}
}

View File

@@ -0,0 +1,56 @@
package com.tt.honeyDue.network
import io.ktor.client.*
import io.ktor.client.engine.darwin.*
import io.ktor.client.plugins.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.plugins.logging.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json
import platform.Foundation.NSLocale
import platform.Foundation.NSTimeZone
import platform.Foundation.localTimeZone
import platform.Foundation.preferredLanguages
actual fun getLocalhostAddress(): String = "127.0.0.1"
actual fun getDeviceLanguage(): String {
val preferredLanguages = NSLocale.preferredLanguages
val firstLanguage = preferredLanguages.firstOrNull() as? String
// Extract just the language code (e.g., "en" from "en-US")
return firstLanguage?.split("-")?.firstOrNull() ?: "en"
}
actual fun getDeviceTimezone(): String {
return NSTimeZone.localTimeZone.name
}
actual fun createHttpClient(): HttpClient {
return HttpClient(Darwin) {
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
isLenient = true
prettyPrint = true
})
}
install(Logging) {
logger = Logger.DEFAULT
// Only log full request/response bodies in local dev to avoid
// leaking auth tokens and PII in production logs.
level = if (ApiConfig.CURRENT_ENV == ApiConfig.Environment.LOCAL) LogLevel.ALL else LogLevel.INFO
}
install(DefaultRequest) {
headers.append("Accept-Language", getDeviceLanguage())
headers.append("X-Timezone", getDeviceTimezone())
}
engine {
configureRequest {
setAllowsCellularAccess(true)
}
}
}
}

View File

@@ -0,0 +1,17 @@
package com.tt.honeyDue.platform
import androidx.compose.runtime.Composable
import com.tt.honeyDue.models.Contractor
/**
* iOS implementation is a no-op - import is handled in Swift layer via ContractorSharingManager.swift.
* The iOS iOSApp.swift handles file imports directly.
*/
@Composable
actual fun ContractorImportHandler(
pendingContractorImportUri: Any?,
onClearContractorImport: () -> Unit,
onImportSuccess: (Contractor) -> Unit
) {
// No-op on iOS - import handled in Swift layer
}

View File

@@ -0,0 +1,56 @@
package com.tt.honeyDue.platform
import androidx.compose.runtime.Composable
import androidx.compose.ui.interop.LocalUIViewController
import com.tt.honeyDue.data.DataManager
import com.tt.honeyDue.models.honeyDueShareCodec
import com.tt.honeyDue.models.Contractor
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.addressOf
import kotlinx.cinterop.usePinned
import platform.Foundation.*
import platform.UIKit.UIActivityViewController
import platform.UIKit.UIViewController
@Composable
actual fun rememberShareContractor(): (Contractor) -> Unit {
val viewController = LocalUIViewController.current
return share@{ contractor: Contractor ->
val currentUsername = DataManager.currentUser.value?.username ?: "Unknown"
val jsonContent = honeyDueShareCodec.encodeContractorPackage(contractor, currentUsername)
val fileUrl = writeShareFile(jsonContent, contractor.name) ?: return@share
presentShareSheet(viewController, fileUrl)
}
}
@OptIn(ExperimentalForeignApi::class)
private fun writeShareFile(jsonContent: String, displayName: String): NSURL? {
val fileName = honeyDueShareCodec.safeShareFileName(displayName)
val filePath = NSTemporaryDirectory().plus(fileName)
val bytes = jsonContent.encodeToByteArray()
val data = bytes.usePinned { pinned ->
NSData.create(bytes = pinned.addressOf(0), length = bytes.size.toULong())
}
val didCreate = NSFileManager.defaultManager.createFileAtPath(
path = filePath,
contents = data,
attributes = null
)
if (!didCreate) return null
return NSURL.fileURLWithPath(filePath)
}
private fun presentShareSheet(viewController: UIViewController, fileUrl: NSURL) {
val activityViewController = UIActivityViewController(
activityItems = listOf(fileUrl),
applicationActivities = null
)
viewController.presentViewController(
viewControllerToPresent = activityViewController,
animated = true,
completion = null
)
}

View File

@@ -0,0 +1,21 @@
package com.tt.honeyDue.platform
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
/**
* iOS implementation - no-op since iOS haptics are handled by SwiftUI.
* This is only used when running the shared Compose code on iOS
* (which isn't the primary iOS UI).
*/
class IOSHapticFeedbackPerformer : HapticFeedbackPerformer {
override fun perform(type: HapticFeedbackType) {
// iOS haptic feedback is handled natively in SwiftUI views
// This is a no-op for the Compose layer on iOS
}
}
@Composable
actual fun rememberHapticFeedback(): HapticFeedbackPerformer {
return remember { IOSHapticFeedbackPerformer() }
}

View File

@@ -0,0 +1,19 @@
package com.tt.honeyDue.platform
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.toComposeImageBitmap
import org.jetbrains.skia.Image
@Composable
actual fun rememberImageBitmap(imageData: ImageData): ImageBitmap? {
return remember(imageData) {
try {
val skiaImage = Image.makeFromEncoded(imageData.bytes)
skiaImage.toComposeImageBitmap()
} catch (e: Exception) {
null
}
}
}

View File

@@ -0,0 +1,154 @@
package com.tt.honeyDue.platform
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.interop.LocalUIViewController
import kotlinx.cinterop.*
import platform.Foundation.*
import platform.PhotosUI.*
import platform.UIKit.*
import platform.darwin.NSObject
import platform.posix.memcpy
@OptIn(ExperimentalForeignApi::class)
@Composable
actual fun rememberImagePicker(
onImagesPicked: (List<ImageData>) -> Unit
): () -> Unit {
val viewController = LocalUIViewController.current
val pickerDelegate = remember {
object : NSObject(), PHPickerViewControllerDelegateProtocol {
override fun picker(
picker: PHPickerViewController,
didFinishPicking: List<*>
) {
picker.dismissViewControllerAnimated(true, null)
val results = didFinishPicking as List<PHPickerResult>
if (results.isEmpty()) {
return
}
val images = mutableListOf<ImageData>()
var processedCount = 0
results.forEach { result ->
val itemProvider = result.itemProvider
// Check if the item has an image using UTType
if (itemProvider.hasItemConformingToTypeIdentifier("public.image")) {
itemProvider.loadFileRepresentationForTypeIdentifier(
typeIdentifier = "public.image",
completionHandler = { url, error ->
if (error == null && url != null) {
// Read the image data from the file URL
val imageData = NSData.dataWithContentsOfURL(url)
if (imageData != null) {
// Convert to UIImage and then to JPEG for consistent format
val image = UIImage.imageWithData(imageData)
if (image != null) {
val jpegData = UIImageJPEGRepresentation(image, 0.8)
if (jpegData != null) {
val bytes = jpegData.toByteArray()
val fileName = "image_${NSDate().timeIntervalSince1970.toLong()}_${processedCount}.jpg"
synchronized(images) {
images.add(ImageData(bytes, fileName))
}
}
}
}
}
processedCount++
// When all images are processed, call the callback
if (processedCount == results.size) {
if (images.isNotEmpty()) {
onImagesPicked(images.toList())
}
}
}
)
} else {
processedCount++
}
}
}
}
}
return {
val config = PHPickerConfiguration().apply {
setSelectionLimit(5)
setFilter(PHPickerFilter.imagesFilter())
}
val picker = PHPickerViewController(config).apply {
setDelegate(pickerDelegate)
}
viewController.presentViewController(picker, animated = true, completion = null)
}
}
@OptIn(ExperimentalForeignApi::class)
private fun NSData.toByteArray(): ByteArray {
return ByteArray(length.toInt()).apply {
usePinned {
memcpy(it.addressOf(0), bytes, length)
}
}
}
@OptIn(ExperimentalForeignApi::class)
@Composable
actual fun rememberCameraPicker(
onImageCaptured: (ImageData) -> Unit
): () -> Unit {
val viewController = LocalUIViewController.current
val cameraDelegate = remember {
object : NSObject(), UIImagePickerControllerDelegateProtocol, UINavigationControllerDelegateProtocol {
override fun imagePickerController(
picker: UIImagePickerController,
didFinishPickingMediaWithInfo: Map<Any?, *>
) {
picker.dismissViewControllerAnimated(true, null)
val image = didFinishPickingMediaWithInfo[UIImagePickerControllerOriginalImage] as? UIImage
if (image != null) {
// Convert to JPEG
val jpegData = UIImageJPEGRepresentation(image, 0.8)
if (jpegData != null) {
val bytes = jpegData.toByteArray()
val fileName = "camera_${NSDate().timeIntervalSince1970.toLong()}.jpg"
onImageCaptured(ImageData(bytes, fileName))
}
}
}
override fun imagePickerControllerDidCancel(picker: UIImagePickerController) {
picker.dismissViewControllerAnimated(true, null)
}
}
}
return {
// Check if camera is available
if (UIImagePickerController.isSourceTypeAvailable(UIImagePickerControllerSourceType.UIImagePickerControllerSourceTypeCamera)) {
val picker = UIImagePickerController().apply {
setSourceType(UIImagePickerControllerSourceType.UIImagePickerControllerSourceTypeCamera)
setDelegate(cameraDelegate)
setCameraCaptureMode(UIImagePickerControllerCameraCaptureMode.UIImagePickerControllerCameraCaptureModePhoto)
}
viewController.presentViewController(picker, animated = true, completion = null)
}
}
}
@Suppress("UNCHECKED_CAST")
private fun <T : Any> synchronized(lock: Any, block: () -> T): T {
return block()
}

View File

@@ -0,0 +1,33 @@
package com.tt.honeyDue.platform
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import com.tt.honeyDue.network.APILayer
import com.tt.honeyDue.ui.subscription.UpgradeScreen
import kotlinx.coroutines.launch
/**
* iOS: Purchase flow is handled in Swift via StoreKitManager.
* Restore calls backend to refresh subscription status.
*/
@Composable
actual fun PlatformUpgradeScreen(
onNavigateBack: () -> Unit,
onSubscriptionChanged: () -> Unit
) {
val scope = rememberCoroutineScope()
UpgradeScreen(
onNavigateBack = onNavigateBack,
onPurchase = { _ ->
// iOS purchase flow is handled by StoreKitManager in Swift layer
onNavigateBack()
},
onRestorePurchases = {
scope.launch {
APILayer.getSubscriptionStatus(forceRefresh = true)
onSubscriptionChanged()
}
}
)
}

View File

@@ -0,0 +1,17 @@
package com.tt.honeyDue.platform
import androidx.compose.runtime.Composable
import com.tt.honeyDue.models.JoinResidenceResponse
/**
* iOS implementation is a no-op - import is handled in Swift layer via ResidenceSharingManager.swift.
* The iOS iOSApp.swift handles file imports directly.
*/
@Composable
actual fun ResidenceImportHandler(
pendingResidenceImportUri: Any?,
onClearResidenceImport: () -> Unit,
onImportSuccess: (JoinResidenceResponse) -> Unit
) {
// No-op on iOS - import handled in Swift layer
}

View File

@@ -0,0 +1,89 @@
package com.tt.honeyDue.platform
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.interop.LocalUIViewController
import com.tt.honeyDue.models.honeyDueShareCodec
import com.tt.honeyDue.models.Residence
import com.tt.honeyDue.network.APILayer
import com.tt.honeyDue.network.ApiResult
import kotlinx.coroutines.launch
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.addressOf
import kotlinx.cinterop.usePinned
import platform.Foundation.*
import platform.UIKit.UIActivityViewController
import platform.UIKit.UIViewController
@Composable
actual fun rememberShareResidence(): Pair<ResidenceSharingState, (Residence) -> Unit> {
val viewController = LocalUIViewController.current
val scope = rememberCoroutineScope()
var state by remember { mutableStateOf(ResidenceSharingState()) }
val shareFunction: (Residence) -> Unit = share@{ residence ->
scope.launch {
state = ResidenceSharingState(isLoading = true)
when (val result = APILayer.generateSharePackage(residence.id)) {
is ApiResult.Success -> {
val jsonContent = honeyDueShareCodec.encodeSharedResidence(result.data)
val fileUrl = writeShareFile(jsonContent, residence.name)
if (fileUrl == null) {
state = ResidenceSharingState(isLoading = false, error = "Failed to create share package")
return@launch
}
state = ResidenceSharingState(isLoading = false)
presentShareSheet(viewController, fileUrl)
}
is ApiResult.Error -> {
state = ResidenceSharingState(
isLoading = false,
error = result.message.ifBlank { "Failed to generate share package" }
)
}
else -> {
state = ResidenceSharingState(isLoading = false, error = "Failed to generate share package")
}
}
}
}
return Pair(state, shareFunction)
}
@OptIn(ExperimentalForeignApi::class)
private fun writeShareFile(jsonContent: String, displayName: String): NSURL? {
val fileName = honeyDueShareCodec.safeShareFileName(displayName)
val filePath = NSTemporaryDirectory().plus(fileName)
val bytes = jsonContent.encodeToByteArray()
val data = bytes.usePinned { pinned ->
NSData.create(bytes = pinned.addressOf(0), length = bytes.size.toULong())
}
val didCreate = NSFileManager.defaultManager.createFileAtPath(
path = filePath,
contents = data,
attributes = null
)
if (!didCreate) return null
return NSURL.fileURLWithPath(filePath)
}
private fun presentShareSheet(viewController: UIViewController, fileUrl: NSURL) {
val activityViewController = UIActivityViewController(
activityItems = listOf(fileUrl),
applicationActivities = null
)
viewController.presentViewController(
viewControllerToPresent = activityViewController,
animated = true,
completion = null
)
}

View File

@@ -0,0 +1,70 @@
package com.tt.honeyDue.storage
import platform.Foundation.NSUserDefaults
import kotlin.concurrent.Volatile
/**
* iOS implementation of TaskCacheManager using NSUserDefaults.
* Note: iOS widget dirty flag is handled by native Swift WidgetDataManager
* using App Groups shared storage, but we provide these methods for KMM compatibility.
*/
actual class TaskCacheManager {
private val userDefaults = NSUserDefaults.standardUserDefaults
actual fun saveTasks(tasksJson: String) {
userDefaults.setObject(tasksJson, KEY_TASKS)
userDefaults.synchronize()
}
actual fun getTasks(): String? {
return userDefaults.stringForKey(KEY_TASKS)
}
actual fun clearTasks() {
userDefaults.removeObjectForKey(KEY_TASKS)
userDefaults.synchronize()
}
/**
* Check if tasks need refresh due to widget interactions.
* Note: iOS primarily uses native Swift WidgetDataManager for this.
*/
actual fun areTasksDirty(): Boolean {
return userDefaults.boolForKey(KEY_DIRTY_FLAG)
}
/**
* Mark tasks as dirty (needs refresh).
*/
actual fun markTasksDirty() {
userDefaults.setBool(true, KEY_DIRTY_FLAG)
userDefaults.synchronize()
}
/**
* Clear the dirty flag after tasks have been refreshed.
*/
actual fun clearDirtyFlag() {
userDefaults.setBool(false, KEY_DIRTY_FLAG)
userDefaults.synchronize()
}
companion object {
private const val KEY_TASKS = "cached_tasks"
private const val KEY_DIRTY_FLAG = "tasks_dirty"
@Volatile
private var instance: TaskCacheManager? = null
fun getInstance(): TaskCacheManager {
return instance ?: synchronized(this) {
instance ?: TaskCacheManager().also { instance = it }
}
}
}
}
// Helper function for synchronization on iOS
private fun <T> synchronized(lock: Any, block: () -> T): T {
return block()
}

View File

@@ -0,0 +1,5 @@
package com.tt.honeyDue.storage
internal actual fun getPlatformTaskCacheManager(): TaskCacheManager? {
return TaskCacheManager.getInstance()
}

View File

@@ -0,0 +1,32 @@
package com.tt.honeyDue.storage
import platform.Foundation.NSUserDefaults
/**
* iOS implementation of theme storage using NSUserDefaults.
*/
actual class ThemeStorageManager {
private val defaults = NSUserDefaults.standardUserDefaults
actual fun saveThemeId(themeId: String) {
defaults.setObject(themeId, forKey = KEY_THEME_ID)
defaults.synchronize()
}
actual fun getThemeId(): String? {
return defaults.stringForKey(KEY_THEME_ID)
}
actual fun clearThemeId() {
defaults.removeObjectForKey(KEY_THEME_ID)
defaults.synchronize()
}
companion object {
private const val KEY_THEME_ID = "theme_id"
private val instance by lazy { ThemeStorageManager() }
fun getInstance(): ThemeStorageManager = instance
}
}

View File

@@ -0,0 +1,96 @@
package com.tt.honeyDue.storage
import kotlin.concurrent.Volatile
/**
* Protocol for iOS Keychain operations. Implemented in Swift (KeychainHelper)
* and injected before DataManager initialization.
*
* Kotlin/Native cannot directly use the Security framework (SecItem* APIs)
* because CFStringRef keys like kSecClass don't bridge to NSCopying.
*/
interface KeychainDelegate {
fun save(key: String, value: String): Boolean
fun get(key: String): String?
fun delete(key: String): Boolean
}
/**
* iOS implementation of TokenManager.
*
* Uses iOS Keychain via [KeychainDelegate] for secure token storage.
* If delegate is missing, operations fail closed (no insecure fallback).
*
* On first read, migrates any existing NSUserDefaults token to Keychain.
*/
actual class TokenManager {
private val prefs = platform.Foundation.NSUserDefaults.standardUserDefaults
actual fun saveToken(token: String) {
val delegate = keychainDelegate
if (delegate != null) {
delegate.save(TOKEN_KEY, token)
// Clean up old NSUserDefaults entry if it exists
prefs.removeObjectForKey(TOKEN_KEY)
prefs.synchronize()
} else {
// Fail closed: never store auth tokens in insecure storage.
println("TokenManager: Keychain delegate not set, refusing to save token insecurely")
}
}
actual fun getToken(): String? {
val delegate = keychainDelegate
// Try Keychain first
if (delegate != null) {
val keychainToken = delegate.get(TOKEN_KEY)
if (keychainToken != null) return keychainToken
// Check NSUserDefaults for migration
val oldToken = prefs.stringForKey(TOKEN_KEY)
if (oldToken != null) {
// Migrate to Keychain
delegate.save(TOKEN_KEY, oldToken)
prefs.removeObjectForKey(TOKEN_KEY)
prefs.synchronize()
return oldToken
}
return null
}
// Fail closed: no insecure fallback reads.
println("TokenManager: Keychain delegate not set, refusing insecure token read")
return null
}
actual fun clearToken() {
keychainDelegate?.delete(TOKEN_KEY)
prefs.removeObjectForKey(TOKEN_KEY)
prefs.synchronize()
}
companion object {
private const val TOKEN_KEY = "auth_token"
/**
* Set from Swift in iOSApp.init() BEFORE DataManager.initialize().
* This enables Keychain storage for auth tokens.
*/
var keychainDelegate: KeychainDelegate? = null
@Volatile
private var instance: TokenManager? = null
fun getInstance(): TokenManager {
return instance ?: synchronized(this) {
instance ?: TokenManager().also { instance = it }
}
}
}
}
// Helper function for synchronization on iOS
private fun <T> synchronized(lock: Any, block: () -> T): T {
return block()
}

View File

@@ -0,0 +1,5 @@
package com.tt.honeyDue.storage
internal actual fun getPlatformTokenManager(): TokenManager? {
return TokenManager.getInstance()
}

View File

@@ -0,0 +1,16 @@
package com.tt.honeyDue.ui.components.auth
import androidx.compose.runtime.Composable
/**
* iOS stub - Google Sign In is not available on iOS (use Apple Sign In instead)
*/
@Composable
actual fun GoogleSignInButton(
onSignInStarted: () -> Unit,
onSignInSuccess: (idToken: String) -> Unit,
onSignInError: (message: String) -> Unit,
enabled: Boolean
) {
// No-op on iOS - Apple Sign In is used instead
}

View File

@@ -0,0 +1,69 @@
package com.tt.honeyDue.util
import com.tt.honeyDue.platform.ImageData
import kotlinx.cinterop.*
import platform.Foundation.*
import platform.UIKit.*
import platform.posix.memcpy
/**
* iOS implementation of image compression
* Compresses images to JPEG format and ensures they don't exceed MAX_IMAGE_SIZE_BYTES
*/
@OptIn(ExperimentalForeignApi::class)
actual object ImageCompressor {
/**
* Compress an ImageData to JPEG format with size limit
* @param imageData The image to compress
* @return Compressed image data as ByteArray
*/
actual fun compressImage(imageData: ImageData): ByteArray {
// Convert ByteArray to NSData
val nsData = imageData.bytes.usePinned { pinned ->
NSData.create(
bytes = pinned.addressOf(0),
length = imageData.bytes.size.toULong()
)
}
// Create UIImage from data
val image = UIImage.imageWithData(nsData) ?: return imageData.bytes
// Compress with iterative quality reduction
return compressImageToTarget(image, ImageConfig.MAX_IMAGE_SIZE_BYTES.toLong())
}
/**
* Compress a UIImage to target size
*/
private fun compressImageToTarget(image: UIImage, targetSizeBytes: Long): ByteArray {
var quality = ImageConfig.INITIAL_JPEG_QUALITY / 100.0
var compressedData: NSData? = null
while (quality >= ImageConfig.MIN_JPEG_QUALITY / 100.0) {
compressedData = UIImageJPEGRepresentation(image, quality)
if (compressedData != null && compressedData.length.toLong() <= targetSizeBytes) {
break
}
quality -= 0.05 // Reduce quality by 5%
}
// Convert NSData to ByteArray
return compressedData?.toByteArray() ?: image.let {
UIImageJPEGRepresentation(it, ImageConfig.MIN_JPEG_QUALITY / 100.0)?.toByteArray() ?: ByteArray(0)
}
}
/**
* Convert NSData to ByteArray
*/
private fun NSData.toByteArray(): ByteArray {
return ByteArray(this.length.toInt()).apply {
usePinned {
memcpy(it.addressOf(0), this@toByteArray.bytes, this@toByteArray.length)
}
}
}
}