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