├── scripts ├── tools │ └── gentool.jar └── gen.sh ├── examples ├── exampleIOS │ ├── Configuration │ │ └── Config.xcconfig │ ├── iosApp │ │ ├── Assets.xcassets │ │ │ ├── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── app-icon-1024.png │ │ │ │ └── Contents.json │ │ │ └── AccentColor.colorset │ │ │ │ └── Contents.json │ │ ├── Preview Content │ │ │ └── Preview Assets.xcassets │ │ │ │ └── Contents.json │ │ ├── iOSApp.swift │ │ ├── ContentView.swift │ │ └── Info.plist │ └── iosApp.xcodeproj │ │ ├── project.xcworkspace │ │ ├── xcuserdata │ │ │ └── sayem.xcuserdatad │ │ │ │ └── UserInterfaceState.xcuserstate │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcuserdata │ │ └── sayem.xcuserdatad │ │ └── xcschemes │ │ └── xcschememanagement.plist ├── exampleApp │ ├── src │ │ └── androidMain │ │ │ ├── res │ │ │ ├── values │ │ │ │ ├── strings.xml │ │ │ │ ├── ic_launcher_background.xml │ │ │ │ └── creds.xml │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ ├── ic_launcher_round.webp │ │ │ │ └── ic_launcher_foreground.webp │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ ├── ic_launcher_round.webp │ │ │ │ └── ic_launcher_foreground.webp │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ ├── ic_launcher_round.webp │ │ │ │ └── ic_launcher_foreground.webp │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ ├── ic_launcher_round.webp │ │ │ │ └── ic_launcher_foreground.webp │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ ├── ic_launcher_round.webp │ │ │ │ └── ic_launcher_foreground.webp │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ └── drawable-v24 │ │ │ │ └── ic_launcher_foreground.xml │ │ │ ├── ic_launcher-playstore.png │ │ │ └── AndroidManifest.xml │ ├── proguard-rules.pro │ └── build.gradle.kts └── shared │ ├── example │ ├── routes │ │ └── example.route.kt │ ├── theme │ │ ├── shape.example.kt │ │ ├── theme.example.kt │ │ └── typography.example.kt │ ├── di │ │ └── koin.example.kt │ └── screens │ │ ├── screen.def.kt │ │ └── home.screen.kt │ ├── exampleIOS │ ├── main.example.kt │ └── theme │ │ └── theme.example.kt │ └── exampleAndroid │ ├── theme │ └── theme.example.kt │ └── main.example.kt ├── exampleIOS ├── Configuration │ └── Config.xcconfig └── iosApp │ ├── Assets.xcassets │ ├── Contents.json │ ├── AppIcon.appiconset │ │ ├── app-icon-1024.png │ │ └── Contents.json │ └── AccentColor.colorset │ │ └── Contents.json │ ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json │ ├── iOSApp.swift │ ├── ContentView.swift │ └── Info.plist ├── exampleApp ├── src │ └── androidMain │ │ ├── res │ │ ├── values │ │ │ ├── strings.xml │ │ │ ├── ic_launcher_background.xml │ │ │ └── creds.xml │ │ ├── mipmap-hdpi │ │ │ ├── ic_launcher.webp │ │ │ ├── ic_launcher_round.webp │ │ │ └── ic_launcher_foreground.webp │ │ ├── mipmap-mdpi │ │ │ ├── ic_launcher.webp │ │ │ ├── ic_launcher_round.webp │ │ │ └── ic_launcher_foreground.webp │ │ ├── mipmap-xhdpi │ │ │ ├── ic_launcher.webp │ │ │ ├── ic_launcher_round.webp │ │ │ └── ic_launcher_foreground.webp │ │ ├── mipmap-xxhdpi │ │ │ ├── ic_launcher.webp │ │ │ ├── ic_launcher_round.webp │ │ │ └── ic_launcher_foreground.webp │ │ ├── mipmap-xxxhdpi │ │ │ ├── ic_launcher.webp │ │ │ ├── ic_launcher_round.webp │ │ │ └── ic_launcher_foreground.webp │ │ ├── mipmap-anydpi-v26 │ │ │ ├── ic_launcher.xml │ │ │ └── ic_launcher_round.xml │ │ └── drawable-v24 │ │ │ └── ic_launcher_foreground.xml │ │ ├── ic_launcher-playstore.png │ │ └── AndroidManifest.xml ├── proguard-rules.pro └── build.gradle.kts ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── library ├── src │ ├── commonMain │ │ └── kotlin │ │ │ ├── utils │ │ │ ├── expected │ │ │ │ ├── date.util.kt │ │ │ │ ├── datastore.koin-module.kt │ │ │ │ ├── crash-analytics.kt │ │ │ │ └── platform.util.kt │ │ │ ├── files.kt │ │ │ ├── text.util.kt │ │ │ ├── datastore.kt │ │ │ ├── utility.kt │ │ │ ├── logger.kt │ │ │ ├── time.kt │ │ │ └── http-utils.kt │ │ │ ├── filters │ │ │ └── filters.kt │ │ │ ├── data │ │ │ ├── types │ │ │ │ ├── states.kt │ │ │ │ ├── types.kt │ │ │ │ └── Errors.kt │ │ │ ├── pagination.kt │ │ │ └── validation │ │ │ │ └── validation.kt │ │ │ ├── modules │ │ │ └── common │ │ │ │ ├── koin.lib.kt │ │ │ │ ├── models │ │ │ │ └── error-responses.kt │ │ │ │ └── features │ │ │ │ └── auth │ │ │ │ └── models │ │ │ │ └── auth.model.kt │ │ │ └── configs │ │ │ ├── web-client.conf.kt │ │ │ └── websocket.client.kt │ ├── jvmMain │ │ └── kotlin │ │ │ ├── utils │ │ │ └── expected │ │ │ │ ├── date.util.kt │ │ │ │ ├── platform.util.kt │ │ │ │ ├── crash-analytics.kt │ │ │ │ └── datastore.koin-module.kt │ │ │ └── configs │ │ │ └── web-client.conf.jvm.kt │ ├── androidMain │ │ └── kotlin │ │ │ ├── utils │ │ │ └── expected │ │ │ │ ├── date.util.kt │ │ │ │ ├── datastore.koin-module.kt │ │ │ │ ├── crash-analytics.kt │ │ │ │ └── platform.util.kt │ │ │ └── configs │ │ │ └── webclient.conf.kt │ ├── linuxX64Main │ │ └── kotlin │ │ │ ├── utils │ │ │ └── expected │ │ │ │ ├── date.util.kt │ │ │ │ ├── platform.util.kt │ │ │ │ └── datastore.koin-module.kt │ │ │ └── configs │ │ │ └── web.client.linux.kt │ └── iosMain │ │ └── kotlin │ │ ├── utils │ │ └── expected │ │ │ ├── date.util.kt │ │ │ ├── platform.util.kt │ │ │ ├── crash-analytics.kt │ │ │ └── datastore.koin-module.kt │ │ └── configs │ │ └── web-client.conf.kt └── build.gradle.kts ├── shared ├── src │ ├── commonMain │ │ ├── kotlin │ │ │ ├── configs │ │ │ │ ├── app-config.kt │ │ │ │ └── Credentials.kt │ │ │ ├── modules │ │ │ │ ├── common │ │ │ │ │ ├── views │ │ │ │ │ │ ├── screens │ │ │ │ │ │ │ └── screens.kt │ │ │ │ │ │ ├── components │ │ │ │ │ │ │ ├── expected │ │ │ │ │ │ │ │ ├── sharable.kt │ │ │ │ │ │ │ │ └── images.kt │ │ │ │ │ │ │ ├── resource-components.kt │ │ │ │ │ │ │ ├── charts.kt │ │ │ │ │ │ │ ├── header.kt │ │ │ │ │ │ │ ├── search.view.kt │ │ │ │ │ │ │ └── tag-scroll.view.kt │ │ │ │ │ │ ├── dimensions │ │ │ │ │ │ │ └── size.kt │ │ │ │ │ │ └── layouts │ │ │ │ │ │ │ └── generic-layout.kt │ │ │ │ │ ├── base │ │ │ │ │ │ ├── ws.kt │ │ │ │ │ │ ├── ViewModel.kt │ │ │ │ │ │ └── BaseResponse.kt │ │ │ │ │ ├── ads │ │ │ │ │ │ └── ads.kt │ │ │ │ │ ├── features │ │ │ │ │ │ ├── preferences │ │ │ │ │ │ │ ├── pref.model.kt │ │ │ │ │ │ │ └── pref.vm.kt │ │ │ │ │ │ ├── auth │ │ │ │ │ │ │ ├── models │ │ │ │ │ │ │ │ └── user.model.kt │ │ │ │ │ │ │ ├── auth.kt │ │ │ │ │ │ │ ├── AuthService.kt │ │ │ │ │ │ │ └── AuthRepository.kt │ │ │ │ │ │ ├── notifications │ │ │ │ │ │ │ └── models │ │ │ │ │ │ │ │ └── notifications.kt │ │ │ │ │ │ └── events │ │ │ │ │ │ │ └── models │ │ │ │ │ │ │ └── event.model.kt │ │ │ │ │ ├── animations │ │ │ │ │ │ ├── transition-animations.kt │ │ │ │ │ │ └── transitions.kt │ │ │ │ │ ├── routes │ │ │ │ │ │ └── route.kt │ │ │ │ │ └── di │ │ │ │ │ │ └── koin.common.kt │ │ │ │ └── exampleModule │ │ │ │ │ ├── routes │ │ │ │ │ └── example.route.kt │ │ │ │ │ ├── theme │ │ │ │ │ ├── shape.example.kt │ │ │ │ │ ├── theme.example.kt │ │ │ │ │ └── typography.example.kt │ │ │ │ │ ├── features │ │ │ │ │ └── todo │ │ │ │ │ │ ├── dto │ │ │ │ │ │ └── todo.response.kt │ │ │ │ │ │ ├── todo.service.kt │ │ │ │ │ │ ├── todo.vm.kt │ │ │ │ │ │ └── todo.repository.kt │ │ │ │ │ ├── screens │ │ │ │ │ ├── screen.def.kt │ │ │ │ │ └── home.screen.kt │ │ │ │ │ └── di │ │ │ │ │ └── koin.example.kt │ │ │ └── utils │ │ │ │ ├── expected │ │ │ │ ├── ui-utility.kt │ │ │ │ ├── toast.kt │ │ │ │ └── analytics.kt │ │ │ │ └── ui-utils.kt │ │ └── composeResources │ │ │ ├── font │ │ │ ├── solaimanlipi.ttf │ │ │ ├── charu_chandan_bold.ttf │ │ │ ├── charu_chandan_light.ttf │ │ │ └── charu_chandan_regular.ttf │ │ │ ├── values │ │ │ ├── preferences.xml │ │ │ ├── strings.xml │ │ │ └── cred.xml │ │ │ ├── values-bn │ │ │ └── strings.xml │ │ │ └── drawable │ │ │ ├── box.xml │ │ │ └── compose-multiplatform.xml │ ├── iosMain │ │ └── kotlin │ │ │ ├── utils │ │ │ └── expected │ │ │ │ ├── analytics.ios.kt │ │ │ │ ├── toast.ios.kt │ │ │ │ └── ui-utility.ios.kt │ │ │ └── modules │ │ │ ├── exampleModule │ │ │ ├── main.example.kt │ │ │ └── theme │ │ │ │ └── theme.example.kt │ │ │ └── common │ │ │ ├── base │ │ │ └── ViewModel.kt │ │ │ ├── ads │ │ │ └── ads.ios.kt │ │ │ └── views │ │ │ └── components │ │ │ └── expected │ │ │ ├── sharable.kt │ │ │ └── images.kt │ └── androidMain │ │ ├── AndroidManifest.xml │ │ └── kotlin │ │ ├── modules │ │ ├── common │ │ │ ├── base │ │ │ │ └── ViewModel.kt │ │ │ ├── views │ │ │ │ └── components │ │ │ │ │ └── expected │ │ │ │ │ ├── sharable.kt │ │ │ │ │ └── images.kt │ │ │ └── ads │ │ │ │ └── ads.android.kt │ │ └── exampleModule │ │ │ ├── theme │ │ │ └── theme.example.kt │ │ │ └── main.example.kt │ │ └── utils │ │ └── expected │ │ ├── analytics.android.kt │ │ ├── ui-utility.android.kt │ │ └── toast.android.kt ├── shared.podspec └── build.gradle.kts ├── cleanup.sh ├── .gitignore ├── gradle.properties ├── settings.gradle.kts ├── README.md └── gradlew.bat /scripts/tools/gentool.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sayemoid/kmm-booster-template/HEAD/scripts/tools/gentool.jar -------------------------------------------------------------------------------- /examples/exampleIOS/Configuration/Config.xcconfig: -------------------------------------------------------------------------------- 1 | TEAM_ID= 2 | BUNDLE_ID=org.noksha.exampleapp 3 | APP_NAME=ExampleApp 4 | -------------------------------------------------------------------------------- /exampleIOS/Configuration/Config.xcconfig: -------------------------------------------------------------------------------- 1 | TEAM_ID= 2 | BUNDLE_ID=com.myapplication.MyApplication 3 | APP_NAME=My application 4 | -------------------------------------------------------------------------------- /exampleApp/src/androidMain/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | ExampleApp 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sayemoid/kmm-booster-template/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /exampleIOS/iosApp/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /examples/exampleApp/src/androidMain/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | ExampleApp 3 | -------------------------------------------------------------------------------- /library/src/commonMain/kotlin/utils/expected/date.util.kt: -------------------------------------------------------------------------------- 1 | package utils.expected 2 | 3 | expect fun getDefaultDateInMillis(): Long 4 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/configs/app-config.kt: -------------------------------------------------------------------------------- 1 | package configs 2 | 3 | enum class AppThemes { 4 | DEFAULT, DARK, LIGHT 5 | } 6 | -------------------------------------------------------------------------------- /examples/exampleIOS/iosApp/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /exampleIOS/iosApp/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } -------------------------------------------------------------------------------- /library/src/jvmMain/kotlin/utils/expected/date.util.kt: -------------------------------------------------------------------------------- 1 | package utils.expected 2 | actual fun getDefaultDateInMillis(): Long = System.currentTimeMillis() 3 | -------------------------------------------------------------------------------- /examples/exampleIOS/iosApp/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } -------------------------------------------------------------------------------- /library/src/androidMain/kotlin/utils/expected/date.util.kt: -------------------------------------------------------------------------------- 1 | package utils.expected 2 | actual fun getDefaultDateInMillis(): Long = System.currentTimeMillis() 3 | -------------------------------------------------------------------------------- /exampleApp/src/androidMain/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sayemoid/kmm-booster-template/HEAD/exampleApp/src/androidMain/ic_launcher-playstore.png -------------------------------------------------------------------------------- /exampleApp/src/androidMain/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sayemoid/kmm-booster-template/HEAD/exampleApp/src/androidMain/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /exampleApp/src/androidMain/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sayemoid/kmm-booster-template/HEAD/exampleApp/src/androidMain/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /library/src/commonMain/kotlin/utils/expected/datastore.koin-module.kt: -------------------------------------------------------------------------------- 1 | package utils.expected 2 | 3 | import org.koin.core.module.Module 4 | 5 | expect val datastoreModule: Module -------------------------------------------------------------------------------- /exampleApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sayemoid/kmm-booster-template/HEAD/exampleApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /exampleApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sayemoid/kmm-booster-template/HEAD/exampleApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /examples/exampleApp/src/androidMain/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sayemoid/kmm-booster-template/HEAD/examples/exampleApp/src/androidMain/ic_launcher-playstore.png -------------------------------------------------------------------------------- /shared/src/commonMain/composeResources/font/solaimanlipi.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sayemoid/kmm-booster-template/HEAD/shared/src/commonMain/composeResources/font/solaimanlipi.ttf -------------------------------------------------------------------------------- /exampleApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sayemoid/kmm-booster-template/HEAD/exampleApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /exampleApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sayemoid/kmm-booster-template/HEAD/exampleApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /exampleApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sayemoid/kmm-booster-template/HEAD/exampleApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /exampleApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sayemoid/kmm-booster-template/HEAD/exampleApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /exampleApp/src/androidMain/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #4285F4 4 | -------------------------------------------------------------------------------- /shared/src/commonMain/composeResources/font/charu_chandan_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sayemoid/kmm-booster-template/HEAD/shared/src/commonMain/composeResources/font/charu_chandan_bold.ttf -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/modules/common/views/screens/screens.kt: -------------------------------------------------------------------------------- 1 | package modules.common.views.screens 2 | 3 | import cafe.adriel.voyager.core.screen.Screen 4 | 5 | interface AppScreen : Screen -------------------------------------------------------------------------------- /exampleApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sayemoid/kmm-booster-template/HEAD/exampleApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /exampleApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sayemoid/kmm-booster-template/HEAD/exampleApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /exampleApp/src/androidMain/res/values/creds.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | ca-app-pub-3940256099942544~3347511713 4 | -------------------------------------------------------------------------------- /examples/exampleApp/src/androidMain/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sayemoid/kmm-booster-template/HEAD/examples/exampleApp/src/androidMain/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /examples/exampleApp/src/androidMain/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sayemoid/kmm-booster-template/HEAD/examples/exampleApp/src/androidMain/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /examples/exampleApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sayemoid/kmm-booster-template/HEAD/examples/exampleApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /library/src/linuxX64Main/kotlin/utils/expected/date.util.kt: -------------------------------------------------------------------------------- 1 | package utils.expected 2 | 3 | import arrow.fx.coroutines.timeInMillis 4 | 5 | actual fun getDefaultDateInMillis(): Long = timeInMillis() 6 | -------------------------------------------------------------------------------- /shared/src/commonMain/composeResources/font/charu_chandan_light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sayemoid/kmm-booster-template/HEAD/shared/src/commonMain/composeResources/font/charu_chandan_light.ttf -------------------------------------------------------------------------------- /shared/src/commonMain/composeResources/font/charu_chandan_regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sayemoid/kmm-booster-template/HEAD/shared/src/commonMain/composeResources/font/charu_chandan_regular.ttf -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/modules/common/base/ws.kt: -------------------------------------------------------------------------------- 1 | package modules.common.base 2 | 3 | interface WSMessage { 4 | val username: String? 5 | val message: String 6 | val data: Map 7 | } -------------------------------------------------------------------------------- /exampleApp/src/androidMain/res/mipmap-hdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sayemoid/kmm-booster-template/HEAD/exampleApp/src/androidMain/res/mipmap-hdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /exampleApp/src/androidMain/res/mipmap-mdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sayemoid/kmm-booster-template/HEAD/exampleApp/src/androidMain/res/mipmap-mdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /exampleApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sayemoid/kmm-booster-template/HEAD/exampleApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /exampleIOS/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sayemoid/kmm-booster-template/HEAD/exampleIOS/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png -------------------------------------------------------------------------------- /examples/exampleApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sayemoid/kmm-booster-template/HEAD/examples/exampleApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /examples/exampleApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sayemoid/kmm-booster-template/HEAD/examples/exampleApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /examples/exampleApp/src/androidMain/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #4285F4 4 | -------------------------------------------------------------------------------- /exampleApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sayemoid/kmm-booster-template/HEAD/exampleApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /exampleApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sayemoid/kmm-booster-template/HEAD/exampleApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /examples/exampleApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sayemoid/kmm-booster-template/HEAD/examples/exampleApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /examples/exampleApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sayemoid/kmm-booster-template/HEAD/examples/exampleApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /examples/exampleApp/src/androidMain/res/values/creds.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | ca-app-pub-3940256099942544~3347511713 4 | -------------------------------------------------------------------------------- /shared/src/iosMain/kotlin/utils/expected/analytics.ios.kt: -------------------------------------------------------------------------------- 1 | package utils.expected 2 | 3 | import utils.Tag 4 | 5 | actual object Analytics { 6 | actual fun log(event: Tag.Event, data: Data) {} 7 | } 8 | 9 | -------------------------------------------------------------------------------- /exampleApp/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Suppress warnings for missing classes 2 | -dontwarn java.net.http.HttpResponse 3 | -dontwarn java.net.http.WebSocketHandshakeException 4 | -dontwarn org.slf4j.impl.StaticLoggerBinder 5 | -------------------------------------------------------------------------------- /examples/exampleApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sayemoid/kmm-booster-template/HEAD/examples/exampleApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /examples/exampleApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sayemoid/kmm-booster-template/HEAD/examples/exampleApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /examples/exampleApp/src/androidMain/res/mipmap-hdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sayemoid/kmm-booster-template/HEAD/examples/exampleApp/src/androidMain/res/mipmap-hdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /examples/exampleApp/src/androidMain/res/mipmap-mdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sayemoid/kmm-booster-template/HEAD/examples/exampleApp/src/androidMain/res/mipmap-mdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /examples/exampleApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sayemoid/kmm-booster-template/HEAD/examples/exampleApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /examples/exampleIOS/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sayemoid/kmm-booster-template/HEAD/examples/exampleIOS/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png -------------------------------------------------------------------------------- /library/src/commonMain/kotlin/filters/filters.kt: -------------------------------------------------------------------------------- 1 | package filters 2 | 3 | import io.ktor.client.statement.HttpResponse 4 | 5 | interface HttpFilter { 6 | suspend fun apply(response: HttpResponse): HttpResponse 7 | } 8 | -------------------------------------------------------------------------------- /library/src/iosMain/kotlin/utils/expected/date.util.kt: -------------------------------------------------------------------------------- 1 | package utils.expected 2 | 3 | import platform.QuartzCore.CACurrentMediaTime 4 | 5 | actual fun getDefaultDateInMillis(): Long = CACurrentMediaTime().toLong() 6 | -------------------------------------------------------------------------------- /examples/exampleApp/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Suppress warnings for missing classes 2 | -dontwarn java.net.http.HttpResponse 3 | -dontwarn java.net.http.WebSocketHandshakeException 4 | -dontwarn org.slf4j.impl.StaticLoggerBinder 5 | -------------------------------------------------------------------------------- /examples/exampleApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sayemoid/kmm-booster-template/HEAD/examples/exampleApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /examples/exampleApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sayemoid/kmm-booster-template/HEAD/examples/exampleApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /examples/shared/example/routes/example.route.kt: -------------------------------------------------------------------------------- 1 | package modules.exampleModule.routes 2 | 3 | object ExampleRoutes { 4 | object Todo{ 5 | const val GET_TODO_ITEMS = "https://jsonplaceholder.typicode.com/todos" 6 | } 7 | } -------------------------------------------------------------------------------- /examples/exampleApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_foreground.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sayemoid/kmm-booster-template/HEAD/examples/exampleApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_foreground.webp -------------------------------------------------------------------------------- /library/src/commonMain/kotlin/utils/expected/crash-analytics.kt: -------------------------------------------------------------------------------- 1 | package utils.expected 2 | 3 | expect class CrashAnalytics { 4 | fun init(dsn: String) 5 | 6 | companion object { 7 | fun capture(throwable: Throwable) 8 | } 9 | } -------------------------------------------------------------------------------- /exampleIOS/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } -------------------------------------------------------------------------------- /shared/src/commonMain/composeResources/values/preferences.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Profile 4 | user-name 5 | -------------------------------------------------------------------------------- /examples/exampleIOS/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } -------------------------------------------------------------------------------- /shared/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/modules/exampleModule/routes/example.route.kt: -------------------------------------------------------------------------------- 1 | package modules.exampleModule.routes 2 | 3 | object ExampleRoutes { 4 | object Todo{ 5 | const val GET_TODO_ITEMS = "https://jsonplaceholder.typicode.com/todos" 6 | } 7 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/utils/expected/ui-utility.kt: -------------------------------------------------------------------------------- 1 | package utils.expected 2 | 3 | import androidx.compose.runtime.Composable 4 | 5 | expect class RemoteUrl() { 6 | @Composable 7 | fun create() 8 | fun open(url: String) 9 | } 10 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/modules/common/base/ViewModel.kt: -------------------------------------------------------------------------------- 1 | package modules.common.base 2 | 3 | import kotlinx.coroutines.CoroutineScope 4 | 5 | expect abstract class ViewModel() { 6 | val viewModelScope: CoroutineScope 7 | protected open fun onCleared() 8 | } -------------------------------------------------------------------------------- /library/src/commonMain/kotlin/utils/expected/platform.util.kt: -------------------------------------------------------------------------------- 1 | package utils.expected 2 | 3 | expect fun uuid(): String 4 | 5 | expect val isDebug: Boolean 6 | 7 | enum class Platforms{ 8 | ANDROID, IOS, JVM; 9 | } 10 | 11 | expect fun platform(): Platforms 12 | -------------------------------------------------------------------------------- /cleanup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | rm -rf .idea 3 | ./gradlew clean 4 | rm -rf .gradle 5 | rm -rf build 6 | rm -rf */build 7 | rm -rf iosApp/iosApp.xcworkspace 8 | rm -rf iosApp/Pods 9 | rm -rf iosApp/iosApp.xcodeproj/project.xcworkspace 10 | rm -rf iosApp/iosApp.xcodeproj/xcuserdata -------------------------------------------------------------------------------- /library/src/jvmMain/kotlin/utils/expected/platform.util.kt: -------------------------------------------------------------------------------- 1 | package utils.expected 2 | 3 | import java.util.UUID 4 | 5 | actual fun uuid(): String = UUID.randomUUID().toString() 6 | 7 | actual val isDebug = true 8 | 9 | actual fun platform(): Platforms = Platforms.JVM 10 | -------------------------------------------------------------------------------- /exampleIOS/iosApp/iOSApp.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import shared 3 | 4 | @main 5 | struct iOSApp: App { 6 | 7 | init() { 8 | Koin_exampleKt.doInitKoin() 9 | } 10 | 11 | var body: some Scene { 12 | WindowGroup { 13 | ContentView() 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /examples/exampleIOS/iosApp.xcodeproj/project.xcworkspace/xcuserdata/sayem.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sayemoid/kmm-booster-template/HEAD/examples/exampleIOS/iosApp.xcodeproj/project.xcworkspace/xcuserdata/sayem.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Aug 12 15:26:48 BDT 2023 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /examples/exampleIOS/iosApp/iOSApp.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import shared 3 | 4 | @main 5 | struct iOSApp: App { 6 | 7 | init() { 8 | Koin_exampleKt.doInitKoin() 9 | } 10 | 11 | var body: some Scene { 12 | WindowGroup { 13 | ContentView() 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/modules/common/ads/ads.kt: -------------------------------------------------------------------------------- 1 | package modules.common.ads 2 | 3 | import androidx.compose.runtime.Composable 4 | 5 | expect class Ads { 6 | fun load(adUnitId: String) 7 | 8 | fun show() 9 | } 10 | 11 | @Composable 12 | expect fun interstitialAd(adUnitId: String): Ads 13 | -------------------------------------------------------------------------------- /library/src/commonMain/kotlin/utils/files.kt: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | fun ByteArray.resizeTo(newSize: Int): ByteArray { 4 | if (newSize > this.size) return this 5 | val newByteArray = ByteArray(newSize) 6 | for (i in 0 until newSize) { 7 | newByteArray[i] = this[i] 8 | } 9 | return newByteArray 10 | } -------------------------------------------------------------------------------- /library/src/iosMain/kotlin/configs/web-client.conf.kt: -------------------------------------------------------------------------------- 1 | package configs 2 | 3 | import io.ktor.client.engine.HttpClientEngineConfig 4 | import io.ktor.client.engine.HttpClientEngineFactory 5 | import io.ktor.client.engine.darwin.Darwin 6 | 7 | actual fun getEngine(): HttpClientEngineFactory = Darwin -------------------------------------------------------------------------------- /library/src/linuxX64Main/kotlin/configs/web.client.linux.kt: -------------------------------------------------------------------------------- 1 | package configs 2 | 3 | import io.ktor.client.engine.HttpClientEngineConfig 4 | import io.ktor.client.engine.HttpClientEngineFactory 5 | import io.ktor.client.engine.cio.CIO 6 | 7 | actual fun getEngine(): HttpClientEngineFactory = CIO -------------------------------------------------------------------------------- /library/src/androidMain/kotlin/configs/webclient.conf.kt: -------------------------------------------------------------------------------- 1 | package configs 2 | 3 | import io.ktor.client.engine.HttpClientEngineConfig 4 | import io.ktor.client.engine.HttpClientEngineFactory 5 | import io.ktor.client.engine.okhttp.OkHttp 6 | 7 | actual fun getEngine(): HttpClientEngineFactory = OkHttp -------------------------------------------------------------------------------- /library/src/jvmMain/kotlin/configs/web-client.conf.jvm.kt: -------------------------------------------------------------------------------- 1 | package configs 2 | 3 | import io.ktor.client.engine.HttpClientEngineConfig 4 | import io.ktor.client.engine.HttpClientEngineFactory 5 | import io.ktor.client.engine.okhttp.OkHttp 6 | 7 | actual fun getEngine(): HttpClientEngineFactory = OkHttp -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/modules/common/views/components/expected/sharable.kt: -------------------------------------------------------------------------------- 1 | package modules.common.views.components.expected 2 | 3 | import androidx.compose.runtime.Composable 4 | 5 | expect class ShareData { 6 | fun shareContent(text: String) 7 | } 8 | 9 | @Composable 10 | expect fun createShareSheet(): ShareData -------------------------------------------------------------------------------- /examples/shared/exampleIOS/main.example.kt: -------------------------------------------------------------------------------- 1 | package modules.exampleModule 2 | 3 | import androidx.compose.ui.window.ComposeUIViewController 4 | import cafe.adriel.voyager.navigator.Navigator 5 | import modules.exampleModule.screens.MainScreen 6 | 7 | fun MainViewController() = ComposeUIViewController { 8 | Navigator(MainScreen) 9 | } -------------------------------------------------------------------------------- /exampleApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /shared/src/iosMain/kotlin/utils/expected/toast.ios.kt: -------------------------------------------------------------------------------- 1 | package utils.expected 2 | 3 | actual class Toasty { 4 | actual companion object { 5 | actual fun show() = Unit 6 | actual fun with( 7 | context: Any, 8 | resId: Int, 9 | duration: Duration 10 | ): Companion { 11 | TODO("Not yet implemented") 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /exampleApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /exampleIOS/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "app-icon-1024.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | } 9 | ], 10 | "info" : { 11 | "author" : "xcode", 12 | "version" : 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/exampleApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /shared/src/iosMain/kotlin/modules/exampleModule/main.example.kt: -------------------------------------------------------------------------------- 1 | package modules.exampleModule 2 | 3 | import androidx.compose.ui.window.ComposeUIViewController 4 | import cafe.adriel.voyager.navigator.Navigator 5 | import modules.exampleModule.screens.MainScreen 6 | 7 | fun MainViewController() = ComposeUIViewController { 8 | Navigator(MainScreen) 9 | } -------------------------------------------------------------------------------- /examples/exampleApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /examples/exampleIOS/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "app-icon-1024.png", 5 | "idiom" : "universal", 6 | "platform" : "ios", 7 | "size" : "1024x1024" 8 | } 9 | ], 10 | "info" : { 11 | "author" : "xcode", 12 | "version" : 1 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/exampleIOS/iosApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/shared/example/theme/shape.example.kt: -------------------------------------------------------------------------------- 1 | package modules.exampleModule.theme 2 | 3 | import androidx.compose.foundation.shape.RoundedCornerShape 4 | import androidx.compose.material3.Shapes 5 | import androidx.compose.ui.unit.dp 6 | 7 | val shapes = Shapes( 8 | extraSmall = RoundedCornerShape(4.dp), 9 | small = RoundedCornerShape(12.dp), 10 | ) 11 | -------------------------------------------------------------------------------- /examples/shared/example/theme/theme.example.kt: -------------------------------------------------------------------------------- 1 | package modules.exampleModule.theme 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.runtime.Composable 5 | 6 | @Composable 7 | expect fun ExampleAppTheme( 8 | useDarkTheme: Boolean = isSystemInDarkTheme(), 9 | dynamicColorScheme: Boolean = false, 10 | content: @Composable () -> Unit 11 | ) -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/modules/exampleModule/theme/shape.example.kt: -------------------------------------------------------------------------------- 1 | package modules.exampleModule.theme 2 | 3 | import androidx.compose.foundation.shape.RoundedCornerShape 4 | import androidx.compose.material3.Shapes 5 | import androidx.compose.ui.unit.dp 6 | 7 | val shapes = Shapes( 8 | extraSmall = RoundedCornerShape(4.dp), 9 | small = RoundedCornerShape(12.dp), 10 | ) 11 | -------------------------------------------------------------------------------- /library/src/commonMain/kotlin/utils/text.util.kt: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | fun htmlToPlainText(html: String): String { 4 | var text = "" 5 | var inTag = false 6 | 7 | for (char in html) { 8 | if (char == '<') { 9 | inTag = true 10 | } else if (char == '>') { 11 | inTag = false 12 | } else if (!inTag) { 13 | text += char 14 | } 15 | } 16 | return text.trim() 17 | } 18 | -------------------------------------------------------------------------------- /library/src/iosMain/kotlin/utils/expected/platform.util.kt: -------------------------------------------------------------------------------- 1 | package utils.expected 2 | 3 | import platform.Foundation.NSUUID 4 | import kotlin.experimental.ExperimentalNativeApi 5 | 6 | actual fun uuid(): String = NSUUID().UUIDString() 7 | 8 | @OptIn(ExperimentalNativeApi::class) 9 | actual val isDebug = Platform.isDebugBinary 10 | 11 | actual fun platform(): Platforms = Platforms.IOS 12 | -------------------------------------------------------------------------------- /shared/src/iosMain/kotlin/modules/common/base/ViewModel.kt: -------------------------------------------------------------------------------- 1 | package modules.common.base 2 | 3 | import kotlinx.coroutines.MainScope 4 | import kotlinx.coroutines.cancel 5 | 6 | actual abstract class ViewModel { 7 | actual val viewModelScope = MainScope() 8 | 9 | protected actual open fun onCleared() {} 10 | 11 | fun clear() { 12 | onCleared() 13 | viewModelScope.cancel() 14 | } 15 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/modules/exampleModule/theme/theme.example.kt: -------------------------------------------------------------------------------- 1 | package modules.exampleModule.theme 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.runtime.Composable 5 | 6 | @Composable 7 | expect fun ExampleAppTheme( 8 | useDarkTheme: Boolean = isSystemInDarkTheme(), 9 | dynamicColorScheme: Boolean = false, 10 | content: @Composable () -> Unit 11 | ) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | build/ 7 | /captures 8 | .externalNativeBuild 9 | .cxx 10 | /.kotlin 11 | 12 | 13 | exampleIOS/iosApp.xcworkspace/* 14 | exampleIOS/iosApp.xcodeproj/* 15 | !exampleIOS/iosApp.xcodeproj/project.pbxproj 16 | 17 | iosQuizBox/iosApp.xcworkspace/* 18 | iosQuizBox/iosApp.xcodeproj/* 19 | !iosQuizBox/iosApp.xcodeproj/project.pbxproj 20 | 21 | -------------------------------------------------------------------------------- /library/src/iosMain/kotlin/utils/expected/crash-analytics.kt: -------------------------------------------------------------------------------- 1 | package utils.expected 2 | 3 | import utils.Tag 4 | import utils.logD 5 | 6 | actual class CrashAnalytics(private val debug: Boolean) { 7 | actual fun init(dsn: String) { 8 | logD(Tag.Crash, "Sentry initialization for IOS (Not yet implemented)!") 9 | } 10 | 11 | actual companion object { 12 | actual fun capture(throwable: Throwable) {} 13 | } 14 | } -------------------------------------------------------------------------------- /library/src/jvmMain/kotlin/utils/expected/crash-analytics.kt: -------------------------------------------------------------------------------- 1 | package utils.expected 2 | 3 | import utils.Tag 4 | import utils.logD 5 | 6 | actual class CrashAnalytics(private val debug: Boolean) { 7 | actual fun init(dsn: String) { 8 | logD(Tag.Crash, "Sentry initialization for IOS (Not yet implemented)!") 9 | } 10 | 11 | actual companion object { 12 | actual fun capture(throwable: Throwable) {} 13 | } 14 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/modules/exampleModule/features/todo/dto/todo.response.kt: -------------------------------------------------------------------------------- 1 | package modules.exampleModule.features.todo.dto 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | data class TodoResponse( 8 | val id: Long, 9 | 10 | @SerialName("userId") 11 | val userId: Long, 12 | 13 | val title: String, 14 | 15 | val completed: Boolean 16 | ) -------------------------------------------------------------------------------- /examples/shared/example/di/koin.example.kt: -------------------------------------------------------------------------------- 1 | package modules.exampleModule.di 2 | 3 | import modules.common.di.commonModule 4 | import org.koin.core.context.startKoin 5 | import org.koin.dsl.module 6 | 7 | val exampleModule = module { 8 | 9 | /* 10 | Register koin components here 11 | */ 12 | 13 | } 14 | 15 | val koinExampleModules = commonModule + exampleModule 16 | 17 | fun initKoin() = startKoin { modules(koinExampleModules) } -------------------------------------------------------------------------------- /shared/src/iosMain/kotlin/modules/common/ads/ads.ios.kt: -------------------------------------------------------------------------------- 1 | package modules.common.ads 2 | 3 | import androidx.compose.runtime.Composable 4 | import modules.common.ads.Ads 5 | 6 | 7 | actual class Ads { 8 | 9 | 10 | actual fun load(adUnitId: String) { 11 | } 12 | 13 | actual fun show() { 14 | } 15 | } 16 | 17 | @Composable 18 | actual fun interstitialAd(adUnitId: String): Ads { 19 | val ad = Ads() 20 | ad.load(adUnitId) 21 | return ad 22 | } -------------------------------------------------------------------------------- /shared/src/iosMain/kotlin/utils/expected/ui-utility.ios.kt: -------------------------------------------------------------------------------- 1 | package utils.expected 2 | 3 | import androidx.compose.runtime.Composable 4 | import platform.Foundation.NSURL 5 | import platform.UIKit.UIApplication 6 | 7 | actual class RemoteUrl actual constructor() { 8 | 9 | @Composable 10 | actual fun create() { 11 | } 12 | 13 | actual fun open(url: String) { 14 | NSURL.URLWithString(url)?.let { 15 | UIApplication.sharedApplication.openURL(it) 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /examples/exampleIOS/iosApp.xcodeproj/xcuserdata/sayem.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | iosApp.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /shared/src/commonMain/composeResources/values-bn/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | প্রস্তুতি 4 | 5 | 6 | হোম 7 | ভোটের সারাংশ 8 | লগইন 9 | সাইন আপ 10 | প্রোফাইল 11 | 12 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/modules/common/base/BaseResponse.kt: -------------------------------------------------------------------------------- 1 | package modules.common.base 2 | 3 | import kotlinx.serialization.SerialName 4 | import kotlinx.serialization.Serializable 5 | 6 | @Serializable 7 | open class BaseReq { 8 | val id: Long? = null 9 | } 10 | 11 | @Serializable 12 | open class BaseResponse { 13 | val id: Long = 0 14 | 15 | @SerialName("created_at") 16 | lateinit var createdAt: String 17 | 18 | @SerialName("updated_at") 19 | var updatedAt: String? = null 20 | } 21 | -------------------------------------------------------------------------------- /shared/src/androidMain/kotlin/modules/common/base/ViewModel.kt: -------------------------------------------------------------------------------- 1 | package modules.common.base 2 | 3 | import kotlinx.coroutines.CoroutineScope 4 | import androidx.lifecycle.ViewModel as AndroidXViewModel 5 | import androidx.lifecycle.viewModelScope as androidXViewModelScope 6 | 7 | actual abstract class ViewModel actual constructor() : AndroidXViewModel() { 8 | actual val viewModelScope: CoroutineScope = androidXViewModelScope 9 | 10 | actual override fun onCleared() { 11 | super.onCleared() 12 | } 13 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/utils/expected/toast.kt: -------------------------------------------------------------------------------- 1 | package utils.expected 2 | 3 | sealed interface Duration { 4 | data object Short : Duration 5 | data object Medium : Duration 6 | data object Long : Duration 7 | 8 | fun millisFromDuration(duration: Duration): Int = 9 | when (duration) { 10 | Short -> 500 11 | Medium -> 1000 12 | Long -> 2000 13 | } 14 | } 15 | 16 | expect class Toasty { 17 | companion object { 18 | fun with(context: Any, resId: Int, duration: Duration): Companion 19 | fun show() 20 | 21 | } 22 | } -------------------------------------------------------------------------------- /shared/src/commonMain/composeResources/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Example 4 | 5 | 6 | Home 7 | Home 8 | Poll Summary 9 | Login 10 | Signup 11 | Profile 12 | 13 | -------------------------------------------------------------------------------- /shared/src/commonMain/composeResources/values/cred.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | https://8d1741cf46ba474d9539eac057269f51@o4505135996272640.ingest.sentry.io/4505135998828544 5 | 6 | ca-app-pub-3940256099942544/1033173712 7 | ca-app-pub-7944444189407778/1316428355 8 | 9 | client_id 10 | client_secret 11 | 12 | -------------------------------------------------------------------------------- /examples/shared/exampleIOS/theme/theme.example.kt: -------------------------------------------------------------------------------- 1 | package modules.exampleModule.theme 2 | 3 | import androidx.compose.material3.MaterialTheme 4 | import androidx.compose.runtime.Composable 5 | 6 | @Composable 7 | actual fun ExampleAppTheme( 8 | useDarkTheme: Boolean, 9 | dynamicColorScheme: Boolean, 10 | content: @Composable () -> Unit 11 | ) { 12 | val colors = when { 13 | useDarkTheme -> darkColors 14 | else -> lightColors 15 | } 16 | 17 | MaterialTheme( 18 | colorScheme = colors, 19 | shapes = shapes, 20 | typography = typography, 21 | content = content, 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /library/src/commonMain/kotlin/data/types/states.kt: -------------------------------------------------------------------------------- 1 | package data.types 2 | 3 | import arrow.core.Either 4 | import data.responses.ErrMessage 5 | 6 | sealed interface State { 7 | data object Init : State 8 | data object Loading : State 9 | data class Result(val result: Either) : State 10 | } 11 | 12 | sealed interface SignUpStates { 13 | data object Init : SignUpStates 14 | data object Loading : SignUpStates 15 | data class Acknowledgement(val result: Either) : SignUpStates 16 | data class Result(val result: Either) : SignUpStates 17 | } -------------------------------------------------------------------------------- /exampleIOS/iosApp/ContentView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SwiftUI 3 | import shared 4 | 5 | struct ComposeView: UIViewControllerRepresentable { 6 | func makeUIViewController(context: Context) -> UIViewController { 7 | Main_exampleKt.MainViewController() 8 | } 9 | 10 | func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} 11 | } 12 | 13 | struct ContentView: View { 14 | var body: some View { 15 | ComposeView() 16 | .ignoresSafeArea(.all, edges: .bottom) // Compose has own keyboard handler 17 | } 18 | } 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /library/src/commonMain/kotlin/utils/datastore.kt: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import androidx.datastore.core.DataStore 4 | import androidx.datastore.preferences.core.PreferenceDataStoreFactory 5 | import androidx.datastore.preferences.core.Preferences 6 | import okio.Path.Companion.toPath 7 | 8 | fun createDataStore( 9 | producePath: () -> String, 10 | ): DataStore = PreferenceDataStoreFactory.createWithPath( 11 | corruptionHandler = null, 12 | migrations = emptyList(), 13 | produceFile = { producePath().toPath() }, 14 | ) 15 | 16 | internal const val dataStoreFileName = "meetings.preferences_pb" -------------------------------------------------------------------------------- /examples/exampleIOS/iosApp/ContentView.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import SwiftUI 3 | import shared 4 | 5 | struct ComposeView: UIViewControllerRepresentable { 6 | func makeUIViewController(context: Context) -> UIViewController { 7 | Main_exampleKt.MainViewController() 8 | } 9 | 10 | func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} 11 | } 12 | 13 | struct ContentView: View { 14 | var body: some View { 15 | ComposeView() 16 | .ignoresSafeArea(.all, edges: .bottom) // Compose has own keyboard handler 17 | } 18 | } 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /shared/src/iosMain/kotlin/modules/exampleModule/theme/theme.example.kt: -------------------------------------------------------------------------------- 1 | package modules.exampleModule.theme 2 | 3 | import androidx.compose.material3.MaterialTheme 4 | import androidx.compose.runtime.Composable 5 | 6 | @Composable 7 | actual fun ExampleAppTheme( 8 | useDarkTheme: Boolean, 9 | dynamicColorScheme: Boolean, 10 | content: @Composable () -> Unit 11 | ) { 12 | val colors = when { 13 | useDarkTheme -> darkColors 14 | else -> lightColors 15 | } 16 | 17 | MaterialTheme( 18 | colorScheme = colors, 19 | shapes = shapes, 20 | typography = typography, 21 | content = content, 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | #Gradle 2 | org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M" 3 | 4 | #Kotlin 5 | kotlin.code.style=official 6 | 7 | #MPP 8 | kotlin.mpp.stability.nowarn=true 9 | kotlin.mpp.enableCInteropCommonization=true 10 | kotlin.mpp.androidSourceSetLayoutVersion=2 11 | 12 | #Compose 13 | org.jetbrains.compose.experimental.uikit.enabled=true 14 | 15 | #Android 16 | android.useAndroidX=true 17 | android.targetSdk=34 18 | android.compileSdk=34 19 | android.minSdk=24 20 | 21 | #Versions 22 | kotlin.compiler.extensionVersion=1.5.3 23 | kotlin.version=1.9.10 24 | agp.version=8.0.2 25 | compose.version=1.5.3 -------------------------------------------------------------------------------- /library/src/androidMain/kotlin/utils/expected/datastore.koin-module.kt: -------------------------------------------------------------------------------- 1 | package utils.expected 2 | 3 | import android.content.Context 4 | import androidx.datastore.core.DataStore 5 | import androidx.datastore.preferences.core.Preferences 6 | import org.koin.core.module.Module 7 | import org.koin.dsl.module 8 | import utils.createDataStore 9 | import utils.dataStoreFileName 10 | 11 | 12 | actual val datastoreModule: Module = module { 13 | single { dataStore(get())} 14 | } 15 | 16 | fun dataStore(context: Context): DataStore = 17 | createDataStore( 18 | producePath = { context.filesDir.resolve(dataStoreFileName).absolutePath } 19 | ) 20 | -------------------------------------------------------------------------------- /library/src/commonMain/kotlin/utils/utility.kt: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import kotlinx.datetime.Instant 4 | 5 | fun List.qPop(): List = 6 | if (this.isEmpty()) this 7 | else this.subList(1, lastIndex + 1) 8 | 9 | fun List.qPush(element: T): List = this + listOf(element) 10 | 11 | fun List.qPeek(): T? = this.firstOrNull() 12 | 13 | fun Map.toParamString() = 14 | if (this.isEmpty()) "" 15 | else { 16 | this.map { 17 | val value = when (val p = it.value) { 18 | is Instant -> p.toString() 19 | null -> "" 20 | else -> p.toString() 21 | } 22 | "${it.key}=${value}" 23 | }.joinToString("&") 24 | } -------------------------------------------------------------------------------- /library/src/commonMain/kotlin/data/types/types.kt: -------------------------------------------------------------------------------- 1 | package data.types 2 | 3 | import arrow.core.Either 4 | import arrow.core.Option 5 | import arrow.core.left 6 | import data.Page 7 | import data.responses.ErrMessage 8 | import data.responses.toMessage 9 | 10 | typealias RemoteListData = Option>> 11 | typealias RemoteDataPaginated = Option>> 12 | typealias RemoteData = Option> 13 | 14 | fun RemoteData.flatten() = this.fold( 15 | { Err.NotExistsError.toMessage().left() }, 16 | { 17 | it 18 | } 19 | ) 20 | 21 | fun Option.flatten() = this.fold({ false }, { it }) -------------------------------------------------------------------------------- /library/src/androidMain/kotlin/utils/expected/crash-analytics.kt: -------------------------------------------------------------------------------- 1 | package utils.expected 2 | 3 | import android.content.Context 4 | import io.sentry.kotlin.multiplatform.OptionsConfiguration 5 | import io.sentry.kotlin.multiplatform.Sentry 6 | 7 | actual class CrashAnalytics(private val context: Context) { 8 | 9 | private fun optionsConfiguration(dsn: String): OptionsConfiguration = { 10 | it.dsn = dsn 11 | } 12 | 13 | actual fun init(dsn: String) { 14 | Sentry.init(context, optionsConfiguration(dsn)) 15 | } 16 | 17 | actual companion object { 18 | actual fun capture(throwable: Throwable) { 19 | Sentry.captureException(throwable) 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/modules/exampleModule/features/todo/todo.service.kt: -------------------------------------------------------------------------------- 1 | package modules.exampleModule.features.todo 2 | 3 | import arrow.core.toOption 4 | import data.responses.toMessage 5 | import data.types.RemoteListData 6 | import modules.exampleModule.features.todo.dto.TodoResponse 7 | 8 | interface TodoService { 9 | suspend fun getTodos(): RemoteListData 10 | } 11 | 12 | class TodoServiceImpl( 13 | private val todoRepository: TodoRepository 14 | ) : TodoService { 15 | 16 | override suspend fun getTodos(): RemoteListData = 17 | this.todoRepository.getTodos() 18 | .mapLeft { it.toMessage() } 19 | .toOption() 20 | 21 | } -------------------------------------------------------------------------------- /library/src/jvmMain/kotlin/utils/expected/datastore.koin-module.kt: -------------------------------------------------------------------------------- 1 | package utils.expected 2 | 3 | import androidx.datastore.core.DataStore 4 | import androidx.datastore.preferences.core.Preferences 5 | import org.koin.core.module.Module 6 | import org.koin.dsl.module 7 | import utils.createDataStore 8 | import utils.dataStoreFileName 9 | import java.io.File 10 | 11 | actual val datastoreModule: Module = module { 12 | single { dataStore() } 13 | } 14 | 15 | fun dataStore(): DataStore = createDataStore( 16 | producePath = { 17 | // Define the path to the local file 18 | val file = File("/tmp/", dataStoreFileName) 19 | file.absolutePath 20 | } 21 | ) 22 | -------------------------------------------------------------------------------- /library/src/commonMain/kotlin/modules/common/koin.lib.kt: -------------------------------------------------------------------------------- 1 | package modules.common 2 | 3 | import configs.ktorClient 4 | import org.koin.core.component.KoinComponent 5 | import org.koin.core.component.inject 6 | import org.koin.dsl.module 7 | import utils.expected.datastoreModule 8 | 9 | fun libModule(authCredentials: AuthCredentials) = module { 10 | 11 | single { ktorClient(authCredentials) } 12 | 13 | } + datastoreModule 14 | 15 | inline fun getKoinInstance(): T { 16 | return object : KoinComponent { 17 | val value: T by inject() 18 | }.value 19 | } 20 | 21 | data class AuthCredentials( 22 | val tokenUrl: String, 23 | val clientId: String, 24 | val clientSecret: String 25 | ) 26 | 27 | -------------------------------------------------------------------------------- /shared/src/androidMain/kotlin/utils/expected/analytics.android.kt: -------------------------------------------------------------------------------- 1 | package utils.expected 2 | 3 | import com.google.firebase.analytics.FirebaseAnalytics 4 | import com.google.firebase.analytics.ktx.analytics 5 | import com.google.firebase.analytics.logEvent 6 | import com.google.firebase.ktx.Firebase 7 | import utils.Tag 8 | 9 | actual object Analytics { 10 | private val fa = Firebase.analytics 11 | 12 | actual fun log(event: Tag.Event, data: Data) { 13 | fa.logEvent(event.name) { 14 | data.id?.let { param(FirebaseAnalytics.Param.ITEM_ID, it) } 15 | param(FirebaseAnalytics.Param.ITEM_NAME, data.name) 16 | param(FirebaseAnalytics.Param.CONTENT_TYPE, data.contentType.name) 17 | } 18 | } 19 | } 20 | 21 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/modules/exampleModule/features/todo/todo.vm.kt: -------------------------------------------------------------------------------- 1 | package modules.exampleModule.features.todo 2 | 3 | import arrow.core.none 4 | import cafe.adriel.voyager.core.model.ScreenModel 5 | import data.types.RemoteListData 6 | import kotlinx.coroutines.flow.MutableStateFlow 7 | import kotlinx.coroutines.flow.asStateFlow 8 | import modules.exampleModule.features.todo.dto.TodoResponse 9 | 10 | class TodoVM( 11 | private val todoService: TodoService 12 | ) : ScreenModel { 13 | private val _todos = MutableStateFlow>(none()) 14 | val todos = _todos.asStateFlow() 15 | 16 | 17 | suspend fun fetchTodos() { 18 | this._todos.value = this.todoService.getTodos() 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /shared/src/androidMain/kotlin/utils/expected/ui-utility.android.kt: -------------------------------------------------------------------------------- 1 | package utils.expected 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.net.Uri 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.platform.LocalContext 8 | import androidx.core.content.ContextCompat 9 | 10 | actual class RemoteUrl actual constructor() { 11 | var context: Context? = null 12 | 13 | @Composable 14 | actual fun create() { 15 | context = LocalContext.current 16 | } 17 | 18 | actual fun open(url: String) { 19 | val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) 20 | context?.let { 21 | ContextCompat.startActivity(it, intent, null) 22 | } 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "cognito-kmm-template" 2 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") 3 | 4 | pluginManagement { 5 | repositories { 6 | gradlePluginPortal() 7 | mavenCentral() 8 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 9 | google() 10 | } 11 | } 12 | 13 | plugins { 14 | id("org.gradle.toolchains.foojay-resolver-convention") version("0.4.0") 15 | } 16 | 17 | dependencyResolutionManagement { 18 | repositories { 19 | google() 20 | mavenCentral() 21 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 22 | } 23 | } 24 | 25 | 26 | include(":library") 27 | include(":shared") 28 | 29 | include(":exampleApp") -------------------------------------------------------------------------------- /examples/shared/example/screens/screen.def.kt: -------------------------------------------------------------------------------- 1 | package modules.exampleModule.screens 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.material.icons.outlined.Home 5 | import androidx.compose.material.icons.outlined.Menu 6 | import androidx.compose.material.icons.outlined.Task 7 | import androidx.compose.ui.graphics.vector.ImageVector 8 | import modules.common.views.screens.AppScreen 9 | 10 | sealed class Screens(val screen: AppScreen, val name: String, val iconRes: ImageVector) { 11 | 12 | data object Main : Screens(MainScreen, "HOME", Icons.Outlined.Home) 13 | data object Todo : Screens(MainScreen, "TODO", Icons.Outlined.Task) 14 | data object Settings : Screens(SettingsScreen, "SETTINGS", Icons.Outlined.Menu) 15 | } 16 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/modules/exampleModule/screens/screen.def.kt: -------------------------------------------------------------------------------- 1 | package modules.exampleModule.screens 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.material.icons.outlined.Home 5 | import androidx.compose.material.icons.outlined.Menu 6 | import androidx.compose.material.icons.outlined.Task 7 | import androidx.compose.ui.graphics.vector.ImageVector 8 | import modules.common.views.screens.AppScreen 9 | 10 | sealed class Screens(val screen: AppScreen, val name: String, val iconRes: ImageVector) { 11 | 12 | data object Main : Screens(MainScreen, "HOME", Icons.Outlined.Home) 13 | data object Todo : Screens(MainScreen, "TODO", Icons.Outlined.Task) 14 | data object Settings : Screens(SettingsScreen, "SETTINGS", Icons.Outlined.Menu) 15 | } 16 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/modules/common/views/components/expected/images.kt: -------------------------------------------------------------------------------- 1 | package modules.common.views.components.expected 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.Modifier 5 | import androidx.compose.ui.graphics.painter.Painter 6 | import androidx.compose.ui.layout.ContentScale 7 | 8 | @Composable 9 | expect fun ImageLoader( 10 | url: String, 11 | contentDescription: String? = null, 12 | modifier: Modifier = Modifier, 13 | placeholder: Painter? = null, 14 | scale: ContentScale = ContentScale.Crop, 15 | content: @Composable () -> Unit = {} 16 | ) 17 | 18 | expect class ImagePicker { 19 | @Composable 20 | fun registerPicker(onImagePicked: (ByteArray) -> Unit) 21 | 22 | fun pickImage() 23 | } 24 | 25 | @Composable 26 | expect fun createPicker(): ImagePicker -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/utils/expected/analytics.kt: -------------------------------------------------------------------------------- 1 | package utils.expected 2 | 3 | import utils.Tag 4 | 5 | expect object Analytics { 6 | fun log(event: Tag.Event, data: Data) 7 | } 8 | 9 | data class Data( 10 | val name: String, 11 | val id: String? = null, 12 | val contentType: CType = CType.Button, 13 | val extra: Map = mapOf() 14 | ) 15 | 16 | sealed class CType(val name: String) { 17 | data object Button : CType("Button") 18 | data object Label : CType("Label") 19 | data object Questionnaire : CType("Questionnaire") 20 | data object Screen : CType("Screen") 21 | data object Page : CType("Page") 22 | data object Pref : CType("Pref") 23 | data object Registration : CType("Registration") 24 | } 25 | 26 | fun anal(data: Data, event: Tag.Event = Tag.Event.Select) = Analytics.log(event, data) 27 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/modules/exampleModule/di/koin.example.kt: -------------------------------------------------------------------------------- 1 | package modules.exampleModule.di 2 | 3 | import modules.common.di.commonModule 4 | import modules.exampleModule.features.todo.TodoRepository 5 | import modules.exampleModule.features.todo.TodoRepositoryImpl 6 | import modules.exampleModule.features.todo.TodoService 7 | import modules.exampleModule.features.todo.TodoServiceImpl 8 | import modules.exampleModule.features.todo.TodoVM 9 | import org.koin.core.context.startKoin 10 | import org.koin.dsl.module 11 | 12 | val exampleModule = module { 13 | 14 | /* 15 | To Do 16 | */ 17 | single { TodoRepositoryImpl(get()) } 18 | single { TodoServiceImpl(get()) } 19 | 20 | factory { TodoVM(get()) } 21 | 22 | } 23 | 24 | val koinExampleModules = commonModule + exampleModule 25 | 26 | fun initKoin() = startKoin { modules(koinExampleModules) } -------------------------------------------------------------------------------- /library/src/linuxX64Main/kotlin/utils/expected/platform.util.kt: -------------------------------------------------------------------------------- 1 | package utils.expected 2 | 3 | import kotlin.random.Random 4 | 5 | 6 | actual fun uuid(): String = generateUuid() 7 | 8 | actual val isDebug = true 9 | 10 | actual fun platform(): Platforms = Platforms.JVM 11 | 12 | fun generateUuid(): String { 13 | // Generate random 128-bit value by combining two random 64-bit long values 14 | val mostSigBits = Random.nextLong() 15 | val leastSigBits = Random.nextLong() 16 | 17 | return "${digits(mostSigBits shr 32, 8)}-" + 18 | "${digits(mostSigBits shr 16, 4)}-" + 19 | "${digits(mostSigBits, 4)}-" + 20 | "${digits(leastSigBits shr 48, 4)}-" + 21 | digits(leastSigBits, 12) 22 | } 23 | 24 | // Helper function to format the UUID parts 25 | private fun digits(value: Long, digits: Int): String { 26 | val hexValue = value.toString(16) 27 | return hexValue.padStart(digits, '0') 28 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/modules/common/features/preferences/pref.model.kt: -------------------------------------------------------------------------------- 1 | package modules.common.features.preferences 2 | 3 | import androidx.compose.ui.graphics.Color 4 | import androidx.compose.ui.graphics.vector.ImageVector 5 | import androidx.datastore.preferences.core.Preferences 6 | import androidx.datastore.preferences.core.booleanPreferencesKey 7 | import androidx.datastore.preferences.core.stringPreferencesKey 8 | 9 | data class PrefItem( 10 | val title: String, 11 | val icon: ImageVector, 12 | val key: Preferences.Key, 13 | val value: T, 14 | val color: Color 15 | ) 16 | 17 | object PrefKeys { 18 | val theme = stringPreferencesKey("pref:app-theme") 19 | val dynamicColorScheme = booleanPreferencesKey("pref:dynamic-color-scheme") 20 | val firebaseTokenKey = stringPreferencesKey("firebase-token-key") 21 | val appIdentifierKey = stringPreferencesKey("app-identifier") 22 | } 23 | 24 | -------------------------------------------------------------------------------- /shared/src/androidMain/kotlin/modules/common/views/components/expected/sharable.kt: -------------------------------------------------------------------------------- 1 | package modules.common.views.components.expected 2 | 3 | import android.content.Intent 4 | import androidx.activity.ComponentActivity 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.runtime.remember 7 | import androidx.compose.ui.platform.LocalContext 8 | 9 | 10 | actual class ShareData( 11 | private val activity: ComponentActivity 12 | ) { 13 | actual fun shareContent(text: String) { 14 | val sendIntent: Intent = Intent().apply { 15 | action = Intent.ACTION_SEND 16 | putExtra(Intent.EXTRA_TEXT, text) 17 | type = "text/plain" 18 | } 19 | activity.startActivity(Intent.createChooser(sendIntent, null)) 20 | } 21 | } 22 | 23 | @Composable 24 | actual fun createShareSheet(): ShareData { 25 | val activity = LocalContext.current as ComponentActivity 26 | return remember(activity) { ShareData(activity) } 27 | } -------------------------------------------------------------------------------- /library/src/commonMain/kotlin/modules/common/models/error-responses.kt: -------------------------------------------------------------------------------- 1 | package modules.common.models 2 | 3 | import data.responses.ErrActions 4 | import kotlinx.datetime.Clock 5 | import kotlinx.datetime.Instant 6 | import kotlinx.serialization.Serializable 7 | 8 | @Serializable 9 | data class ErrResponse( 10 | val code: Int, 11 | val status: String, 12 | val message: String 13 | ) { 14 | override fun toString(): String = message 15 | } 16 | 17 | @Serializable 18 | data class ErrResponseV2( 19 | val type: ResponseType, 20 | val status: HttpStatus, 21 | val code: Int = status.value, 22 | val time: Instant = Clock.System.now(), 23 | val error: ErrData 24 | ) { 25 | override fun toString(): String = error.message 26 | } 27 | 28 | @Serializable 29 | data class ErrData( 30 | val type: String, 31 | val message: String, 32 | val status: HttpStatus, 33 | val description: String = "", 34 | val actions: Set = setOf(), 35 | ) -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/modules/common/features/auth/models/user.model.kt: -------------------------------------------------------------------------------- 1 | package modules.common.features.auth.models 2 | 3 | import kotlinx.datetime.Instant 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | data class UserBriefResponse( 9 | val id: Long = 0, 10 | 11 | @SerialName("created_at") 12 | val createdAt: Instant, 13 | 14 | @SerialName("updated_at") 15 | val updatedAt: Instant? = null, 16 | 17 | val name: String, 18 | 19 | val username: String, 20 | 21 | var phone: String? = null, 22 | 23 | var email: String? = null, 24 | 25 | val gender: Genders, 26 | 27 | val roles: List, 28 | 29 | val avatar: String? = null, 30 | 31 | val label: String, 32 | ) 33 | 34 | 35 | enum class Genders(@SerialName("label") val label: String) { 36 | MALE("Male"), 37 | FEMALE("Female"), 38 | OTHER("Other"), 39 | NOT_SPECIFIED("Not Specified") 40 | } 41 | 42 | -------------------------------------------------------------------------------- /shared/src/androidMain/kotlin/utils/expected/toast.android.kt: -------------------------------------------------------------------------------- 1 | package utils.expected 2 | 3 | import android.content.Context 4 | import android.widget.Toast 5 | import utils.Tag 6 | import utils.expected.Duration.Long.millisFromDuration 7 | import utils.logW 8 | 9 | actual class Toasty private constructor() { 10 | 11 | actual companion object { 12 | private lateinit var toast: Toast 13 | actual fun with(context: Any, resId: Int, duration: Duration): Companion { 14 | toast = Toast.makeText( 15 | context as Context, resId, when (millisFromDuration(duration)) { 16 | in 500..1000 -> Toast.LENGTH_SHORT 17 | else -> Toast.LENGTH_LONG 18 | } 19 | ) 20 | return this 21 | } 22 | 23 | actual fun show() = if (this::toast.isInitialized) 24 | this.toast.show() 25 | else { 26 | logW( 27 | Tag.Crash, 28 | "You should call Toast() with constructor parameters, otherwise it won't work!" 29 | ) 30 | } 31 | } 32 | 33 | 34 | } 35 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/modules/common/views/components/resource-components.kt: -------------------------------------------------------------------------------- 1 | package modules.common.views.components 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.Alignment 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.ui.graphics.ColorFilter 8 | import androidx.compose.ui.graphics.DefaultAlpha 9 | import androidx.compose.ui.graphics.painter.Painter 10 | import androidx.compose.ui.layout.ContentScale 11 | 12 | @Composable 13 | fun WImage( 14 | modifier: Modifier = Modifier, 15 | painter: Painter, 16 | contentDescription: String?, 17 | alignment: Alignment = Alignment.Center, 18 | contentScale: ContentScale = ContentScale.Fit, 19 | alpha: Float = DefaultAlpha, 20 | colorFilter: ColorFilter? = null 21 | ) { 22 | Image( 23 | painter = painter, 24 | contentDescription = contentDescription, 25 | modifier = modifier, 26 | alignment = alignment, 27 | contentScale = contentScale, 28 | alpha = alpha, 29 | colorFilter = colorFilter 30 | ) 31 | } -------------------------------------------------------------------------------- /shared/src/iosMain/kotlin/modules/common/views/components/expected/sharable.kt: -------------------------------------------------------------------------------- 1 | package modules.common.views.components.expected 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.remember 5 | import androidx.compose.ui.interop.LocalUIViewController 6 | import platform.UIKit.UIActivityViewController 7 | import platform.UIKit.UIApplication 8 | import platform.UIKit.UIViewController 9 | 10 | 11 | actual class ShareData( 12 | private val rootController: UIViewController 13 | ) { 14 | actual fun shareContent(text: String) { 15 | // Use iOS sharing functionality 16 | val activityItems = listOf(text) 17 | val activityController = UIActivityViewController(activityItems, null) 18 | val context = UIApplication.sharedApplication.keyWindow?.rootViewController 19 | context?.presentViewController(activityController, true, null) 20 | } 21 | } 22 | 23 | @Composable 24 | actual fun createShareSheet(): ShareData { 25 | val uiViewController = LocalUIViewController.current 26 | return remember { ShareData(uiViewController) } 27 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/modules/common/features/auth/auth.kt: -------------------------------------------------------------------------------- 1 | package modules.common.features.auth 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.collectAsState 5 | import arrow.core.Either 6 | import arrow.core.none 7 | import data.responses.ErrMessage 8 | import data.types.flatten 9 | import modules.common.features.auth.models.Auth 10 | 11 | @Composable 12 | fun authentication(authVM: AuthVM): Authentication { 13 | AuthComponent(authVM) 14 | val auth = authVM.getAuth().collectAsState(none()).value 15 | return Authentication( 16 | authVM, 17 | auth.flatten(), 18 | auth.fold( 19 | { false }, 20 | { it.isRight() } 21 | )) 22 | } 23 | 24 | data class Authentication( 25 | val authVM: AuthVM, 26 | val auth: Either, 27 | val authenticated: Boolean 28 | ) { 29 | fun require(block: () -> Unit = {}) = if (authenticated) { 30 | block() 31 | } else { 32 | authVM.trigger(block = block) 33 | } 34 | 35 | fun matches(userId: Long) = auth.fold( 36 | { false }, 37 | { it.id == userId } 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/modules/exampleModule/features/todo/todo.repository.kt: -------------------------------------------------------------------------------- 1 | package modules.exampleModule.features.todo 2 | 3 | import arrow.core.Either 4 | import data.types.Err 5 | import io.ktor.client.HttpClient 6 | import io.ktor.client.call.body 7 | import io.ktor.client.request.get 8 | import io.ktor.client.request.url 9 | import io.ktor.http.ContentType 10 | import io.ktor.http.contentType 11 | import modules.common.models.ErrResponse 12 | import modules.exampleModule.features.todo.dto.TodoResponse 13 | import modules.exampleModule.routes.ExampleRoutes 14 | import utils.result 15 | 16 | interface TodoRepository { 17 | suspend fun getTodos(): Either, List> 18 | } 19 | 20 | class TodoRepositoryImpl( 21 | private val httpClient: HttpClient 22 | ) : TodoRepository { 23 | 24 | override suspend fun getTodos(): Either, List> = result { 25 | this.httpClient.get { 26 | url(ExampleRoutes.Todo.GET_TODO_ITEMS) 27 | contentType(ContentType.Application.Json) 28 | }.body() 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /library/src/iosMain/kotlin/utils/expected/datastore.koin-module.kt: -------------------------------------------------------------------------------- 1 | package utils.expected 2 | 3 | import androidx.datastore.core.DataStore 4 | import androidx.datastore.preferences.core.Preferences 5 | import kotlinx.cinterop.ExperimentalForeignApi 6 | import org.koin.core.module.Module 7 | import org.koin.dsl.module 8 | import platform.Foundation.NSDocumentDirectory 9 | import platform.Foundation.NSFileManager 10 | import platform.Foundation.NSURL 11 | import platform.Foundation.NSUserDomainMask 12 | import utils.createDataStore 13 | import utils.dataStoreFileName 14 | 15 | actual val datastoreModule: Module = module { 16 | single { dataStore() } 17 | } 18 | 19 | @OptIn(ExperimentalForeignApi::class) 20 | fun dataStore(): DataStore = createDataStore( 21 | producePath = { 22 | val documentDirectory: NSURL? = NSFileManager.defaultManager.URLForDirectory( 23 | directory = NSDocumentDirectory, 24 | inDomain = NSUserDomainMask, 25 | appropriateForURL = null, 26 | create = false, 27 | error = null, 28 | ) 29 | requireNotNull(documentDirectory).path + "/$dataStoreFileName" 30 | } 31 | ) 32 | -------------------------------------------------------------------------------- /shared/src/commonMain/composeResources/drawable/box.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/modules/common/features/notifications/models/notifications.kt: -------------------------------------------------------------------------------- 1 | package modules.common.features.notifications.models 2 | 3 | import kotlinx.serialization.SerialName 4 | 5 | data class NChannel( 6 | val id: Long, 7 | 8 | @SerialName("channel_id") 9 | val channelId: String, 10 | 11 | @SerialName("name") 12 | val name: String, 13 | 14 | @SerialName("description") 15 | val description: String, 16 | 17 | @SerialName("priority") 18 | val priority: Int, 19 | 20 | @SerialName("topics") 21 | val topics: Set 22 | ) 23 | 24 | data class Notification( 25 | val title: String, 26 | val body: String, 27 | val image: String? 28 | ) 29 | 30 | data class NotificationData( 31 | val notificationId: Int, 32 | val referenceId: Long?, 33 | val referenceId2: Long?, 34 | val navigable: Boolean, 35 | val intentAction: String?, 36 | val channelId: String, 37 | ) 38 | 39 | enum class NDataKeys( 40 | val key: String 41 | ) { 42 | NOTIFICATION_ID("notification_id"), 43 | REFERENCE_ID("reference_id"), 44 | REFERENCE_ID_2("reference_id_2"), 45 | NAVIGABLE("navigable"), 46 | CHANNEL_ID("channel_id"), 47 | INTENT_ACTION("intent_action"), 48 | VAL_OPEN_POLL_DETAILS_PAGE("open_poll_details_page"), 49 | VAL_OPEN_KOLLYAN_MAIN("open_kollyan_main"), 50 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/configs/Credentials.kt: -------------------------------------------------------------------------------- 1 | package configs 2 | 3 | import utils.expected.isDebug 4 | 5 | sealed interface Credentials { 6 | data object Sentry : Credentials { 7 | // TODO: Update sentry credentials 8 | val dsn = Credential( 9 | debug = "https://invalidf46ba474d9invalid57269f51@o4505135996272640.ingest.sentry.io/450513599invalid", 10 | release = "https://invalidf46ba474d9539eac057269f51@invalid5996272640.ingest.sentry.io/450513599invalid" 11 | ) 12 | } 13 | 14 | data object Auth : Credentials { 15 | val clientId = Credential( 16 | debug = "client_id", 17 | release = "client_id" 18 | ) 19 | val clientSecret = Credential( 20 | debug = "client_secret", 21 | release = "client_secret" 22 | ) 23 | } 24 | 25 | data object Ads { 26 | // ad credentials 27 | val adUnitInterstitialPB = Credential( 28 | debug = "", 29 | release = "" 30 | ) 31 | 32 | } 33 | 34 | data object UserPass { 35 | val username = Credential( 36 | debug = "", 37 | release = "" 38 | ) 39 | val password = Credential( 40 | debug = "", 41 | release = "" 42 | ) 43 | } 44 | 45 | data class Credential(val debug: String, val release: String) { 46 | override fun toString() = get() 47 | fun get() = if (isDebug) debug else release 48 | } 49 | } 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /library/src/androidMain/kotlin/utils/expected/platform.util.kt: -------------------------------------------------------------------------------- 1 | package utils.expected 2 | 3 | import android.os.Build 4 | import android.os.Debug 5 | import org.cognitox.clientlib.BuildConfig 6 | import java.util.UUID 7 | 8 | 9 | actual fun uuid(): String = UUID.randomUUID().toString() 10 | 11 | actual val isDebug = BuildConfig.DEBUG || isEmulator 12 | 13 | private val isEmulator: Boolean 14 | get() = (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic")) 15 | || Build.FINGERPRINT.startsWith("generic") 16 | || Build.FINGERPRINT.startsWith("unknown") 17 | || Build.HARDWARE.contains("goldfish") 18 | || Build.HARDWARE.contains("ranchu") 19 | || Build.MODEL.contains("google_sdk") 20 | || Build.MODEL.contains("Emulator") 21 | || Build.MODEL.contains("Android SDK built for x86") 22 | || Build.MANUFACTURER.contains("Genymotion") 23 | || Build.PRODUCT.contains("sdk_google") 24 | || Build.PRODUCT.contains("google_sdk") 25 | || Build.PRODUCT.contains("sdk") 26 | || Build.PRODUCT.contains("sdk_x86") 27 | || Build.PRODUCT.contains("sdk_gphone64_arm64") 28 | || Build.PRODUCT.contains("vbox86p") 29 | || Build.PRODUCT.contains("emulator") 30 | || Build.PRODUCT.contains("simulator") 31 | || Debug.isDebuggerConnected() 32 | 33 | 34 | actual fun platform(): Platforms = Platforms.ANDROID 35 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/modules/common/animations/transition-animations.kt: -------------------------------------------------------------------------------- 1 | package modules.common.animations 2 | 3 | import androidx.compose.animation.AnimatedVisibility 4 | import androidx.compose.animation.AnimatedVisibilityScope 5 | import androidx.compose.animation.EnterTransition 6 | import androidx.compose.animation.ExitTransition 7 | import androidx.compose.animation.core.MutableTransitionState 8 | import androidx.compose.animation.fadeIn 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.platform.LocalDensity 12 | 13 | @Composable 14 | fun EnterAnimation(content: @Composable () -> Unit) { 15 | AnimatedVisibility( 16 | visibleState = MutableTransitionState( 17 | initialState = false 18 | ).apply { targetState = true }, 19 | modifier = Modifier, 20 | enter = Transition.FadeInOut(1000).enter, 21 | exit = Transition.FadeInOut(500).exit, 22 | ) { 23 | content() 24 | } 25 | } 26 | 27 | @Composable 28 | fun LoadingAnimation( 29 | visible: Boolean, 30 | modifier: Modifier = Modifier, 31 | content: @Composable() AnimatedVisibilityScope.() -> Unit 32 | ) = 33 | AnimatedVisibility( 34 | visible = visible, 35 | modifier = Modifier.then(modifier), 36 | enter = Transition.FadeInOut().enter, 37 | exit = Transition.FadeInOut().exit, 38 | content = content 39 | ) -------------------------------------------------------------------------------- /library/src/linuxX64Main/kotlin/utils/expected/datastore.koin-module.kt: -------------------------------------------------------------------------------- 1 | package utils.expected 2 | 3 | import androidx.datastore.core.DataStore 4 | import androidx.datastore.preferences.core.Preferences 5 | import io.ktor.http.encodeURLPath 6 | import kotlinx.cinterop.ExperimentalForeignApi 7 | import kotlinx.cinterop.refTo 8 | import kotlinx.cinterop.toKString 9 | import org.koin.core.module.Module 10 | import org.koin.dsl.module 11 | import platform.posix.fclose 12 | import platform.posix.fgets 13 | import platform.posix.fopen 14 | import utils.createDataStore 15 | import utils.dataStoreFileName 16 | 17 | actual val datastoreModule: Module = module { 18 | single { dataStore() } 19 | } 20 | 21 | fun dataStore(): DataStore = createDataStore( 22 | producePath = { 23 | // Define the path to the local file 24 | val file = readFile("/tmp/" + dataStoreFileName) 25 | file.encodeURLPath() 26 | } 27 | ) 28 | 29 | @OptIn(ExperimentalForeignApi::class) 30 | fun readFile(filePath: String): String { 31 | val file = fopen(filePath, "r") ?: throw IllegalArgumentException("Cannot open file: $filePath") 32 | try { 33 | val buffer = StringBuilder() 34 | val line = ByteArray(1024) 35 | while (fgets(line.refTo(0), line.size, file) != null) { 36 | buffer.append(line.toKString()) 37 | } 38 | return buffer.toString() 39 | } finally { 40 | fclose(file) 41 | } 42 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/modules/common/features/events/models/event.model.kt: -------------------------------------------------------------------------------- 1 | package modules.common.features.events.models 2 | 3 | import kotlinx.datetime.Clock 4 | import kotlinx.datetime.Instant 5 | import kotlinx.serialization.SerialName 6 | import kotlinx.serialization.Serializable 7 | 8 | @Serializable 9 | data class TaskEventReq( 10 | 11 | @SerialName("start_at") 12 | val startAt: Instant, 13 | 14 | @SerialName("end_at") 15 | val endAt: Instant? 16 | 17 | ) 18 | 19 | @Serializable 20 | data class EventBrief( 21 | val id: Long, 22 | 23 | @SerialName("created_at") 24 | val createdAt: Instant, 25 | 26 | @SerialName("updated_at") 27 | val updatedAt: Instant? = null, 28 | 29 | val title: String, 30 | 31 | @SerialName("ref_id") 32 | val refId: Long?, 33 | 34 | val image: String?, 35 | 36 | val type: EventTypes, 37 | 38 | val active: Boolean, 39 | 40 | @SerialName("start_at") 41 | val startAt: Instant, 42 | 43 | @SerialName("end_at") 44 | val endAt: Instant?, 45 | 46 | val repetitive: Boolean, 47 | 48 | @SerialName("repeat_interval") 49 | val repeatInterval: Long, 50 | 51 | @SerialName("repeat_count") 52 | val repeatCount: Int, 53 | 54 | @SerialName("user_id") 55 | val userId: Long 56 | ){ 57 | val isExpired = !this.active 58 | || this.endAt?.let { it 24 | 25 | Column( 26 | modifier = Modifier.padding(paddingValues) 27 | .fillMaxSize() 28 | .padding(Paddings.Screen.horizontal), 29 | verticalArrangement = Arrangement.Center, 30 | horizontalAlignment = Alignment.CenterHorizontally 31 | ) { 32 | Text( 33 | modifier = Modifier.clickable { 34 | snackbar.show("Unbelievable! You've successfully clicked me.") 35 | }, 36 | textAlign = TextAlign.Center, 37 | text = "Click Me" 38 | ) 39 | } 40 | } 41 | } 42 | 43 | } 44 | 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Quickstart: Getting Started 2 | ![Promo Image](https://cognitox.gitbook.io/~gitbook/image?url=https%3A%2F%2F3790227239-files.gitbook.io%2F%7E%2Ffiles%2Fv0%2Fb%2Fgitbook-x-prod.appspot.com%2Fo%2Fspaces%252FowzMuJgX1sZF5Ebeh2BD%252Fuploads%252FjcpCOp4gskG7E22LRSaj%252Ffeature%2520promo.png%3Falt%3Dmedia%26token%3Dfb28d556-8f4a-49cf-b3e2-77bf70bc3cd5&width=768&dpr=2&quality=100&sign=46c577d0&sv=1) 3 | 4 | ## Getting Started with this project 5 | 6 | This quickstart guide provides a brief overview of the essential steps to get started with this project. For more detailed information, refer to the linked documentation pages. 7 | 8 | ## Key Steps: 9 | 10 | ### [Prerequisites:](https://cognitox.gitbook.io/cognitox-docs/getting-started/prerequisites) 11 | 12 | Ensure you have the necessary tools and dependencies installed. 13 | 14 | ### [Installation:](https://cognitox.gitbook.io/cognitox-docs/getting-started/installation) 15 | 16 | Follow the instructions to set up your KMM development environment. 17 | 18 | ### [Creating a New App:](https://cognitox.gitbook.io/cognitox-docs/getting-started/creating-new-app) 19 | 20 | Learn how to quickly generate a new app module within your project. 21 | 22 | ### [Project Architecture:](https://cognitox.gitbook.io/cognitox-docs/architecture/editor) 23 | 24 | Familiarize yourself with the project structure and how different modules interact. 25 | 26 | 27 | ## [Documentation](https://cognitox.gitbook.io/cognitox-docs) 28 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/modules/common/routes/route.kt: -------------------------------------------------------------------------------- 1 | package modules.common.routes 2 | 3 | import utils.expected.isDebug 4 | 5 | val BASE_URL = if (isDebug) { 6 | "https://dev.example.com" 7 | } else { 8 | "https://api.example.com" 9 | } 10 | 11 | val SOCKET_CONNECT = if (isDebug) { 12 | "wss://dev.example.com/connect" 13 | } else { 14 | "wss://api.example.com/connect" 15 | } 16 | 17 | 18 | object SocketRoutes { 19 | private const val MESSAGES = "/messages" 20 | fun queue(sessionId: String, name: String? = null) = 21 | name?.let { "/user/$sessionId/queue/$name" } ?: "/user/$sessionId/queue/default" 22 | 23 | object V1 { 24 | /* 25 | Send/Destinations 26 | */ 27 | const val SEND_PING = "$MESSAGES/ping" 28 | 29 | /* 30 | Topics 31 | */ 32 | const val TOPIC_PING = "/ping" 33 | 34 | fun queuePing(sessionId: String) = queue(sessionId, "ping") 35 | } 36 | } 37 | 38 | object Routes { 39 | 40 | private const val API_VERSION = "/api/v1" 41 | private const val API_VERSION_V2 = "/api/v2" 42 | 43 | // Auth 44 | val GET_TOKEN = "$BASE_URL/oauth/token" 45 | fun getOTP(phoneOrEmail: String) = 46 | "$BASE_URL$API_VERSION/register/verify?identity=$phoneOrEmail" 47 | 48 | fun checkUsername(username: String) = 49 | "$BASE_URL$API_VERSION/public/register/check-username?username=$username" 50 | 51 | fun signup(token: String) = "$BASE_URL$API_VERSION/register?token=$token" 52 | fun registerFirebaseToken() = "$BASE_URL$API_VERSION/firebase/token" 53 | 54 | 55 | } 56 | -------------------------------------------------------------------------------- /exampleApp/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 13 | 16 | 17 | 18 | 21 | 22 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /examples/exampleApp/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 13 | 16 | 17 | 18 | 21 | 22 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/modules/common/di/koin.common.kt: -------------------------------------------------------------------------------- 1 | package modules.common.di 2 | 3 | import configs.Credentials 4 | import modules.common.AuthCredentials 5 | import modules.common.features.auth.AuthRepository 6 | import modules.common.features.auth.AuthRepositoryImpl 7 | import modules.common.features.auth.AuthService 8 | import modules.common.features.auth.AuthServiceImpl 9 | import modules.common.features.auth.AuthVM 10 | import modules.common.features.preferences.PrefVM 11 | import modules.common.libModule 12 | import modules.common.routes.Routes 13 | import org.koin.core.component.KoinComponent 14 | import org.koin.core.component.inject 15 | import org.koin.dsl.module 16 | 17 | val commonModule = module { 18 | 19 | /* 20 | Auth Components 21 | */ 22 | single { AuthRepositoryImpl(get()) } 23 | single { AuthServiceImpl(get()) } 24 | // AuthVM needs to be singleton because auth is used in every screens and ViewModel 25 | // needs to survive as long as app is running on the foreground, so the auth state isn't lost. 26 | single { AuthVM(get(), get(), get()) } 27 | 28 | /* 29 | Preference/Settings Components 30 | */ 31 | factory { PrefVM(get()) } 32 | 33 | } + libModule( 34 | AuthCredentials( 35 | tokenUrl = Routes.GET_TOKEN, 36 | clientId = Credentials.Auth.clientId.get(), 37 | clientSecret = Credentials.Auth.clientSecret.get() 38 | ) 39 | ) 40 | 41 | inline fun getKoinInstance(): T { 42 | return object : KoinComponent { 43 | val value: T by inject() 44 | }.value 45 | } 46 | 47 | 48 | -------------------------------------------------------------------------------- /scripts/gen.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function execTool() { 4 | java -jar scripts/tools/gentool.jar "$1" "$2" "$srcDir" "$3" 5 | } 6 | 7 | if [ "$1" = "crud" ]; then 8 | srcDir="examples/crudexample" 9 | execTool "$1" "$2" "." 10 | elif [ "$1" = "module" ]; then 11 | srcDir="examples/exampleApp" 12 | execTool "$1" "$2" "./" 13 | mv "./$2" "$2App" 14 | 15 | srcDir="examples/exampleIOS" 16 | execTool "$1" "$2" "./" 17 | mv "$2" "./$2IOS" 18 | 19 | srcDir="examples/shared/example" 20 | execTool "$1" "$2" "shared/src/commonMain/kotlin/modules" 21 | mv shared/src/commonMain/kotlin/modules/"$2" shared/src/commonMain/kotlin/modules/"$2"Module 22 | 23 | srcDir="examples/shared/exampleAndroid" 24 | execTool "$1" "$2" "shared/src/androidMain/kotlin/modules" 25 | mv shared/src/androidMain/kotlin/modules/"$2" shared/src/androidMain/kotlin/modules/"$2"Module 26 | 27 | srcDir="examples/shared/exampleIOS" 28 | execTool "$1" "$2" "shared/src/iosMain/kotlin/modules" 29 | mv shared/src/iosMain/kotlin/modules/"$2" shared/src/iosMain/kotlin/modules/"$2"Module 30 | cp examples/exampleIOS/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png "$2"IOS/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png 31 | 32 | echo -e "\ninclude(\":$2App\")" >> ./settings.gradle.kts 33 | 34 | elif [ "$1" = "migration" ]; then 35 | echo "-- $(date "+%b %d, %Y")" >"app/src/main/resources/db/migration/V$(date +%s)__$2.sql" 36 | echo "Migration Created!" 37 | else 38 | echo "Must specify type of generated asset. i.e. crud | module | migration" 39 | fi 40 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/modules/common/views/components/charts.kt: -------------------------------------------------------------------------------- 1 | package modules.common.views.components 2 | 3 | import androidx.compose.foundation.layout.fillMaxSize 4 | import androidx.compose.material3.Card 5 | import androidx.compose.material3.CardDefaults 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.text.TextStyle 10 | import androidx.compose.ui.text.font.FontWeight 11 | import com.aay.compose.baseComponents.model.LegendPosition 12 | import com.aay.compose.donutChart.PieChart 13 | import com.aay.compose.donutChart.model.PieChartData 14 | 15 | @Composable 16 | fun PiChart( 17 | modifier: Modifier, 18 | data: List 19 | ) { 20 | if (data.sumOf { it.data } <= 0) return 21 | Card( 22 | modifier = modifier, 23 | colors = CardDefaults.cardColors( 24 | containerColor = MaterialTheme.colorScheme.surfaceVariant 25 | ) 26 | ) { 27 | PieChart( 28 | modifier = Modifier.fillMaxSize(), 29 | pieChartData = data, 30 | ratioLineColor = MaterialTheme.colorScheme.surfaceVariant, 31 | outerCircularColor = MaterialTheme.colorScheme.surfaceVariant, 32 | textRatioStyle = TextStyle(color = MaterialTheme.colorScheme.onSurfaceVariant), 33 | legendPosition = LegendPosition.BOTTOM, 34 | descriptionStyle = TextStyle( 35 | color = MaterialTheme.colorScheme.onSurfaceVariant, 36 | fontSize = MaterialTheme.typography.labelLarge.fontSize, 37 | fontWeight = FontWeight.Bold 38 | ), 39 | ) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /library/src/commonMain/kotlin/data/pagination.kt: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | import kotlinx.serialization.Serializable 4 | import utils.toParamString 5 | 6 | @Serializable 7 | data class Page( 8 | val content: List, 9 | val totalElements: Long, 10 | val last: Boolean, 11 | val totalPages: Int, 12 | val sort: Sort, 13 | val numberOfElements: Long, 14 | val first: Boolean, 15 | val size: Int, 16 | val number: Long, 17 | val empty: Boolean, 18 | ) { 19 | companion object { 20 | fun of(content: List) = 21 | Page( 22 | content = content, 23 | totalElements = 0, 24 | last = true, 25 | totalPages = 0, 26 | sort = Sort(false, true, false), 27 | numberOfElements = 0L, 28 | first = true, 29 | size = 10, 30 | number = 0, 31 | empty = false 32 | ) 33 | } 34 | } 35 | 36 | @Serializable 37 | data class Sort( 38 | val sorted: Boolean, 39 | val unsorted: Boolean, 40 | val empty: Boolean, 41 | ) 42 | 43 | data class PageableParams( 44 | val query: String? = null, 45 | val page: Long = 0, 46 | val size: Int = 10, 47 | val sortBy: SortByFields = SortByFields.ID, 48 | val direction: SortDirections = SortDirections.DESC 49 | ){ 50 | fun toParamString() = mapOf( 51 | "q" to if (this.query == null) "" else this.query.toString(), 52 | "page" to this.page, 53 | "size" to this.size, 54 | "sort_by" to this.sortBy.name, 55 | "sort_direction" to this.direction.name 56 | ).toParamString() 57 | } 58 | 59 | enum class SortByFields { 60 | ID, CREATED_AT, SERIAL; 61 | } 62 | 63 | enum class SortDirections { 64 | ASC, DESC 65 | } 66 | 67 | -------------------------------------------------------------------------------- /examples/shared/exampleAndroid/theme/theme.example.kt: -------------------------------------------------------------------------------- 1 | package modules.exampleModule.theme 2 | 3 | import android.app.Activity 4 | import android.os.Build 5 | import androidx.compose.material3.MaterialTheme 6 | import androidx.compose.material3.dynamicDarkColorScheme 7 | import androidx.compose.material3.dynamicLightColorScheme 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.SideEffect 10 | import androidx.compose.ui.graphics.toArgb 11 | import androidx.compose.ui.platform.LocalContext 12 | import androidx.compose.ui.platform.LocalView 13 | import androidx.core.view.WindowCompat 14 | 15 | @Composable 16 | actual fun ExampleAppTheme( 17 | useDarkTheme: Boolean, 18 | dynamicColorScheme: Boolean, 19 | content: @Composable () -> Unit 20 | ) { 21 | val context = LocalContext.current 22 | val colors = when { 23 | dynamicColorScheme && (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) -> { 24 | if (useDarkTheme) dynamicDarkColorScheme(context) 25 | else dynamicLightColorScheme(context) 26 | } 27 | 28 | useDarkTheme -> darkColors 29 | else -> lightColors 30 | } 31 | 32 | // Add primary status bar color from chosen color scheme. 33 | val view = LocalView.current 34 | if (!view.isInEditMode) { 35 | SideEffect { 36 | val window = (view.context as Activity).window 37 | window.statusBarColor = colors.primary.toArgb() 38 | WindowCompat 39 | .getInsetsController(window, view) 40 | .isAppearanceLightStatusBars = useDarkTheme 41 | } 42 | } 43 | 44 | MaterialTheme( 45 | colorScheme = colors, 46 | shapes = shapes, 47 | typography = typography, 48 | content = content, 49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /shared/src/androidMain/kotlin/modules/exampleModule/theme/theme.example.kt: -------------------------------------------------------------------------------- 1 | package modules.exampleModule.theme 2 | 3 | import android.app.Activity 4 | import android.os.Build 5 | import androidx.compose.material3.MaterialTheme 6 | import androidx.compose.material3.dynamicDarkColorScheme 7 | import androidx.compose.material3.dynamicLightColorScheme 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.SideEffect 10 | import androidx.compose.ui.graphics.toArgb 11 | import androidx.compose.ui.platform.LocalContext 12 | import androidx.compose.ui.platform.LocalView 13 | import androidx.core.view.WindowCompat 14 | 15 | @Composable 16 | actual fun ExampleAppTheme( 17 | useDarkTheme: Boolean, 18 | dynamicColorScheme: Boolean, 19 | content: @Composable () -> Unit 20 | ) { 21 | val context = LocalContext.current 22 | val colors = when { 23 | dynamicColorScheme && (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) -> { 24 | if (useDarkTheme) dynamicDarkColorScheme(context) 25 | else dynamicLightColorScheme(context) 26 | } 27 | 28 | useDarkTheme -> darkColors 29 | else -> lightColors 30 | } 31 | 32 | // Add primary status bar color from chosen color scheme. 33 | val view = LocalView.current 34 | if (!view.isInEditMode) { 35 | SideEffect { 36 | val window = (view.context as Activity).window 37 | window.statusBarColor = colors.primary.toArgb() 38 | WindowCompat 39 | .getInsetsController(window, view) 40 | .isAppearanceLightStatusBars = useDarkTheme 41 | } 42 | } 43 | 44 | MaterialTheme( 45 | colorScheme = colors, 46 | shapes = shapes, 47 | typography = typography, 48 | content = content, 49 | ) 50 | } 51 | -------------------------------------------------------------------------------- /exampleIOS/iosApp/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | CADisableMinimumFrameDurationOnPhone 24 | 25 | UIApplicationSceneManifest 26 | 27 | UIApplicationSupportsMultipleScenes 28 | 29 | 30 | UILaunchScreen 31 | 32 | UIRequiredDeviceCapabilities 33 | 34 | armv7 35 | 36 | UISupportedInterfaceOrientations 37 | 38 | UIInterfaceOrientationPortrait 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UISupportedInterfaceOrientations~ipad 43 | 44 | UIInterfaceOrientationPortrait 45 | UIInterfaceOrientationPortraitUpsideDown 46 | UIInterfaceOrientationLandscapeLeft 47 | UIInterfaceOrientationLandscapeRight 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /examples/exampleIOS/iosApp/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | CADisableMinimumFrameDurationOnPhone 24 | 25 | UIApplicationSceneManifest 26 | 27 | UIApplicationSupportsMultipleScenes 28 | 29 | 30 | UILaunchScreen 31 | 32 | UIRequiredDeviceCapabilities 33 | 34 | armv7 35 | 36 | UISupportedInterfaceOrientations 37 | 38 | UIInterfaceOrientationPortrait 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UISupportedInterfaceOrientations~ipad 43 | 44 | UIInterfaceOrientationPortrait 45 | UIInterfaceOrientationPortraitUpsideDown 46 | UIInterfaceOrientationLandscapeLeft 47 | UIInterfaceOrientationLandscapeRight 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /shared/shared.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | spec.name = 'shared' 3 | spec.version = '1.0.0' 4 | spec.homepage = 'Link to the Shared Module homepage' 5 | spec.source = { :http=> ''} 6 | spec.authors = '' 7 | spec.license = '' 8 | spec.summary = 'Some description for the Shared Module' 9 | spec.vendored_frameworks = 'build/cocoapods/framework/shared.framework' 10 | spec.libraries = 'c++' 11 | spec.ios.deployment_target = '14.1' 12 | 13 | 14 | spec.pod_target_xcconfig = { 15 | 'KOTLIN_PROJECT_PATH' => ':shared', 16 | 'PRODUCT_MODULE_NAME' => 'shared', 17 | } 18 | 19 | spec.script_phases = [ 20 | { 21 | :name => 'Build shared', 22 | :execution_position => :before_compile, 23 | :shell_path => '/bin/sh', 24 | :script => <<-SCRIPT 25 | if [ "YES" = "$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED" ]; then 26 | echo "Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \"YES\"" 27 | exit 0 28 | fi 29 | set -ev 30 | REPO_ROOT="$PODS_TARGET_SRCROOT" 31 | "$REPO_ROOT/../gradlew" -p "$REPO_ROOT" $KOTLIN_PROJECT_PATH:syncFramework \ 32 | -Pkotlin.native.cocoapods.platform=$PLATFORM_NAME \ 33 | -Pkotlin.native.cocoapods.archs="$ARCHS" \ 34 | -Pkotlin.native.cocoapods.configuration="$CONFIGURATION" 35 | SCRIPT 36 | } 37 | ] 38 | spec.resources = ['src/commonMain/resources/**', 'src/iosMain/resources/**'] 39 | end -------------------------------------------------------------------------------- /exampleApp/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /examples/exampleApp/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/modules/exampleModule/screens/home.screen.kt: -------------------------------------------------------------------------------- 1 | package modules.exampleModule.screens 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.runtime.LaunchedEffect 7 | import androidx.compose.runtime.collectAsState 8 | import androidx.compose.ui.Modifier 9 | import cafe.adriel.voyager.koin.koinScreenModel 10 | import modules.common.features.auth.AuthVM 11 | import modules.common.features.auth.authentication 12 | import modules.common.views.components.LazyColumnWithLoadingForList 13 | import modules.common.views.dimensions.Paddings 14 | import modules.common.views.screens.AppScreen 15 | import modules.exampleModule.features.todo.TodoCategoryView 16 | import modules.exampleModule.features.todo.TodoItemView 17 | import modules.exampleModule.features.todo.TodoVM 18 | import modules.exampleModule.screens.layouts.ExampleAppLayout 19 | import utils.show 20 | 21 | object MainScreen : AppScreen { 22 | 23 | @Composable 24 | override fun Content() { 25 | val todoVM = koinScreenModel() 26 | val authVM = koinScreenModel() 27 | 28 | ExampleAppLayout { paddingValues, snackbar -> 29 | 30 | LaunchedEffect(Unit) { 31 | todoVM.fetchTodos() 32 | } 33 | 34 | val auth = authentication(authVM) 35 | 36 | Column( 37 | modifier = Modifier.padding(paddingValues) 38 | .padding(Paddings.Screen.horizontal), 39 | ) { 40 | TodoCategoryView() 41 | 42 | LazyColumnWithLoadingForList( 43 | remoteData = todoVM.todos.collectAsState().value, 44 | itemView = { 45 | TodoItemView( 46 | todo = it, 47 | itemClick = { 48 | auth.require { 49 | snackbar.show("Hi, you clicked #${it.id}") 50 | } 51 | } 52 | ) 53 | } 54 | ) 55 | } 56 | } 57 | } 58 | 59 | } 60 | 61 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/modules/common/views/dimensions/size.kt: -------------------------------------------------------------------------------- 1 | package modules.common.views.dimensions 2 | 3 | import androidx.compose.foundation.layout.Spacer 4 | import androidx.compose.foundation.layout.height 5 | import androidx.compose.foundation.layout.width 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.unit.dp 9 | 10 | @Composable 11 | fun HorizontalSpacer() = Spacer(modifier = Modifier.width(Paddings.General.spacerWidth)) 12 | 13 | @Composable 14 | fun VerticalSpacer() = Spacer(modifier = Modifier.height(Paddings.General.spacerWidth)) 15 | 16 | object Paddings { 17 | object Internal { 18 | val innerPadding = 10.dp 19 | 20 | object SmallObjects { 21 | val vertical = 5.dp 22 | val horizontal = 5.dp 23 | val tiny = 2.dp 24 | } 25 | } 26 | 27 | object Grid { 28 | val top = 24.dp 29 | val bottom = 24.dp 30 | val bottomWithButton = 50.dp 31 | val horizontalSpacing = 16.dp 32 | val verticalSpacing = 16.dp 33 | } 34 | 35 | object Card { 36 | val horizontal = 16.dp 37 | val vertical = 20.dp 38 | val horizontalLarge = 24.dp 39 | val verticalLarge = 24.dp 40 | val verticalLargeWithButtons = 50.dp 41 | val spacerHeight = 16.dp 42 | val internalHorizontal = 5.dp 43 | val internalVertical = 8.dp 44 | } 45 | 46 | object General { 47 | val surround = 16.dp 48 | val spacerHeightSmall = 5.dp 49 | val spacerHeight = 10.dp 50 | val spacerWidth = 10.dp 51 | val buttonSpacerWidth = 20.dp 52 | } 53 | 54 | object Image { 55 | val surround = 5.dp 56 | } 57 | 58 | object Screen { 59 | val horizontal = 10.dp 60 | val vertical = 0.dp 61 | } 62 | 63 | object Quiz { 64 | object Header { 65 | val horizontal = 16.dp 66 | val vertical = 10.dp 67 | } 68 | 69 | object Buttons { 70 | val horizontal = 0.dp 71 | val vertical = 20.dp 72 | } 73 | } 74 | 75 | object Dialogs { 76 | val surround = 30.dp 77 | 78 | object Buttons { 79 | val horizontal = 16.dp 80 | val vertical = 5.dp 81 | } 82 | } 83 | 84 | } 85 | 86 | -------------------------------------------------------------------------------- /library/src/commonMain/kotlin/utils/logger.kt: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import co.touchlab.kermit.Logger 4 | import utils.expected.isDebug 5 | 6 | sealed interface Tag { 7 | 8 | sealed interface View : Tag { 9 | data object SocialButton : View 10 | data object Pager : View 11 | data object Quiz : View 12 | data object Pref : View 13 | data object DatePicker : View 14 | } 15 | 16 | sealed interface Auth : Tag { 17 | data object LoadAuthFromStorage : Auth 18 | data object RefreshToken : Auth 19 | data object SignUp : Auth 20 | } 21 | 22 | sealed interface Network : Tag { 23 | data object PushMessage : Network 24 | data object Call : Network 25 | data object JsonParsing : Network 26 | 27 | data object WebSocket : Network { 28 | override fun toString() = "WebSocket" 29 | } 30 | } 31 | 32 | sealed interface Notification : Tag { 33 | data object FirebaseServiceToken : Notification 34 | } 35 | 36 | data object General : Tag 37 | data object Locale : Tag 38 | data object Crash : Tag 39 | data class Service(val name: String) : Tag { 40 | override fun toString() = name 41 | } 42 | 43 | data object BroadcastReceiver : Tag 44 | 45 | sealed class Event(val name: String) : Tag { 46 | data object Select : Event("select_item") 47 | data object Completed : Event("completed") 48 | data object Open : Event("open") 49 | } 50 | 51 | } 52 | 53 | fun logD(tag: Tag, msg: String, throwable: Throwable? = null) { 54 | if (isDebug) Logger.d(msg, throwable, tag::class.simpleName ?: "") 55 | } 56 | 57 | fun logE(tag: Tag, msg: String, throwable: Throwable? = null) { 58 | if (isDebug) Logger.e(msg, throwable, tag::class.simpleName ?: "") 59 | } 60 | 61 | fun logI(tag: Tag, msg: String) = Logger.e(msg, null, tag::class.simpleName ?: "") 62 | fun logV(tag: Tag, msg: String) = Logger.v(msg, null, tag::class.simpleName ?: "") 63 | fun logW(tag: Tag, msg: String, throwable: Throwable? = null) = 64 | Logger.w(msg, throwable, tag::class.simpleName ?: "") 65 | 66 | fun logWTF(tag: Tag, msg: String, throwable: Throwable? = null) = 67 | Logger.a(msg, throwable, tag::class.simpleName ?: "") -------------------------------------------------------------------------------- /shared/src/androidMain/kotlin/modules/common/ads/ads.android.kt: -------------------------------------------------------------------------------- 1 | package modules.common.ads 2 | 3 | import androidx.activity.ComponentActivity 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.platform.LocalContext 6 | import com.google.android.gms.ads.AdError 7 | import com.google.android.gms.ads.AdRequest 8 | import com.google.android.gms.ads.FullScreenContentCallback 9 | import com.google.android.gms.ads.LoadAdError 10 | import com.google.android.gms.ads.interstitial.InterstitialAd 11 | import com.google.android.gms.ads.interstitial.InterstitialAdLoadCallback 12 | import configs.Credentials 13 | 14 | actual class Ads( 15 | private val context: ComponentActivity 16 | ) { 17 | private var mInterstitialAd: InterstitialAd? = null 18 | private final val TAG = "Ads" 19 | 20 | actual fun load(adUnitId: String) { 21 | val adRequest = AdRequest.Builder().build() 22 | 23 | InterstitialAd.load( 24 | context, 25 | adUnitId, 26 | adRequest, 27 | object : InterstitialAdLoadCallback() { 28 | override fun onAdFailedToLoad(adError: LoadAdError) { 29 | mInterstitialAd = null 30 | } 31 | 32 | override fun onAdLoaded(interstitialAd: InterstitialAd) { 33 | mInterstitialAd = interstitialAd 34 | } 35 | }) 36 | 37 | mInterstitialAd?.fullScreenContentCallback = object : FullScreenContentCallback() { 38 | override fun onAdClicked() { 39 | // Called when a click is recorded for an ad. 40 | } 41 | 42 | override fun onAdDismissedFullScreenContent() { 43 | // Called when ad is dismissed. 44 | mInterstitialAd = null 45 | } 46 | 47 | override fun onAdFailedToShowFullScreenContent(p0: AdError) { 48 | mInterstitialAd = null 49 | } 50 | 51 | override fun onAdImpression() { 52 | // Called when an impression is recorded for an ad. 53 | } 54 | 55 | override fun onAdShowedFullScreenContent() { 56 | // Called when ad is shown. 57 | } 58 | } 59 | } 60 | 61 | actual fun show() { 62 | this.mInterstitialAd?.show(this.context) 63 | } 64 | } 65 | 66 | @Composable 67 | actual fun interstitialAd(adUnitId: String): Ads { 68 | val context = LocalContext.current as ComponentActivity 69 | val ad = Ads(context) 70 | ad.load(adUnitId) 71 | return ad 72 | } -------------------------------------------------------------------------------- /exampleApp/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.kotlinMultiplatform) 3 | alias(libs.plugins.androidApplication) 4 | alias(libs.plugins.jetbrainsCompose) 5 | alias(libs.plugins.compose.compiler) 6 | alias(libs.plugins.crashlytics) 7 | } 8 | /* 9 | Conditionally apply google services plugin based on existence of google-services.json file 10 | */ 11 | val googleServicesFile = File(projectDir, "google-services.json") 12 | if (googleServicesFile.exists()) { 13 | println("google-services.json found, Google Services will be enabled.") 14 | apply(plugin = libs.plugins.googleServices.get().pluginId) 15 | } else { 16 | println("google-services.json not found, Google Services will be disabled.") 17 | } 18 | /* 19 | End loading google services plugin 20 | */ 21 | 22 | kotlin { 23 | androidTarget() 24 | sourceSets { 25 | val androidMain by getting { 26 | dependencies { 27 | implementation(project(":shared")) 28 | } 29 | } 30 | } 31 | } 32 | 33 | android { 34 | compileSdk = (findProperty("android.compileSdk") as String).toInt() 35 | namespace = "org.cognitox" 36 | 37 | sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") 38 | 39 | defaultConfig { 40 | applicationId = "$namespace" 41 | minSdk = libs.versions.android.minSdk.get().toInt() 42 | targetSdk = libs.versions.android.targetSdk.get().toInt() 43 | versionCode = 1 44 | versionName = "1.0" 45 | } 46 | packaging { 47 | resources { 48 | excludes += "/META-INF/{AL2.0,LGPL2.1}" 49 | excludes += "META-INF/versions/9/previous-compilation-data.bin" 50 | } 51 | } 52 | compileOptions { 53 | sourceCompatibility = JavaVersion.VERSION_17 54 | targetCompatibility = JavaVersion.VERSION_17 55 | } 56 | kotlin { 57 | jvmToolchain(libs.versions.jdk.get().toInt()) 58 | } 59 | 60 | composeOptions { 61 | kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get() 62 | } 63 | 64 | buildFeatures { 65 | compose = true 66 | } 67 | 68 | // buildTypes { 69 | // release { 70 | // isMinifyEnabled = true 71 | // proguardFiles( 72 | // getDefaultProguardFile("proguard-android-optimize.txt"), 73 | // "proguard-rules.pro" 74 | // ) 75 | // } 76 | // } 77 | } 78 | -------------------------------------------------------------------------------- /examples/exampleApp/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.kotlinMultiplatform) 3 | alias(libs.plugins.androidApplication) 4 | alias(libs.plugins.jetbrainsCompose) 5 | alias(libs.plugins.compose.compiler) 6 | alias(libs.plugins.crashlytics) 7 | } 8 | /* 9 | Conditionally apply google services plugin based on existence of google-services.json file 10 | */ 11 | val googleServicesFile = File(projectDir, "google-services.json") 12 | if (googleServicesFile.exists()) { 13 | println("google-services.json found, Google Services will be enabled.") 14 | apply(plugin = libs.plugins.googleServices.get().pluginId) 15 | } else { 16 | println("google-services.json not found, Google Services will be disabled.") 17 | } 18 | /* 19 | End loading google services plugin 20 | */ 21 | 22 | kotlin { 23 | androidTarget() 24 | sourceSets { 25 | val androidMain by getting { 26 | dependencies { 27 | implementation(project(":shared")) 28 | } 29 | } 30 | } 31 | } 32 | 33 | android { 34 | compileSdk = (findProperty("android.compileSdk") as String).toInt() 35 | namespace = "org.cognitox" 36 | 37 | sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") 38 | 39 | defaultConfig { 40 | applicationId = "$namespace" 41 | minSdk = libs.versions.android.minSdk.get().toInt() 42 | targetSdk = libs.versions.android.targetSdk.get().toInt() 43 | versionCode = 1 44 | versionName = "1.0" 45 | } 46 | packaging { 47 | resources { 48 | excludes += "/META-INF/{AL2.0,LGPL2.1}" 49 | excludes += "META-INF/versions/9/previous-compilation-data.bin" 50 | } 51 | } 52 | compileOptions { 53 | sourceCompatibility = JavaVersion.VERSION_17 54 | targetCompatibility = JavaVersion.VERSION_17 55 | } 56 | kotlin { 57 | jvmToolchain(libs.versions.jdk.get().toInt()) 58 | } 59 | 60 | composeOptions { 61 | kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get() 62 | } 63 | 64 | buildFeatures { 65 | compose = true 66 | } 67 | 68 | // buildTypes { 69 | // release { 70 | // isMinifyEnabled = true 71 | // proguardFiles( 72 | // getDefaultProguardFile("proguard-android-optimize.txt"), 73 | // "proguard-rules.pro" 74 | // ) 75 | // } 76 | // } 77 | } 78 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/modules/common/views/components/header.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalMaterial3Api::class) 2 | 3 | package modules.common.views.components 4 | 5 | import androidx.compose.foundation.clickable 6 | import androidx.compose.foundation.layout.Box 7 | import androidx.compose.foundation.layout.RowScope 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.layout.size 10 | import androidx.compose.material3.CenterAlignedTopAppBar 11 | import androidx.compose.material3.ExperimentalMaterial3Api 12 | import androidx.compose.material3.Icon 13 | import androidx.compose.material3.MaterialTheme 14 | import androidx.compose.material3.TopAppBarScrollBehavior 15 | import androidx.compose.runtime.Composable 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.graphics.painter.Painter 18 | import androidx.compose.ui.semantics.Role 19 | import androidx.compose.ui.semantics.contentDescription 20 | import androidx.compose.ui.semantics.role 21 | import androidx.compose.ui.semantics.semantics 22 | import androidx.compose.ui.unit.dp 23 | 24 | @OptIn(ExperimentalMaterial3Api::class) 25 | @Composable 26 | fun AppBarTop( 27 | modifier: Modifier = Modifier, 28 | logo: @Composable ()->Unit, 29 | scrollBehavior: TopAppBarScrollBehavior? = null, 30 | onLogoClicked: () -> Unit = { }, 31 | title: @Composable () -> Unit, 32 | actions: @Composable RowScope.() -> Unit = {} 33 | ) { 34 | CenterAlignedTopAppBar( 35 | modifier = modifier, 36 | actions = actions, 37 | title = title, 38 | scrollBehavior = scrollBehavior, 39 | navigationIcon = { 40 | HeaderAppLogo( 41 | modifier = Modifier 42 | .size(64.dp) 43 | .clickable(onClick = onLogoClicked) 44 | .padding(16.dp), 45 | logo = logo, 46 | contentDescription = "Open Drawer" 47 | ) 48 | } 49 | ) 50 | } 51 | 52 | @Composable 53 | fun HeaderAppLogo( 54 | modifier: Modifier = Modifier, 55 | logo: @Composable ()->Unit, 56 | contentDescription: String? 57 | ) { 58 | 59 | val semantics = contentDescription?.let { 60 | Modifier.semantics { 61 | this.contentDescription = contentDescription 62 | this.role = Role.Image 63 | } 64 | } ?: Modifier 65 | 66 | Box(modifier = modifier.then(semantics)) { 67 | logo() 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /library/src/commonMain/kotlin/utils/time.kt: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import kotlinx.datetime.Clock 4 | import kotlinx.datetime.Instant 5 | import kotlinx.datetime.TimeZone 6 | import kotlinx.datetime.toLocalDateTime 7 | import kotlin.math.min 8 | 9 | fun Instant.toReadableDate(timeZone: TimeZone = TimeZone.currentSystemDefault()): String { 10 | val localDate = this.toLocalDateTime(timeZone).date 11 | val month = localDate.month.name.lowercase() 12 | .replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() } 13 | val monthShort = month.substring(0, min(3, month.length)) 14 | return "$monthShort ${localDate.dayOfMonth}, ${localDate.year}" 15 | } 16 | 17 | fun Instant.toReadableTime( 18 | timeZone: TimeZone = TimeZone.currentSystemDefault(), 19 | withSeconds: Boolean = false 20 | ): String { 21 | val localTime = this.toLocalDateTime(timeZone).time 22 | return "${localTime.hour}:${localTime.minute}" + if (withSeconds) ":${localTime.second}" else "" 23 | } 24 | 25 | fun Instant.toReadableDuration( 26 | instant: Instant = Clock.System.now(), 27 | short: Boolean = false 28 | ): String { 29 | val duration = (instant - this).absoluteValue 30 | val days = duration.inWholeDays 31 | val hours = duration.inWholeHours % 24 32 | val minutes = duration.inWholeMinutes % 60 33 | val seconds = duration.inWholeSeconds % 60 34 | val years = duration.inWholeDays / 365 35 | val yName = if (short) "y" else "years" 36 | val dName = if (short) "d" else "days" 37 | val hName = if (short) "h" else "hours" 38 | val mnName = if (short) "m" else "minutes" 39 | val sName = if (short) "s" else "seconds" 40 | return when { 41 | years > 0 -> "$years $yName ${days % 365} $dName" 42 | days > 0 -> "$days $dName $hours $hName" 43 | hours > 0 -> "$hours $hName $minutes $mnName" 44 | else -> "$minutes $mnName $seconds $sName" 45 | } 46 | } 47 | 48 | fun Instant.toHumanReadable( 49 | timeZone: TimeZone = TimeZone.currentSystemDefault() 50 | ): String { 51 | val duration = Clock.System.now() - this 52 | return if (duration.absoluteValue.inWholeDays > 0) { 53 | this.toReadableDate(timeZone) + " " + this.toReadableTime(timeZone) 54 | } else { 55 | this.toReadableDuration(short = true) 56 | } 57 | } 58 | 59 | fun secondsToTime(seconds: Long): String { 60 | val hours = seconds / 3600 61 | val minutes = (seconds % 3600) / 60 62 | val remainingSeconds = seconds % 60 63 | 64 | return "$hours h $minutes m $remainingSeconds s" 65 | } 66 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/modules/common/features/preferences/pref.vm.kt: -------------------------------------------------------------------------------- 1 | package modules.common.features.preferences 2 | 3 | import androidx.datastore.core.DataStore 4 | import androidx.datastore.preferences.core.Preferences 5 | import arrow.core.Option 6 | import arrow.core.toOption 7 | import arrow.fx.coroutines.parZip 8 | import cafe.adriel.voyager.core.model.ScreenModel 9 | import cafe.adriel.voyager.core.model.screenModelScope 10 | import kotlinx.coroutines.flow.Flow 11 | import kotlinx.coroutines.flow.first 12 | import kotlinx.coroutines.flow.map 13 | import kotlinx.coroutines.launch 14 | import kotlinx.coroutines.runBlocking 15 | 16 | class PrefVM( 17 | private val dataStore: DataStore 18 | ) : ScreenModel { 19 | 20 | fun updatePref(prefItem: PrefItem) = screenModelScope.launch { 21 | dataStore.updateData { 22 | val pref = it.toMutablePreferences() 23 | pref[prefItem.key] = prefItem.value 24 | pref 25 | } 26 | } 27 | 28 | fun getPref( 29 | key: Preferences.Key, 30 | defaultValue: T? = null 31 | ): Flow> = 32 | dataStore.data.map { pref -> 33 | pref[key].toOption() 34 | }.map { r -> 35 | r.onNone { 36 | defaultValue.toOption().onSome { dv -> 37 | dataStore.updateData { 38 | val pref = it.toMutablePreferences() 39 | pref[key] = dv 40 | pref 41 | } 42 | } 43 | } 44 | } 45 | 46 | fun getPrefs(keys: Set>): Flow, Option>> = 47 | dataStore.data.map { prefs -> 48 | keys.associateWith { 49 | prefs[it].toOption() 50 | } 51 | } 52 | 53 | /* 54 | CAUTION: Blocking code. 55 | Never use them below unless absolutely necessary 56 | */ 57 | fun getPrefBlocking(key: Preferences.Key): Option = runBlocking { 58 | dataStore.data.map { pref -> 59 | pref[key].toOption() 60 | }.first() 61 | } 62 | 63 | /* 64 | double blocking. could have better performance with something like 65 | parZip( 66 | { getPref(key1).first() }, 67 | { getPref(key2).first() } 68 | ) { a, b -> Pair(a, b) } 69 | but this triggers recomposition for some reason. 70 | */ 71 | fun getPrefsBlocking( 72 | key1: Preferences.Key, 73 | key2: Preferences.Key 74 | ): Pair, Option> = runBlocking { 75 | parZip( 76 | { getPrefBlocking(key1) }, 77 | { getPrefBlocking(key2) } 78 | ) { a, b -> Pair(a, b) } 79 | } 80 | /* 81 | End Blocking code 82 | */ 83 | 84 | } -------------------------------------------------------------------------------- /library/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi 2 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 3 | 4 | plugins { 5 | alias(libs.plugins.kotlinMultiplatform) 6 | alias(libs.plugins.androidLibrary) 7 | alias(libs.plugins.serialization) 8 | // id("module.publication") 9 | } 10 | 11 | kotlin { 12 | jvm { 13 | // Set compiler options directly here 14 | @OptIn(ExperimentalKotlinGradlePluginApi::class) 15 | compilerOptions { 16 | // Set JVM target using the new DSL 17 | jvmTarget.set(JvmTarget.JVM_17) 18 | } 19 | } 20 | androidTarget { 21 | publishLibraryVariants("release") 22 | @OptIn(ExperimentalKotlinGradlePluginApi::class) 23 | compilerOptions { 24 | jvmTarget.set(JvmTarget.JVM_17) 25 | } 26 | } 27 | iosX64() 28 | iosArm64() 29 | iosSimulatorArm64() 30 | // linuxX64() 31 | 32 | sourceSets { 33 | val commonMain by getting { 34 | dependencies { 35 | api(libs.arrow.core) 36 | api(libs.arrow.fx.coroutines) 37 | // DateTime 38 | api(libs.kotlinx.datetime) 39 | 40 | // Logging 41 | implementation(libs.kermit) 42 | implementation(libs.kermit.stately) 43 | 44 | // Serialization 45 | api(libs.kotlinx.serialization) 46 | // ktor 47 | implementation(libs.ktor) 48 | implementation(libs.ktor.client.content.negotiation) 49 | implementation(libs.ktor.serialization.kotlinx.json) 50 | implementation(libs.ktor.client.auth) 51 | api(libs.krossbow.stomp.core) 52 | implementation(libs.krossbow.websocket.ktor) 53 | api(libs.krossbow.stomp.kxserialization) 54 | // DI 55 | api(libs.koin.core) 56 | api(libs.koin.test) 57 | // DataStore 58 | api(libs.datastore.preferences.core) 59 | 60 | // Capture Crashes 61 | api(libs.sentry.kotlin.multiplatform) 62 | } 63 | } 64 | val commonTest by getting { 65 | dependencies { 66 | implementation(libs.kotlin.test) 67 | } 68 | } 69 | 70 | val androidMain by getting { 71 | dependencies { 72 | api(libs.koin.android) 73 | api(libs.ktor.client.okhttp) 74 | } 75 | } 76 | val iosMain by creating { 77 | dependencies { 78 | api(libs.ktor.client.darwin) 79 | } 80 | } 81 | val jvmMain by getting { 82 | dependencies { 83 | api(libs.ktor.client.okhttp) 84 | } 85 | } 86 | // val linuxX64Main by getting { 87 | // dependencies { 88 | // implementation(libs.ktor.client.cio) 89 | // } 90 | // } 91 | } 92 | } 93 | 94 | android { 95 | namespace = "org.cognitox.clientlib" 96 | compileSdk = libs.versions.android.compileSdk.get().toInt() 97 | defaultConfig { 98 | minSdk = libs.versions.android.minSdk.get().toInt() 99 | } 100 | buildFeatures { 101 | buildConfig = true 102 | } 103 | kotlin { 104 | jvmToolchain(libs.versions.jdk.get().toInt()) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /library/src/commonMain/kotlin/data/types/Errors.kt: -------------------------------------------------------------------------------- 1 | package data.types 2 | 3 | import arrow.core.Option 4 | import io.ktor.client.plugins.ClientRequestException 5 | import io.ktor.client.plugins.RedirectResponseException 6 | import io.ktor.client.plugins.ResponseException 7 | import io.ktor.client.plugins.ServerResponseException 8 | import io.ktor.util.toMap 9 | 10 | sealed class Err(val throwable: Throwable) { 11 | 12 | sealed class ValidationErr(ex: Throwable, val instructionMsg: String) : Err(ex) { 13 | data class Generic(val ex: Throwable, val instruction: String) : 14 | ValidationErr(ex, instruction) 15 | 16 | data class TextValidationErr(private val ex: Throwable, private val instruction: String) : 17 | ValidationErr(ex, instruction) 18 | 19 | data class PhoneValidationErr(private val ex: Throwable, private val instruction: String) : 20 | ValidationErr(ex, instruction) 21 | 22 | data class EmailValidationErr(private val ex: Throwable, private val instruction: String) : 23 | ValidationErr(ex, instruction) 24 | } 25 | 26 | data object GenericError : 27 | Err(RuntimeException("Generic Error, usually used as a placeholder for error.")) 28 | 29 | data object NotExistsError : 30 | Err(RuntimeException("Data doesn't exist")) 31 | 32 | class UserErr(ex: Throwable) : Err(ex) 33 | sealed class ParseErr(ex: Throwable) : Err(ex) { 34 | class JsonParseErr(ex: Throwable) : ParseErr(ex) 35 | class DateParseErr(ex: Throwable) : ParseErr(ex) 36 | } 37 | 38 | sealed class HttpErr(ex: Throwable, val statusCode: Int, val body: Option) : Err(ex) { 39 | class ClientErr( 40 | ex: Throwable, 41 | status: Int, 42 | val headers: Map>, 43 | body: Option 44 | ) : 45 | HttpErr(ex, status, body) 46 | 47 | class RedirectErr(ex: Throwable, status: Int, body: Option) : 48 | HttpErr(ex, status, body) 49 | 50 | class ServerErr(ex: Throwable, status: Int, body: Option) : 51 | HttpErr(ex, status, body) 52 | 53 | class GenericHttpErr(ex: Throwable, status: Int, body: Option) : 54 | HttpErr(ex, status, body) 55 | 56 | class ConnectionErr(ex: Throwable, status: Int, body: Option) : 57 | HttpErr(ex, status, body) 58 | 59 | class HttpJsonParseErr(ex: Throwable, status: Int, body: Option) : 60 | HttpErr(ex, status, body) 61 | } 62 | 63 | } 64 | 65 | fun ResponseException.toHttpError(body: Option) = 66 | when (this) { 67 | is ServerResponseException -> Err.HttpErr.ServerErr( 68 | this, this.response.status.value, body 69 | ) 70 | 71 | is ClientRequestException -> Err.HttpErr.ClientErr( 72 | this, this.response.status.value, this.response.headers.toMap(), body 73 | ) 74 | 75 | is RedirectResponseException -> Err.HttpErr.RedirectErr( 76 | this, 77 | this.response.status.value, body 78 | ) 79 | 80 | else -> Err.HttpErr.GenericHttpErr( 81 | this, this.response.status.value, body 82 | ) 83 | } 84 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/modules/common/animations/transitions.kt: -------------------------------------------------------------------------------- 1 | package modules.common.animations 2 | 3 | import androidx.compose.animation.EnterTransition 4 | import androidx.compose.animation.ExitTransition 5 | import androidx.compose.animation.core.tween 6 | import androidx.compose.animation.expandIn 7 | import androidx.compose.animation.expandVertically 8 | import androidx.compose.animation.fadeIn 9 | import androidx.compose.animation.fadeOut 10 | import androidx.compose.animation.scaleIn 11 | import androidx.compose.animation.scaleOut 12 | import androidx.compose.animation.shrinkOut 13 | import androidx.compose.animation.shrinkVertically 14 | import androidx.compose.animation.slideInHorizontally 15 | import androidx.compose.animation.slideInVertically 16 | import androidx.compose.animation.slideOutHorizontally 17 | import androidx.compose.animation.slideOutVertically 18 | import androidx.compose.ui.Alignment 19 | import androidx.compose.ui.unit.Density 20 | import androidx.compose.ui.unit.IntSize 21 | import androidx.compose.ui.unit.dp 22 | 23 | object Transition { 24 | class SlideInSlideOut(density: Density) { 25 | val enter = slideInVertically { 26 | // Slide in from 40 dp from the top. 27 | with(density) { -40.dp.roundToPx() } 28 | } + expandVertically( 29 | // Expand from the top. 30 | expandFrom = Alignment.Top 31 | ) + fadeIn( 32 | // Fade in with the initial alpha of 0.3f. 33 | initialAlpha = 0.3f 34 | ) 35 | val exit = slideOutVertically() + shrinkVertically() + fadeOut(targetAlpha = 1.0f) 36 | } 37 | 38 | object ScaleInOut { 39 | val enter = scaleIn() + expandVertically(expandFrom = Alignment.CenterVertically) 40 | 41 | val exit = scaleOut() + shrinkVertically(shrinkTowards = Alignment.CenterVertically) 42 | } 43 | 44 | object SimpleSlideInOut { 45 | val enter = slideInHorizontally(animationSpec = tween(500)) 46 | val exit = slideOutHorizontally(animationSpec = tween(500)) 47 | } 48 | 49 | class FadeInOut(duration: Int = 500) { 50 | val enter = fadeIn(animationSpec = tween(duration)) 51 | val exit = fadeOut(animationSpec = tween(duration)) 52 | } 53 | 54 | object ExpandShrink { 55 | fun enter(size: Int) = expandIn( 56 | expandFrom = Alignment.Center, 57 | animationSpec = tween(100), 58 | initialSize = { IntSize(size,size) } 59 | ) 60 | fun exit(size: Int) = shrinkOut( 61 | shrinkTowards = Alignment.Center, 62 | animationSpec = tween(1000), 63 | targetSize = { IntSize(size,size) } 64 | ) 65 | 66 | } 67 | 68 | object HeartTransition { 69 | fun enter(toggle: Boolean, size: Int) = if (toggle) expandIn( 70 | expandFrom = Alignment.Center, 71 | animationSpec = tween(1000), 72 | initialSize = { IntSize(size,size) } 73 | ) else EnterTransition.None 74 | fun exit(toggle: Boolean, size: Int) = if (toggle) shrinkOut( 75 | shrinkTowards = Alignment.Center, 76 | animationSpec = tween(1000), 77 | targetSize = { IntSize(size,size) } 78 | ) else ExitTransition.None 79 | 80 | } 81 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/modules/common/features/auth/AuthService.kt: -------------------------------------------------------------------------------- 1 | package modules.common.features.auth 2 | 3 | import arrow.core.Either 4 | import arrow.core.flatMap 5 | import arrow.core.left 6 | import data.types.Err 7 | import data.validation.EmailValidation 8 | import data.validation.PhoneValidation 9 | import io.ktor.client.statement.HttpResponse 10 | import modules.common.features.auth.models.Auth 11 | import modules.common.features.auth.models.FirebaseToken 12 | import modules.common.features.auth.models.FirebaseTokenReq 13 | import modules.common.features.auth.models.SignUpReq 14 | import modules.common.features.auth.models.UsernameAvailableResponse 15 | import modules.common.features.auth.models.VerificationData 16 | import modules.common.features.auth.models.VerificationResponse 17 | import modules.common.models.ErrResponse 18 | import utils.RemoteResult 19 | 20 | interface AuthService { 21 | fun isAuthenticated(): Boolean 22 | suspend fun login(username: String, password: String): Either 23 | suspend fun refreshToken(refreshToken: String): HttpResponse 24 | suspend fun checkUsername(username: String): Either 25 | suspend fun signup(token: String, signUpReq: SignUpReq): Either, Auth> 26 | suspend fun requestOTP( 27 | data: VerificationData 28 | ): Either> 29 | 30 | suspend fun registerFirebaseToken( 31 | tokenReq: FirebaseTokenReq 32 | ): Either, FirebaseToken> 33 | } 34 | 35 | class AuthServiceImpl( 36 | private val authRepository: AuthRepository 37 | ) : AuthService { 38 | override fun isAuthenticated() = false 39 | 40 | override suspend fun login(username: String, password: String): Either = 41 | this.authRepository.token(username, password) 42 | 43 | override suspend fun refreshToken(refreshToken: String): HttpResponse = 44 | this.authRepository.refreshToken(refreshToken) 45 | 46 | override suspend fun checkUsername(username: String): Either = 47 | if (username.length < 5) { 48 | Err.ValidationErr 49 | .Generic(RuntimeException("Username not available"), "Enter Username") 50 | .left() 51 | } else { 52 | authRepository.checkUsername(username) 53 | } 54 | 55 | override suspend fun signup( 56 | token: String, 57 | signUpReq: SignUpReq 58 | ): Either, Auth> = 59 | this.authRepository.signup(token, signUpReq) 60 | 61 | override suspend fun requestOTP( 62 | data: VerificationData 63 | ): Either> = when (data) { 64 | is VerificationData.Phone -> PhoneValidation(data.countryCodes).apply(data.phone) 65 | is VerificationData.Email -> EmailValidation().apply(data.email) 66 | }.flatMap { authRepository.requestOTP(it) } 67 | 68 | override suspend fun registerFirebaseToken( 69 | tokenReq: FirebaseTokenReq 70 | ): Either, FirebaseToken> = 71 | this.authRepository.registerFirebaseToken(tokenReq) 72 | 73 | 74 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if %ERRORLEVEL% equ 0 goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if %ERRORLEVEL% equ 0 goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | set EXIT_CODE=%ERRORLEVEL% 84 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 85 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 86 | exit /b %EXIT_CODE% 87 | 88 | :mainEnd 89 | if "%OS%"=="Windows_NT" endlocal 90 | 91 | :omega 92 | -------------------------------------------------------------------------------- /shared/src/androidMain/kotlin/modules/common/views/components/expected/images.kt: -------------------------------------------------------------------------------- 1 | package modules.common.views.components.expected 2 | 3 | import androidx.activity.ComponentActivity 4 | import androidx.activity.compose.rememberLauncherForActivityResult 5 | import androidx.activity.result.ActivityResultLauncher 6 | import androidx.activity.result.contract.ActivityResultContracts 7 | import androidx.compose.foundation.layout.Box 8 | import androidx.compose.foundation.layout.fillMaxSize 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.material3.CircularProgressIndicator 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.getValue 13 | import androidx.compose.runtime.mutableStateOf 14 | import androidx.compose.runtime.remember 15 | import androidx.compose.runtime.setValue 16 | import androidx.compose.ui.Alignment 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.graphics.painter.Painter 19 | import androidx.compose.ui.layout.ContentScale 20 | import androidx.compose.ui.platform.LocalContext 21 | import androidx.compose.ui.unit.dp 22 | import coil.compose.AsyncImage 23 | import coil.decode.SvgDecoder 24 | import coil.request.ImageRequest 25 | 26 | @Composable 27 | actual fun ImageLoader( 28 | url: String, 29 | contentDescription: String?, 30 | modifier: Modifier, 31 | placeholder: Painter?, 32 | scale: ContentScale, 33 | content: @Composable () -> Unit 34 | ) { 35 | Box(modifier = modifier) { 36 | var imageLoading by remember { mutableStateOf(true) } 37 | 38 | val isSvg = url.endsWith(".svg") 39 | val imageBuilder = ImageRequest.Builder(LocalContext.current) 40 | .data(url) 41 | .crossfade(true) 42 | if (isSvg) { 43 | imageBuilder.decoderFactory(SvgDecoder.Factory()) 44 | } 45 | AsyncImage( 46 | modifier = Modifier 47 | .fillMaxSize() 48 | .align(Alignment.Center), 49 | model = imageBuilder 50 | .build(), 51 | contentDescription = contentDescription, 52 | onLoading = { imageLoading = true }, 53 | onSuccess = { imageLoading = false }, 54 | onError = { imageLoading = false }, 55 | contentScale = scale, 56 | ) 57 | if (imageLoading) { 58 | CircularProgressIndicator( 59 | modifier = Modifier 60 | .align(Alignment.Center) 61 | .padding(10.dp) 62 | ) 63 | } 64 | 65 | content() 66 | } 67 | } 68 | 69 | actual class ImagePicker( 70 | private val activity: ComponentActivity 71 | ) { 72 | private lateinit var getContent: ActivityResultLauncher 73 | 74 | @Composable 75 | actual fun registerPicker(onImagePicked: (ByteArray) -> Unit) { 76 | getContent = rememberLauncherForActivityResult( 77 | ActivityResultContracts.GetContent() 78 | ) { uri -> 79 | uri?.let { 80 | activity.contentResolver.openInputStream(uri)?.use { 81 | onImagePicked(it.readBytes()) 82 | } 83 | } 84 | } 85 | } 86 | 87 | actual fun pickImage() { 88 | getContent.launch("image/*") 89 | } 90 | } 91 | 92 | @Composable 93 | actual fun createPicker(): ImagePicker { 94 | val activity = LocalContext.current as ComponentActivity 95 | return remember(activity) { 96 | ImagePicker(activity) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /library/src/commonMain/kotlin/utils/http-utils.kt: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import arrow.core.Either 4 | import arrow.core.left 5 | import arrow.core.none 6 | import arrow.core.right 7 | import arrow.core.toOption 8 | import data.Page 9 | import data.types.Err 10 | import data.types.toHttpError 11 | import io.ktor.client.call.body 12 | import io.ktor.client.plugins.ResponseException 13 | import io.ktor.client.statement.HttpResponse 14 | import io.ktor.serialization.JsonConvertException 15 | import io.ktor.util.toMap 16 | import modules.common.features.auth.models.AuthErr 17 | import modules.common.models.ResponseData 18 | import utils.expected.isDebug 19 | 20 | suspend inline fun resultPaginated(block: () -> Page): Either, Page> = 21 | result(block) 22 | 23 | suspend inline fun resultPaginatedV2(block: () -> ResponseData>) 24 | : Either, ResponseData>> = result(block) 25 | 26 | suspend inline fun result(block: () -> T): Either, T> = 27 | try { 28 | block().right() 29 | } catch (e: ResponseException) { 30 | try { 31 | e.toHttpError(e.response.body().toOption()).left() 32 | } catch (ne: JsonConvertException) { 33 | 34 | /* 35 | * If json is un parsable because of refresh token error, 36 | * let the ui handle it instead of crashing the app in debug mode 37 | */ 38 | if (isDebug && !e.isRefreshTokenExpiredError()) { 39 | logE(Tag.Network.JsonParsing, e.message ?: "") 40 | throw ne 41 | } 42 | val authErr = e.response.body().toErrorResponse() 43 | e.toHttpError((authErr as ErrBody).toOption()).left() 44 | } 45 | } catch (e: JsonConvertException) { 46 | if (isDebug) { 47 | logD(Tag.Network.JsonParsing, e.message ?: "") 48 | throw e 49 | } 50 | logE(Tag.Network.JsonParsing, e.toString()) 51 | Err.HttpErr.HttpJsonParseErr(e, -1, none()).left() 52 | } catch (e: Exception) { 53 | logD(Tag.Network.Call, e.toString()) 54 | Err.HttpErr.ConnectionErr(e, -1, none()).left() 55 | } 56 | 57 | suspend inline fun resultSingleWithHeaders(block: () -> HttpResponse): Either, RemoteResult> = 58 | try { 59 | val response = block() 60 | RemoteResult(response.body(), response.headers.toMap()).right() 61 | } catch (e: ResponseException) { 62 | try { 63 | e.toHttpError(e.response.body().toOption()).left() 64 | } catch (ne: JsonConvertException) { 65 | if (isDebug) { 66 | throw ne 67 | } 68 | val authErr = e.response.body().toErrorResponse() 69 | e.toHttpError((authErr as ErrBody).toOption()).left() 70 | } 71 | } catch (e: JsonConvertException) { 72 | if (isDebug) { 73 | throw e 74 | } 75 | logE(Tag.Network.JsonParsing, e.toString()) 76 | Err.HttpErr.HttpJsonParseErr(e, -1, none()).left() 77 | } 78 | 79 | 80 | typealias HttpHeaders = Map> 81 | 82 | data class RemoteResult( 83 | val body: T, 84 | val headers: HttpHeaders 85 | ) 86 | 87 | suspend fun ResponseException.isRefreshTokenExpiredError(): Boolean = 88 | this.response.body().description.startsWith("Invalid refresh token (expired)") -------------------------------------------------------------------------------- /shared/src/iosMain/kotlin/modules/common/views/components/expected/images.kt: -------------------------------------------------------------------------------- 1 | package modules.common.views.components.expected 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.runtime.remember 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.graphics.painter.Painter 9 | import androidx.compose.ui.interop.LocalUIViewController 10 | import androidx.compose.ui.layout.ContentScale 11 | import com.seiko.imageloader.rememberImagePainter 12 | import kotlinx.cinterop.ExperimentalForeignApi 13 | import kotlinx.cinterop.refTo 14 | import platform.UIKit.UIImage 15 | import platform.UIKit.UIImageJPEGRepresentation 16 | import platform.UIKit.UIImagePickerController 17 | import platform.UIKit.UIImagePickerControllerDelegateProtocol 18 | import platform.UIKit.UIImagePickerControllerSourceType 19 | import platform.UIKit.UINavigationControllerDelegateProtocol 20 | import platform.UIKit.UIViewController 21 | import platform.darwin.NSObject 22 | import platform.posix.memcpy 23 | 24 | @Composable 25 | actual fun ImageLoader( 26 | url: String, 27 | contentDescription: String?, 28 | modifier: Modifier, 29 | placeholder: Painter?, 30 | scale: ContentScale, 31 | content: @Composable () -> Unit 32 | ) { 33 | val painter = rememberImagePainter(url) 34 | Box( 35 | modifier = modifier 36 | ) { 37 | Image( 38 | modifier = Modifier.matchParentSize(), 39 | painter = painter, 40 | contentDescription = contentDescription, 41 | contentScale = scale, 42 | ) 43 | 44 | content() 45 | } 46 | } 47 | 48 | actual class ImagePicker( 49 | private val rootController: UIViewController 50 | ) { 51 | private val imagePickerController = UIImagePickerController().apply { 52 | sourceType = UIImagePickerControllerSourceType.UIImagePickerControllerSourceTypePhotoLibrary 53 | } 54 | 55 | private var onImagePicked: (ByteArray) -> Unit = {} 56 | 57 | @OptIn(ExperimentalForeignApi::class) 58 | private val delegate = object : NSObject(), UIImagePickerControllerDelegateProtocol, 59 | UINavigationControllerDelegateProtocol { 60 | 61 | override fun imagePickerController( 62 | picker: UIImagePickerController, 63 | didFinishPickingImage: UIImage, 64 | editingInfo: Map? 65 | ) { 66 | val imageNsData = UIImageJPEGRepresentation(didFinishPickingImage, 1.0) 67 | ?: return 68 | val bytes = ByteArray(imageNsData.length.toInt()) 69 | memcpy(bytes.refTo(0), imageNsData.bytes, imageNsData.length) 70 | 71 | onImagePicked(bytes) 72 | 73 | picker.dismissViewControllerAnimated(true, null) 74 | } 75 | 76 | override fun imagePickerControllerDidCancel(picker: UIImagePickerController) { 77 | picker.dismissViewControllerAnimated(true, null) 78 | } 79 | } 80 | 81 | @Composable 82 | actual fun registerPicker(onImagePicked: (ByteArray) -> Unit) { 83 | this.onImagePicked = onImagePicked 84 | } 85 | 86 | actual fun pickImage() { 87 | rootController.presentViewController(imagePickerController, true) { 88 | imagePickerController.delegate = delegate 89 | } 90 | } 91 | } 92 | 93 | @Composable 94 | actual fun createPicker(): ImagePicker { 95 | val uiViewController = LocalUIViewController.current 96 | return remember { 97 | ImagePicker(uiViewController) 98 | } 99 | } -------------------------------------------------------------------------------- /library/src/commonMain/kotlin/modules/common/features/auth/models/auth.model.kt: -------------------------------------------------------------------------------- 1 | package modules.common.features.auth.models 2 | 3 | import data.types.CountryCodes 4 | import io.ktor.http.HttpStatusCode 5 | import kotlinx.datetime.Instant 6 | import kotlinx.serialization.ExperimentalSerializationApi 7 | import kotlinx.serialization.SerialName 8 | import kotlinx.serialization.Serializable 9 | import kotlinx.serialization.json.JsonNames 10 | import modules.common.models.ErrResponse 11 | import kotlin.time.Duration.Companion.milliseconds 12 | 13 | @Serializable 14 | @OptIn(ExperimentalSerializationApi::class) 15 | data class Auth constructor( 16 | @JsonNames(names = ["access_token"]) 17 | val accessToken: String, 18 | 19 | @JsonNames(names = ["token_type"]) 20 | val tokenType: String, 21 | 22 | @JsonNames(names = ["refresh_token"]) 23 | val refreshToken: String, 24 | 25 | @JsonNames(names = ["expires_in"]) 26 | val expiresIn: Long, 27 | 28 | val scope: String, 29 | val phone: String? = null, 30 | val name: String, 31 | val id: Long, 32 | val email: String? = null, 33 | val username: String, 34 | val jti: String 35 | ) 36 | 37 | @Serializable 38 | data class AuthErr( 39 | @SerialName("error") 40 | val errType: String, 41 | @SerialName("error_description") 42 | val description: String 43 | ) { 44 | enum class AuthErrTypes(val value: String) { 45 | INVALID_TOKEN("invalid_token") 46 | } 47 | 48 | override fun toString(): String { 49 | return "$errType : $description" 50 | } 51 | 52 | fun toErrorResponse() = ErrResponse(HttpStatusCode.Unauthorized.value, errType, description) 53 | } 54 | 55 | @Serializable 56 | data class VerificationResponse( 57 | @SerialName("identity") 58 | val identity: String, 59 | @SerialName("token_valid_until") 60 | val tokenValidUntil: Instant, 61 | @SerialName("token_validity_millis") 62 | val tokenValidity: Long, 63 | @SerialName("reg_method") 64 | val regMethod: RegMethod 65 | ) { 66 | val validity = tokenValidity.milliseconds 67 | } 68 | 69 | enum class RegMethod { 70 | PHONE, EMAIL 71 | } 72 | 73 | @Serializable 74 | data class UsernameAvailableResponse( 75 | val available: Boolean, 76 | val reason: String 77 | ) 78 | 79 | 80 | @Serializable 81 | data class SignUpReq( 82 | val name: String, 83 | val gender: String, 84 | val email: String? = null, 85 | val username: String, 86 | val password: String, 87 | val phone: String? = null, 88 | val role: String, 89 | ) 90 | 91 | @Serializable 92 | data class FirebaseToken( 93 | val id: Long, 94 | 95 | @SerialName("created_at") 96 | var createdAt: Instant, 97 | 98 | @SerialName("updated_at") 99 | var updatedAt: Instant? = null, 100 | 101 | @SerialName("user_id") 102 | val userId: Long, 103 | 104 | @SerialName("user_token") 105 | val userToken: String 106 | ) 107 | 108 | @Serializable 109 | data class FirebaseTokenReq( 110 | @SerialName("user_id") 111 | val userId: Long, 112 | @SerialName("user_token") 113 | val userToken: String, 114 | @SerialName("app_identifier") 115 | val appIdentifier: String 116 | ) 117 | 118 | sealed interface VerificationData { 119 | data class Email(val email: String) : VerificationData 120 | data class Phone(val countryCodes: CountryCodes, val phone: String) : VerificationData 121 | } -------------------------------------------------------------------------------- /shared/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi 2 | 3 | plugins { 4 | alias(libs.plugins.kotlinMultiplatform) 5 | alias(libs.plugins.androidLibrary) 6 | alias(libs.plugins.jetbrainsCompose) 7 | alias(libs.plugins.compose.compiler) 8 | alias(libs.plugins.serialization) 9 | } 10 | 11 | kotlin { 12 | androidTarget() 13 | 14 | listOf( 15 | iosX64(), 16 | iosArm64(), 17 | iosSimulatorArm64() 18 | ).forEach { iosTarget -> 19 | iosTarget.binaries.framework { 20 | baseName = "shared" 21 | isStatic = true 22 | } 23 | } 24 | 25 | sourceSets { 26 | val commonMain by getting { 27 | dependencies { 28 | implementation(compose.runtime) 29 | implementation(compose.foundation) 30 | api(compose.material3) 31 | api(compose.materialIconsExtended) 32 | @OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class) 33 | implementation(compose.components.resources) 34 | 35 | implementation(project(":library")) 36 | // implementation(libs.cognito.clientlib) 37 | 38 | api(libs.voyager.navigator) 39 | api(libs.voyager.koin) 40 | 41 | // https://github.com/qdsfdhvh/compose-imageloader 42 | api(libs.image.loader) 43 | 44 | api(libs.aaychart) 45 | } 46 | } 47 | val androidMain by getting { 48 | dependencies { 49 | api(libs.compose.ui) 50 | api(libs.compose.ui.tooling) 51 | api(libs.compose.ui.tooling.preview) 52 | api(libs.androidx.activity.compose) 53 | 54 | api(libs.androidx.core.ktx) 55 | api(libs.coil.compose) 56 | api(libs.coil.svg) 57 | 58 | // Import the Firebase BoM 59 | api(project.dependencies.platform(libs.firebase.bom)) 60 | // When using the BoM, don't specify versions in Firebase dependencies 61 | api(libs.firebase.analytics.ktx) 62 | api(libs.firebase.crashlytics.ktx) 63 | api(libs.firebase.messaging.ktx) 64 | api(libs.play.service.ads) 65 | } 66 | } 67 | val iosX64Main by getting 68 | val iosArm64Main by getting 69 | val iosSimulatorArm64Main by getting 70 | val iosMain by creating { 71 | dependencies { 72 | implementation(libs.ktor.client.darwin) 73 | } 74 | } 75 | } 76 | 77 | @OptIn(ExperimentalKotlinGradlePluginApi::class) 78 | compilerOptions { 79 | freeCompilerArgs.add("-Xexpect-actual-classes") 80 | } 81 | } 82 | 83 | android { 84 | namespace = "com.example.shared" 85 | compileSdk = libs.versions.android.compileSdk.get().toInt() 86 | 87 | sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") 88 | sourceSets["main"].res.srcDirs("src/androidMain/res") 89 | sourceSets["main"].resources.srcDirs("src/commonMain/resources") 90 | 91 | defaultConfig { 92 | minSdk = libs.versions.android.minSdk.get().toInt() 93 | } 94 | buildFeatures { 95 | compose = true 96 | buildConfig = true 97 | } 98 | composeOptions { 99 | kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get() 100 | } 101 | packaging { 102 | resources { 103 | excludes += "/META-INF/{AL2.0,LGPL2.1}" 104 | excludes += "META-INF/versions/9/previous-compilation-data.bin" 105 | } 106 | } 107 | buildTypes { 108 | getByName("release") { 109 | isMinifyEnabled = false 110 | } 111 | } 112 | compileOptions { 113 | sourceCompatibility = JavaVersion.VERSION_17 114 | targetCompatibility = JavaVersion.VERSION_17 115 | } 116 | kotlin { 117 | jvmToolchain(libs.versions.jdk.get().toInt()) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /examples/shared/exampleAndroid/main.example.kt: -------------------------------------------------------------------------------- 1 | package modules.exampleModule 2 | 3 | import android.Manifest 4 | import android.app.Application 5 | import android.content.Intent 6 | import android.content.pm.PackageManager 7 | import android.os.Build 8 | import android.os.Bundle 9 | import android.view.WindowManager 10 | import androidx.activity.compose.setContent 11 | import androidx.activity.result.contract.ActivityResultContracts 12 | import androidx.appcompat.app.AppCompatActivity 13 | import androidx.core.content.ContextCompat 14 | import cafe.adriel.voyager.navigator.Navigator 15 | import com.google.android.gms.ads.MobileAds 16 | import utils.expected.CrashAnalytics 17 | import modules.common.features.notifications.models.NDataKeys 18 | import modules.exampleModule.di.koinExampleModules 19 | import modules.exampleModule.screens.MainScreen 20 | import org.koin.android.ext.koin.androidContext 21 | import org.koin.android.ext.koin.androidLogger 22 | import org.koin.core.context.GlobalContext 23 | import utils.Tag 24 | import utils.logD 25 | import java.util.Locale 26 | import configs.Credentials 27 | 28 | 29 | class ExampleApp : Application() { 30 | override fun onCreate() { 31 | CrashAnalytics(this).init(Credentials.Sentry.dsn.get()) 32 | super.onCreate() 33 | GlobalContext.startKoin { 34 | androidContext(this@ExampleApp) 35 | androidLogger() 36 | modules(koinExampleModules) 37 | } 38 | } 39 | } 40 | 41 | class MainActivity : AppCompatActivity() { 42 | override fun onCreate(savedInstanceState: Bundle?) { 43 | super.onCreate(savedInstanceState) 44 | 45 | this.window.setSoftInputMode( 46 | WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE 47 | or WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE 48 | ) 49 | 50 | val taskId = intent.getLongExtra(NDataKeys.REFERENCE_ID.key, -1) 51 | logD(Tag.Network.PushMessage, taskId.toString()) 52 | 53 | setContent { 54 | Navigator( 55 | screen = MainScreen 56 | ) 57 | } 58 | 59 | logD(Tag.Locale, Locale.getDefault().language) 60 | 61 | askNotificationPermission() 62 | 63 | // Initialize mobile ads 64 | MobileAds.initialize(this) 65 | } 66 | 67 | override fun onNewIntent(intent: Intent) { 68 | super.onNewIntent(intent) 69 | /* 70 | Do something based on the intent.action 71 | May be open a different page or something 72 | */ 73 | setContent { 74 | Navigator(screen = MainScreen) 75 | } 76 | } 77 | 78 | // firbase notification permission 79 | // Declare the launcher at the top of your Activity/Fragment: 80 | private val requestPermissionLauncher = registerForActivityResult( 81 | ActivityResultContracts.RequestPermission(), 82 | ) { isGranted: Boolean -> 83 | if (isGranted) { 84 | // FCM SDK (and your app) can post notifications. 85 | } else { 86 | // TODO: Inform user that that your app will not show notifications. 87 | } 88 | } 89 | 90 | private fun askNotificationPermission() { 91 | // This is only necessary for API level >= 33 (TIRAMISU) 92 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 93 | if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) == 94 | PackageManager.PERMISSION_GRANTED 95 | ) { 96 | // FCM SDK (and your app) can post notifications. 97 | } else if (shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS)) { 98 | // TODO: display an educational UI explaining to the user the features that will be enabled 99 | // by them granting the POST_NOTIFICATION permission. This UI should provide the user 100 | // "OK" and "No thanks" buttons. If the user selects "OK," directly request the permission. 101 | // If the user selects "No thanks," allow the user to continue without notifications. 102 | } else { 103 | // Directly ask for the permission 104 | requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) 105 | } 106 | } 107 | } 108 | } -------------------------------------------------------------------------------- /shared/src/androidMain/kotlin/modules/exampleModule/main.example.kt: -------------------------------------------------------------------------------- 1 | package modules.exampleModule 2 | 3 | import android.Manifest 4 | import android.app.Application 5 | import android.content.Intent 6 | import android.content.pm.PackageManager 7 | import android.os.Build 8 | import android.os.Bundle 9 | import android.view.WindowManager 10 | import androidx.activity.compose.setContent 11 | import androidx.activity.result.contract.ActivityResultContracts 12 | import androidx.appcompat.app.AppCompatActivity 13 | import androidx.core.content.ContextCompat 14 | import cafe.adriel.voyager.navigator.Navigator 15 | import com.google.android.gms.ads.MobileAds 16 | import configs.Credentials 17 | import modules.common.features.notifications.models.NDataKeys 18 | import modules.exampleModule.di.koinExampleModules 19 | import modules.exampleModule.screens.MainScreen 20 | import org.koin.android.ext.koin.androidContext 21 | import org.koin.android.ext.koin.androidLogger 22 | import org.koin.core.context.GlobalContext 23 | import utils.Tag 24 | import utils.expected.CrashAnalytics 25 | import utils.logD 26 | import java.util.Locale 27 | 28 | 29 | class ExampleApp : Application() { 30 | override fun onCreate() { 31 | CrashAnalytics(this).init(Credentials.Sentry.dsn.get()) 32 | super.onCreate() 33 | GlobalContext.startKoin { 34 | androidContext(this@ExampleApp) 35 | androidLogger() 36 | modules(koinExampleModules) 37 | } 38 | } 39 | } 40 | 41 | class MainActivity : AppCompatActivity() { 42 | override fun onCreate(savedInstanceState: Bundle?) { 43 | super.onCreate(savedInstanceState) 44 | 45 | this.window.setSoftInputMode( 46 | WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE 47 | or WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE 48 | ) 49 | 50 | val taskId = intent.getLongExtra(NDataKeys.REFERENCE_ID.key, -1) 51 | logD(Tag.Network.PushMessage, taskId.toString()) 52 | 53 | setContent { 54 | Navigator( 55 | screen = MainScreen 56 | ) 57 | } 58 | 59 | logD(Tag.Locale, Locale.getDefault().language) 60 | 61 | askNotificationPermission() 62 | 63 | // Initialize mobile ads 64 | MobileAds.initialize(this) 65 | } 66 | 67 | override fun onNewIntent(intent: Intent) { 68 | super.onNewIntent(intent) 69 | /* 70 | Do something based on the intent.action 71 | May be open a different page or something 72 | */ 73 | setContent { 74 | Navigator(screen = MainScreen) 75 | } 76 | } 77 | 78 | // firbase notification permission 79 | // Declare the launcher at the top of your Activity/Fragment: 80 | private val requestPermissionLauncher = registerForActivityResult( 81 | ActivityResultContracts.RequestPermission(), 82 | ) { isGranted: Boolean -> 83 | if (isGranted) { 84 | // FCM SDK (and your app) can post notifications. 85 | } else { 86 | // TODO: Inform user that that your app will not show notifications. 87 | } 88 | } 89 | 90 | private fun askNotificationPermission() { 91 | // This is only necessary for API level >= 33 (TIRAMISU) 92 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 93 | if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) == 94 | PackageManager.PERMISSION_GRANTED 95 | ) { 96 | // FCM SDK (and your app) can post notifications. 97 | } else if (shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS)) { 98 | // TODO: display an educational UI explaining to the user the features that will be enabled 99 | // by them granting the POST_NOTIFICATION permission. This UI should provide the user 100 | // "OK" and "No thanks" buttons. If the user selects "OK," directly request the permission. 101 | // If the user selects "No thanks," allow the user to continue without notifications. 102 | } else { 103 | // Directly ask for the permission 104 | requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) 105 | } 106 | } 107 | } 108 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/modules/common/views/components/search.view.kt: -------------------------------------------------------------------------------- 1 | package modules.common.views.components 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.foundation.layout.fillMaxSize 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.material.icons.Icons 7 | import androidx.compose.material.icons.outlined.Search 8 | import androidx.compose.material3.Icon 9 | import androidx.compose.material3.LinearProgressIndicator 10 | import androidx.compose.material3.MaterialTheme 11 | import androidx.compose.material3.TextFieldColors 12 | import androidx.compose.material3.TextFieldDefaults 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.runtime.LaunchedEffect 15 | import androidx.compose.runtime.getValue 16 | import androidx.compose.runtime.mutableStateOf 17 | import androidx.compose.runtime.remember 18 | import androidx.compose.runtime.setValue 19 | import androidx.compose.ui.Modifier 20 | import androidx.compose.ui.focus.FocusRequester 21 | import androidx.compose.ui.focus.focusRequester 22 | import androidx.compose.ui.text.style.TextAlign 23 | import arrow.core.toOption 24 | import data.Page 25 | import data.types.State 26 | 27 | @Composable 28 | fun SearchView( 29 | modifier: Modifier = Modifier, 30 | title: String = "Search", 31 | hint: String = "Search", 32 | state: State, 33 | requestFocus: Boolean = false, 34 | hideSearchInput: Boolean = false, 35 | searchInputColors: TextFieldColors = TextFieldDefaults.colors(), 36 | onQueryChanged: (query: String, currentPage: Long, pageSize: Int) -> Unit, 37 | noContentView: (@Composable () -> Unit)? = null, 38 | content: @Composable (Page) -> Unit, 39 | ) { 40 | var loading by remember { mutableStateOf(false) } 41 | var query by remember { mutableStateOf("") } 42 | val focusRequester = remember { FocusRequester() } 43 | 44 | LaunchedEffect(Unit) { 45 | if (requestFocus) { 46 | focusRequester.requestFocus() 47 | } 48 | } 49 | 50 | var currentPage = 0L 51 | var pageSize = 10 52 | Column( 53 | modifier = modifier 54 | ) { 55 | if (loading) { 56 | LinearProgressIndicator( 57 | modifier = Modifier.fillMaxWidth() 58 | ) 59 | } 60 | if (!hideSearchInput) { 61 | WOutlinedTextFieldV2( 62 | modifier = Modifier 63 | .focusRequester(focusRequester) 64 | .fillMaxWidth(), 65 | text = query, 66 | label = hint, 67 | leadingIcon = { 68 | Icon( 69 | imageVector = Icons.Outlined.Search, 70 | contentDescription = "Search Icon" 71 | ) 72 | }, 73 | onTextChanged = { validated -> 74 | val q = validated.copy( 75 | text = validated.text.filter { 76 | true 77 | // it.isLetterOrDigit() || it.isWhitespace() || it in ('\u0980'..'\u09FF') 78 | } 79 | ) 80 | query = q.text 81 | loading = true 82 | onQueryChanged(q.text, currentPage, pageSize) 83 | }, 84 | colors = searchInputColors 85 | ) 86 | } 87 | 88 | StatefulSurface>( 89 | state = state, 90 | initialContent = { 91 | WSubtitleText( 92 | modifier = Modifier.fillMaxSize(), 93 | textAlign = TextAlign.Center, 94 | text = title, 95 | color = MaterialTheme.colorScheme.onSurface 96 | ) 97 | }, 98 | resultContent = { page -> 99 | loading = false 100 | currentPage = page.number 101 | pageSize = page.size 102 | 103 | if (page.numberOfElements > 0) { 104 | content(page) 105 | } else { 106 | noContentView.toOption().fold( 107 | { 108 | NoContentView( 109 | message = "No content", 110 | buttonText = "Clear", 111 | onButtonClick = { 112 | query = "" 113 | onQueryChanged("", 0, pageSize) 114 | } 115 | ) 116 | }, 117 | { 118 | it() 119 | } 120 | ) 121 | } 122 | } 123 | ) 124 | } 125 | } 126 | 127 | 128 | fun Char.isBengaliCharacter(): Boolean { 129 | return this in ('\u0980'..'\u09FF') || this == ' ' // Adjust the Unicode range accordingly 130 | } -------------------------------------------------------------------------------- /examples/shared/example/theme/typography.example.kt: -------------------------------------------------------------------------------- 1 | package modules.exampleModule.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | val fontFamily = FontFamily.Serif 10 | val typography = Typography( 11 | // Display Large - Montserrat 57/64 . -0.25px 12 | displayLarge = TextStyle( 13 | fontFamily = fontFamily, 14 | fontWeight = FontWeight.W400, 15 | fontSize = 57.sp, 16 | lineHeight = 64.sp, 17 | letterSpacing = (-0.25).sp, 18 | ), 19 | 20 | // Display Medium - Montserrat 45/52 . 0px 21 | displayMedium = TextStyle( 22 | fontFamily = fontFamily, 23 | fontWeight = FontWeight.W400, 24 | fontSize = 45.sp, 25 | lineHeight = 52.sp, 26 | letterSpacing = 0.sp, 27 | ), 28 | 29 | // Display Small - Montserrat 36/44 . 0px 30 | displaySmall = TextStyle( 31 | fontFamily = fontFamily, 32 | fontWeight = FontWeight.W400, 33 | fontSize = 36.sp, 34 | lineHeight = 44.sp, 35 | letterSpacing = 0.sp, 36 | ), 37 | 38 | // Headline Large - Montserrat 32/40 . 0px 39 | headlineLarge = TextStyle( 40 | fontFamily = fontFamily, 41 | fontWeight = FontWeight.W400, 42 | fontSize = 32.sp, 43 | lineHeight = 40.sp, 44 | letterSpacing = 0.sp, 45 | ), 46 | 47 | // Headline Medium - Montserrat 28/36 . 0px 48 | headlineMedium = TextStyle( 49 | fontFamily = fontFamily, 50 | fontWeight = FontWeight.W400, 51 | fontSize = 28.sp, 52 | lineHeight = 36.sp, 53 | letterSpacing = 0.sp, 54 | ), 55 | 56 | // Headline Small - Montserrat 24/32 . 0px 57 | headlineSmall = TextStyle( 58 | fontFamily = fontFamily, 59 | fontWeight = FontWeight.W400, 60 | fontSize = 24.sp, 61 | lineHeight = 32.sp, 62 | letterSpacing = 0.sp, 63 | ), 64 | 65 | // Title Large - Montserrat 22/28 . 0px 66 | titleLarge = TextStyle( 67 | fontFamily = fontFamily, 68 | fontWeight = FontWeight.W400, 69 | fontSize = 22.sp, 70 | lineHeight = 28.sp, 71 | letterSpacing = 0.sp, 72 | ), 73 | 74 | // Title Medium - Montserrat 16/24 . 0.15px 75 | titleMedium = TextStyle( 76 | fontFamily = fontFamily, 77 | fontWeight = FontWeight.W500, 78 | fontSize = 16.sp, 79 | lineHeight = 24.sp, 80 | letterSpacing = 0.15.sp, 81 | ), 82 | 83 | // Title Small - Montserrat 14/20 . 0.1px 84 | titleSmall = TextStyle( 85 | fontFamily = fontFamily, 86 | fontWeight = FontWeight.W500, 87 | fontSize = 14.sp, 88 | lineHeight = 20.sp, 89 | letterSpacing = 0.1.sp, 90 | ), 91 | 92 | // Label Large - Montserrat 14/20 . 0.1px 93 | labelLarge = TextStyle( 94 | fontFamily = fontFamily, 95 | fontWeight = FontWeight.W500, 96 | fontSize = 14.sp, 97 | lineHeight = 20.sp, 98 | letterSpacing = 0.1.sp, 99 | ), 100 | 101 | // Label Medium - Montserrat 12/16 . 0.5px 102 | labelMedium = TextStyle( 103 | fontFamily = fontFamily, 104 | fontWeight = FontWeight.W500, 105 | fontSize = 12.sp, 106 | lineHeight = 16.sp, 107 | letterSpacing = 0.5.sp, 108 | ), 109 | 110 | // Label Small - Montserrat 11/16 . 0.5px 111 | labelSmall = TextStyle( 112 | fontFamily = fontFamily, 113 | fontWeight = FontWeight.W500, 114 | fontSize = 11.sp, 115 | lineHeight = 16.sp, 116 | letterSpacing = 0.5.sp, 117 | ), 118 | 119 | // Body Large - Montserrat 16/24 . 0.5px 120 | bodyLarge = TextStyle( 121 | fontFamily = fontFamily, 122 | fontWeight = FontWeight.W400, 123 | fontSize = 16.sp, 124 | lineHeight = 24.sp, 125 | letterSpacing = 0.5.sp, 126 | ), 127 | 128 | // Body Medium - Montserrat 14/20 . 0.25px 129 | bodyMedium = TextStyle( 130 | fontFamily = fontFamily, 131 | fontWeight = FontWeight.W400, 132 | fontSize = 14.sp, 133 | lineHeight = 20.sp, 134 | letterSpacing = 0.25.sp, 135 | ), 136 | // Body Small - Montserrat 12/16 . 0.4px 137 | bodySmall = TextStyle( 138 | fontFamily = fontFamily, 139 | fontWeight = FontWeight.W400, 140 | fontSize = 12.sp, 141 | lineHeight = 16.sp, 142 | letterSpacing = 0.4.sp, 143 | ), 144 | ) 145 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/modules/common/views/components/tag-scroll.view.kt: -------------------------------------------------------------------------------- 1 | package modules.common.views.components 2 | 3 | import androidx.compose.foundation.layout.ExperimentalLayoutApi 4 | import androidx.compose.foundation.layout.FlowRow 5 | import androidx.compose.foundation.layout.Spacer 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.foundation.layout.size 8 | import androidx.compose.foundation.layout.width 9 | import androidx.compose.foundation.lazy.LazyRow 10 | import androidx.compose.material3.AssistChipDefaults 11 | import androidx.compose.material3.Badge 12 | import androidx.compose.material3.BadgeDefaults 13 | import androidx.compose.material3.ExperimentalMaterial3Api 14 | import androidx.compose.material3.FilterChip 15 | import androidx.compose.material3.Icon 16 | import androidx.compose.material3.MaterialTheme 17 | import androidx.compose.material3.Text 18 | import androidx.compose.runtime.Composable 19 | import androidx.compose.runtime.getValue 20 | import androidx.compose.runtime.mutableStateOf 21 | import androidx.compose.runtime.remember 22 | import androidx.compose.runtime.setValue 23 | import androidx.compose.ui.Modifier 24 | import androidx.compose.ui.graphics.Color 25 | import androidx.compose.ui.graphics.vector.ImageVector 26 | import androidx.compose.ui.layout.ContentScale 27 | import androidx.compose.ui.unit.dp 28 | import modules.common.views.components.expected.ImageLoader 29 | import modules.common.views.dimensions.Paddings 30 | 31 | @OptIn(ExperimentalMaterial3Api::class) 32 | @Composable 33 | fun Tags( 34 | modifier: Modifier = Modifier, 35 | tags: List, 36 | selectedTag: Tag? = null, 37 | badgeContainerColor: Color = BadgeDefaults.containerColor, 38 | badgeContentColor: Color? = null, 39 | enabled: Boolean = true, 40 | tagCloud: Boolean = false, 41 | onClick: (tag: Tag) -> Unit 42 | ) { 43 | var selected by remember { mutableStateOf(selectedTag) } 44 | TagRow( 45 | modifier = Modifier 46 | .then(modifier), 47 | tags = tags, 48 | tagCloud = tagCloud 49 | ) { tag -> 50 | FilterChip( 51 | modifier = Modifier.padding( 52 | Paddings.Internal.SmallObjects.tiny, 53 | 0.dp 54 | ), 55 | enabled = enabled, 56 | onClick = { 57 | selected = tag 58 | onClick(tag) 59 | }, 60 | label = { 61 | WLabel( 62 | text = tag.name, 63 | color = MaterialTheme.colorScheme.primary 64 | ) 65 | }, 66 | selected = selected?.id == tag.id, 67 | leadingIcon = { 68 | tag.icon?.let { 69 | ImageLoader( 70 | modifier = Modifier 71 | .size(AssistChipDefaults.IconSize), 72 | url = it, 73 | scale = ContentScale.Inside, 74 | contentDescription = tag.name 75 | ) 76 | } ?: tag.vectorIcon?.let { 77 | Icon( 78 | imageVector = it, 79 | contentDescription = "Icon for ${tag.name}" 80 | ) 81 | } 82 | }, 83 | trailingIcon = { 84 | tag.badgeText?.let { 85 | Badge( 86 | containerColor = badgeContainerColor, 87 | contentColor = badgeContentColor ?: Color.White 88 | ) { 89 | Text( 90 | it 91 | ) 92 | } 93 | } 94 | } 95 | ) 96 | if (tags.lastOrNull()?.id != tag.id) { 97 | Spacer(modifier = Modifier.width(8.dp)) 98 | } 99 | } 100 | } 101 | 102 | @OptIn(ExperimentalLayoutApi::class) 103 | @Composable 104 | fun TagRow( 105 | modifier: Modifier = Modifier, 106 | tags: List = listOf(), 107 | tagCloud: Boolean = false, 108 | itemView: @Composable (tag: Tag) -> Unit 109 | ) { 110 | if (!tagCloud) { 111 | LazyRow( 112 | modifier = Modifier 113 | .then(modifier) 114 | ) { 115 | items(tags.size) { index -> 116 | val tag = tags[index] 117 | itemView(tag) 118 | } 119 | } 120 | } else { 121 | FlowRow(modifier = Modifier.padding(8.dp)) { 122 | tags.forEach { tag -> 123 | itemView(tag) 124 | } 125 | } 126 | } 127 | } 128 | 129 | data class Tag( 130 | val id: Long, 131 | val icon: String? = null, 132 | val vectorIcon: ImageVector? = null, 133 | val badgeText: String? = null, 134 | val name: String, 135 | ) -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/modules/exampleModule/theme/typography.example.kt: -------------------------------------------------------------------------------- 1 | package modules.exampleModule.theme 2 | 3 | import androidx.compose.material3.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | val fontFamily = FontFamily.Serif 10 | val typography = Typography( 11 | // Display Large - Montserrat 57/64 . -0.25px 12 | displayLarge = TextStyle( 13 | fontFamily = fontFamily, 14 | fontWeight = FontWeight.W400, 15 | fontSize = 57.sp, 16 | lineHeight = 64.sp, 17 | letterSpacing = (-0.25).sp, 18 | ), 19 | 20 | // Display Medium - Montserrat 45/52 . 0px 21 | displayMedium = TextStyle( 22 | fontFamily = fontFamily, 23 | fontWeight = FontWeight.W400, 24 | fontSize = 45.sp, 25 | lineHeight = 52.sp, 26 | letterSpacing = 0.sp, 27 | ), 28 | 29 | // Display Small - Montserrat 36/44 . 0px 30 | displaySmall = TextStyle( 31 | fontFamily = fontFamily, 32 | fontWeight = FontWeight.W400, 33 | fontSize = 36.sp, 34 | lineHeight = 44.sp, 35 | letterSpacing = 0.sp, 36 | ), 37 | 38 | // Headline Large - Montserrat 32/40 . 0px 39 | headlineLarge = TextStyle( 40 | fontFamily = fontFamily, 41 | fontWeight = FontWeight.W400, 42 | fontSize = 32.sp, 43 | lineHeight = 40.sp, 44 | letterSpacing = 0.sp, 45 | ), 46 | 47 | // Headline Medium - Montserrat 28/36 . 0px 48 | headlineMedium = TextStyle( 49 | fontFamily = fontFamily, 50 | fontWeight = FontWeight.W400, 51 | fontSize = 28.sp, 52 | lineHeight = 36.sp, 53 | letterSpacing = 0.sp, 54 | ), 55 | 56 | // Headline Small - Montserrat 24/32 . 0px 57 | headlineSmall = TextStyle( 58 | fontFamily = fontFamily, 59 | fontWeight = FontWeight.W400, 60 | fontSize = 24.sp, 61 | lineHeight = 32.sp, 62 | letterSpacing = 0.sp, 63 | ), 64 | 65 | // Title Large - Montserrat 22/28 . 0px 66 | titleLarge = TextStyle( 67 | fontFamily = fontFamily, 68 | fontWeight = FontWeight.W400, 69 | fontSize = 22.sp, 70 | lineHeight = 28.sp, 71 | letterSpacing = 0.sp, 72 | ), 73 | 74 | // Title Medium - Montserrat 16/24 . 0.15px 75 | titleMedium = TextStyle( 76 | fontFamily = fontFamily, 77 | fontWeight = FontWeight.W500, 78 | fontSize = 16.sp, 79 | lineHeight = 24.sp, 80 | letterSpacing = 0.15.sp, 81 | ), 82 | 83 | // Title Small - Montserrat 14/20 . 0.1px 84 | titleSmall = TextStyle( 85 | fontFamily = fontFamily, 86 | fontWeight = FontWeight.W500, 87 | fontSize = 14.sp, 88 | lineHeight = 20.sp, 89 | letterSpacing = 0.1.sp, 90 | ), 91 | 92 | // Label Large - Montserrat 14/20 . 0.1px 93 | labelLarge = TextStyle( 94 | fontFamily = fontFamily, 95 | fontWeight = FontWeight.W500, 96 | fontSize = 14.sp, 97 | lineHeight = 20.sp, 98 | letterSpacing = 0.1.sp, 99 | ), 100 | 101 | // Label Medium - Montserrat 12/16 . 0.5px 102 | labelMedium = TextStyle( 103 | fontFamily = fontFamily, 104 | fontWeight = FontWeight.W500, 105 | fontSize = 12.sp, 106 | lineHeight = 16.sp, 107 | letterSpacing = 0.5.sp, 108 | ), 109 | 110 | // Label Small - Montserrat 11/16 . 0.5px 111 | labelSmall = TextStyle( 112 | fontFamily = fontFamily, 113 | fontWeight = FontWeight.W500, 114 | fontSize = 11.sp, 115 | lineHeight = 16.sp, 116 | letterSpacing = 0.5.sp, 117 | ), 118 | 119 | // Body Large - Montserrat 16/24 . 0.5px 120 | bodyLarge = TextStyle( 121 | fontFamily = fontFamily, 122 | fontWeight = FontWeight.W400, 123 | fontSize = 16.sp, 124 | lineHeight = 24.sp, 125 | letterSpacing = 0.5.sp, 126 | ), 127 | 128 | // Body Medium - Montserrat 14/20 . 0.25px 129 | bodyMedium = TextStyle( 130 | fontFamily = fontFamily, 131 | fontWeight = FontWeight.W400, 132 | fontSize = 14.sp, 133 | lineHeight = 20.sp, 134 | letterSpacing = 0.25.sp, 135 | ), 136 | // Body Small - Montserrat 12/16 . 0.4px 137 | bodySmall = TextStyle( 138 | fontFamily = fontFamily, 139 | fontWeight = FontWeight.W400, 140 | fontSize = 12.sp, 141 | lineHeight = 16.sp, 142 | letterSpacing = 0.4.sp, 143 | ), 144 | ) 145 | -------------------------------------------------------------------------------- /library/src/commonMain/kotlin/data/validation/validation.kt: -------------------------------------------------------------------------------- 1 | package data.validation 2 | 3 | import arrow.core.Either 4 | import arrow.core.left 5 | import arrow.core.right 6 | import data.responses.ErrMessage 7 | import data.responses.toMessage 8 | import data.types.CountryCodes 9 | import data.types.Err 10 | 11 | data class ValidatedText(val text: String, val valid: Boolean) 12 | 13 | @Deprecated("Should be replaced with ValidationV2") 14 | interface Validation { 15 | fun apply(data: T): Either 16 | } 17 | 18 | interface ValidationV2 { 19 | fun apply(data: T): Either 20 | 21 | } 22 | 23 | fun genericValidation( 24 | message: String? = null, 25 | instruction: String = "", 26 | valid: (data: T) -> Boolean 27 | ): ValidationV2 = 28 | object : ValidationV2 { 29 | 30 | override fun apply(data: T): Either = 31 | if (valid(data)) { 32 | data.right() 33 | } else { 34 | Err.ValidationErr 35 | .Generic(RuntimeException(message ?: "$data is invalid"), instruction) 36 | .left() 37 | } 38 | 39 | } 40 | 41 | object OTPValidation : ValidationV2 { 42 | override fun apply(data: String): Either = 43 | if (data.length == 6) data.right() 44 | else Err.ValidationErr 45 | .TextValidationErr( 46 | RuntimeException("OTP should be 6 characters."), 47 | "We've sent an OTP in your phone/email. Please enter it here." 48 | ) 49 | .left() 50 | } 51 | 52 | class PhoneValidation(private val countryCode: CountryCodes) : ValidationV2 { 53 | override fun apply(data: String): Either { 54 | val phoneNumber = countryCode.dialingCode + data 55 | return if ( 56 | phoneNumber.isValidPhoneNumber() 57 | && data.length in countryCode.phoneLength 58 | ) { 59 | phoneNumber.right() 60 | } else { 61 | // Check if it's because of leading 0, then correct it. 62 | val message = if (data.startsWith("0")) 63 | "Invalid phone number. Please remove leading 0 before number." 64 | else "Invalid phone number." 65 | 66 | val (isRange, length) = if (countryCode.phoneLength.first == countryCode.phoneLength.last) { 67 | Pair(true, countryCode.phoneLength.first.toString()) 68 | } else Pair(false, countryCode.phoneLength.last.toString()) 69 | Err.ValidationErr 70 | .PhoneValidationErr( 71 | RuntimeException(message), 72 | "Length should be ${if (isRange) "in range" else ""} $length" 73 | ) 74 | .left() 75 | } 76 | } 77 | 78 | private fun String.isValidPhoneNumber() = Regex( 79 | "\\+(9[976]\\d|8[987530]\\d|6[987]\\d|5[90]\\d|42\\d|3[875]\\d|\n" + 80 | "2[98654321]\\d|9[8543210]|8[6421]|6[6543210]|5[87654321]|\n" + 81 | "4[987654310]|3[9643210]|2[70]|7|1)\\d{1,14}\$" 82 | ).matches(this) 83 | } 84 | 85 | class EmailValidation : ValidationV2 { 86 | override fun apply(data: String): Either = 87 | if ("^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,}$".toRegex().matches(data)) 88 | data.right() 89 | else Err.ValidationErr.EmailValidationErr( 90 | RuntimeException("Invalid email address"), 91 | "Please input a valid email address" 92 | ).left() 93 | } 94 | 95 | @Deprecated("Uses deprecated interface Validation, which should be replaced with ValidationV2") 96 | class GenericTextValidation( 97 | private val length: Int, 98 | private val message: String = "Text must be at least 6 digit" 99 | ) : Validation { 100 | 101 | override fun apply(data: String): Either { 102 | return if (data.length < length) 103 | Err.ValidationErr 104 | .TextValidationErr(RuntimeException(message), "") 105 | .toMessage().left() 106 | else data.right() 107 | } 108 | } 109 | 110 | class FieldIsRequired( 111 | private val message: String = "Field required" 112 | ) : Validation { 113 | 114 | override fun apply(data: String): Either { 115 | return if (data.isBlank()) 116 | Err.ValidationErr 117 | .TextValidationErr(RuntimeException(message), "") 118 | .toMessage().left() 119 | else data.right() 120 | } 121 | 122 | } 123 | -------------------------------------------------------------------------------- /library/src/commonMain/kotlin/configs/web-client.conf.kt: -------------------------------------------------------------------------------- 1 | package configs 2 | 3 | import androidx.datastore.core.DataStore 4 | import androidx.datastore.preferences.core.Preferences 5 | import androidx.datastore.preferences.core.stringPreferencesKey 6 | import arrow.core.Either 7 | import arrow.core.left 8 | import arrow.core.right 9 | import data.responses.ErrMessage 10 | import data.responses.ErrTypes 11 | import filters.HttpFilter 12 | import io.ktor.client.HttpClient 13 | import io.ktor.client.call.body 14 | import io.ktor.client.engine.HttpClientEngineConfig 15 | import io.ktor.client.engine.HttpClientEngineFactory 16 | import io.ktor.client.plugins.HttpTimeout 17 | import io.ktor.client.plugins.auth.providers.BearerAuthProvider 18 | import io.ktor.client.plugins.auth.providers.BearerTokens 19 | import io.ktor.client.plugins.auth.providers.bearer 20 | import io.ktor.client.plugins.contentnegotiation.ContentNegotiation 21 | import io.ktor.client.plugins.plugin 22 | import io.ktor.client.plugins.websocket.WebSockets 23 | import io.ktor.client.request.HttpRequestBuilder 24 | import io.ktor.client.request.forms.submitForm 25 | import io.ktor.client.statement.HttpResponse 26 | import io.ktor.http.parameters 27 | import io.ktor.serialization.kotlinx.json.json 28 | import kotlinx.coroutines.flow.first 29 | import kotlinx.coroutines.flow.map 30 | import kotlinx.serialization.json.Json 31 | import modules.common.AuthCredentials 32 | import modules.common.features.auth.models.Auth 33 | import modules.common.getKoinInstance 34 | import utils.Tag 35 | import utils.logD 36 | 37 | val authKey = stringPreferencesKey("auth") 38 | 39 | suspend fun getAuth(): Either { 40 | val dataStore = getKoinInstance>() 41 | return dataStore.data.map { pref -> 42 | pref[authKey]?.let { 43 | logD(Tag.Auth.LoadAuthFromStorage, it) 44 | Json.decodeFromString(it).right() 45 | } ?: ErrMessage( 46 | ErrTypes.NOT_AUTHENTICATED.type, 47 | ErrTypes.NOT_AUTHENTICATED.msg 48 | ).left() 49 | }.first() 50 | } 51 | 52 | suspend fun HttpClient.cleanupAuth(refreshTokenResponse: HttpResponse? = null) { 53 | if (refreshTokenResponse == null) { 54 | this.plugin(io.ktor.client.plugins.auth.Auth).providers 55 | .filterIsInstance() 56 | .firstOrNull()?.clearToken() 57 | } else { 58 | this.plugin(io.ktor.client.plugins.auth.Auth).providers 59 | .filterIsInstance() 60 | .firstOrNull()?.refreshToken(refreshTokenResponse) 61 | } 62 | } 63 | 64 | expect fun getEngine(): HttpClientEngineFactory 65 | 66 | fun ktorClient(cred: AuthCredentials) = HttpClient(getEngine()) { 67 | expectSuccess = true 68 | install(io.ktor.client.plugins.auth.Auth) { 69 | bearer { 70 | loadTokens { 71 | getAuth().fold( 72 | { null }, 73 | { BearerTokens(it.accessToken, it.refreshToken) } 74 | ) 75 | 76 | } 77 | refreshTokens { 78 | getAuth().fold( 79 | { null }, 80 | { 81 | logD(Tag.Auth.RefreshToken, "Initiating token refresh.") 82 | val refreshTokenInfo: Auth = 83 | refreshToken( 84 | client = client, 85 | tokenUrl = cred.tokenUrl, 86 | clientId = cred.clientId, 87 | clientSecret = cred.clientSecret, 88 | refreshToken = it.refreshToken 89 | ) { markAsRefreshTokenRequest() }.body() 90 | BearerTokens( 91 | refreshTokenInfo.accessToken, 92 | refreshTokenInfo.refreshToken 93 | ) 94 | } 95 | ) 96 | } 97 | } 98 | } 99 | install(ContentNegotiation) { 100 | json( 101 | Json { 102 | prettyPrint = true 103 | isLenient = false 104 | ignoreUnknownKeys = true 105 | } 106 | ) 107 | } 108 | install(WebSockets) 109 | install(HttpTimeout) 110 | } 111 | 112 | suspend fun HttpResponse.applyFilter(filter: HttpFilter) = filter.apply(this) 113 | 114 | 115 | suspend fun refreshToken( 116 | client: HttpClient, 117 | tokenUrl: String, 118 | clientId: String, 119 | clientSecret: String, 120 | refreshToken: String, 121 | block: HttpRequestBuilder.() -> Unit = {} 122 | ): HttpResponse = client.submitForm( 123 | url = tokenUrl, 124 | formParameters = parameters { 125 | append("grant_type", "refresh_token") 126 | append("client_id", clientId) 127 | append("client_secret", clientSecret) 128 | append("refresh_token", refreshToken) 129 | }, 130 | block = block 131 | ) -------------------------------------------------------------------------------- /library/src/commonMain/kotlin/configs/websocket.client.kt: -------------------------------------------------------------------------------- 1 | package configs 2 | 3 | import arrow.core.Option 4 | import arrow.core.none 5 | import arrow.core.toOption 6 | import kotlinx.coroutines.flow.Flow 7 | import kotlinx.serialization.DeserializationStrategy 8 | import kotlinx.serialization.KSerializer 9 | import modules.common.AuthCredentials 10 | import org.hildan.krossbow.stomp.StompClient 11 | import org.hildan.krossbow.stomp.StompSession 12 | import org.hildan.krossbow.stomp.conversions.kxserialization.StompSessionWithKxSerialization 13 | import org.hildan.krossbow.stomp.conversions.kxserialization.json.withJsonConversions 14 | import org.hildan.krossbow.stomp.headers.StompSendHeaders 15 | import org.hildan.krossbow.stomp.headers.StompSubscribeHeaders 16 | import org.hildan.krossbow.stomp.subscribeText 17 | import org.hildan.krossbow.stomp.use 18 | import org.hildan.krossbow.websocket.ktor.KtorWebSocketClient 19 | import utils.Tag 20 | import utils.logD 21 | 22 | fun wsClient(cred: AuthCredentials) = KtorWebSocketClient(ktorClient(cred)) 23 | fun stompClient(cred: AuthCredentials) = StompClient(wsClient(cred)) 24 | 25 | suspend fun socket(cred: AuthCredentials, url: String): StompSession = 26 | stompClient(cred).connect(url = url) 27 | 28 | suspend fun connectWithSerialization( 29 | cred: AuthCredentials, 30 | url: String 31 | ): StompSessionWithKxSerialization = 32 | stompClient(cred).connect(url).withJsonConversions() 33 | 34 | suspend fun StompSession.subscribeTopic(topic: String): Flow = 35 | this.subscribeText(destination = "/topic${topic}") 36 | 37 | suspend fun StompSessionWithKxSerialization.subscribeTopic( 38 | topic: String, deserializer: KSerializer 39 | ): Flow = this.subscribe( 40 | headers = StompSubscribeHeaders( 41 | destination = "/topic${topic}" 42 | ), deserializer 43 | ) 44 | 45 | suspend fun StompSessionWithKxSerialization.sub( 46 | topic: String, 47 | serializer: DeserializationStrategy 48 | ): Flow = 49 | this.subscribe( 50 | StompSubscribeHeaders( 51 | destination = topic 52 | ), 53 | serializer 54 | ) 55 | 56 | suspend fun StompSessionWithKxSerialization.push( 57 | destination: String, 58 | jsonObject: T, 59 | serializer: KSerializer 60 | ) { 61 | this.use { s -> 62 | s.convertAndSend( 63 | headers = StompSendHeaders(destination = destination), 64 | body = jsonObject, 65 | serializer = serializer 66 | ) 67 | } 68 | } 69 | 70 | class WSConnection(private val cred: AuthCredentials, private val socketURI: String) { 71 | private var session: Option = none() 72 | private var isConnected = false 73 | 74 | suspend fun init( 75 | url: String = socketURI, 76 | block: suspend (session: StompSessionWithKxSerialization) -> Unit 77 | ) { 78 | try { 79 | this.session.fold( 80 | { 81 | logD(Tag.Network.WebSocket, "Session doesn't exist, connecting..") 82 | val newSession = socket(cred = cred, url = url).withJsonConversions() 83 | this.session = newSession.toOption() 84 | this.isConnected = true 85 | logD(Tag.Network.WebSocket, "Connection established. Executing block().") 86 | this.init(url) { block(newSession) } 87 | } 88 | ) { 89 | if (this.isConnected) { 90 | logD( 91 | Tag.Network.WebSocket, 92 | "Session already exists and connected. Continuing execution." 93 | ) 94 | block(it) 95 | } else { 96 | logD( 97 | Tag.Network.WebSocket, 98 | "Session exist, but disconnected. " + 99 | "Proceeding to disconnect and creating new connection.." 100 | ) 101 | it.disconnect() 102 | val newSession = socket(cred = cred, url = url).withJsonConversions() 103 | this.session = newSession.toOption() 104 | this.isConnected = true 105 | logD(Tag.Network.WebSocket, "Success. Executing block().") 106 | this.init(url) { block(newSession) } 107 | } 108 | } 109 | this.isConnected = true 110 | } catch (e: Exception) { 111 | logD(Tag.Network.WebSocket, "Error Occurred. Disconnecting session..") 112 | logD(Tag.Network.WebSocket, e.toString()) 113 | this.isConnected = false 114 | if (this.session.isSome()) { 115 | session = none() 116 | logD(Tag.Network.WebSocket, "clearing session because of error.") 117 | } 118 | } 119 | } 120 | 121 | suspend fun close() = session.onSome { 122 | if (this.isConnected) it.disconnect() 123 | session = none() 124 | } 125 | } -------------------------------------------------------------------------------- /shared/src/commonMain/composeResources/drawable/compose-multiplatform.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 14 | 18 | 24 | 30 | 36 | 37 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/modules/common/features/auth/AuthRepository.kt: -------------------------------------------------------------------------------- 1 | package modules.common.features.auth 2 | 3 | import arrow.core.Either 4 | import configs.Credentials 5 | import data.types.Err 6 | import io.ktor.client.HttpClient 7 | import io.ktor.client.call.body 8 | import io.ktor.client.request.HttpRequestBuilder 9 | import io.ktor.client.request.forms.submitForm 10 | import io.ktor.client.request.get 11 | import io.ktor.client.request.post 12 | import io.ktor.client.request.setBody 13 | import io.ktor.client.request.url 14 | import io.ktor.client.statement.HttpResponse 15 | import io.ktor.http.ContentType 16 | import io.ktor.http.contentType 17 | import io.ktor.http.parameters 18 | import modules.common.features.auth.models.Auth 19 | import modules.common.features.auth.models.AuthErr 20 | import modules.common.features.auth.models.FirebaseToken 21 | import modules.common.features.auth.models.FirebaseTokenReq 22 | import modules.common.features.auth.models.SignUpReq 23 | import modules.common.features.auth.models.UsernameAvailableResponse 24 | import modules.common.features.auth.models.VerificationResponse 25 | import modules.common.models.ErrResponse 26 | import modules.common.routes.Routes 27 | import utils.RemoteResult 28 | import utils.result 29 | import utils.resultSingleWithHeaders 30 | 31 | interface AuthRepository { 32 | suspend fun token(username: String, password: String): Either, Auth> 33 | suspend fun refreshToken(refreshToken: String): HttpResponse 34 | suspend fun checkUsername(username: String): Either, UsernameAvailableResponse> 35 | suspend fun requestOTP(phoneOrEmail: String): Either, RemoteResult> 36 | suspend fun signup(token: String, signUpReq: SignUpReq): Either, Auth> 37 | suspend fun registerFirebaseToken( 38 | tokenReq: FirebaseTokenReq 39 | ): Either, FirebaseToken> 40 | } 41 | 42 | class AuthRepositoryImpl( 43 | private val httpClient: HttpClient 44 | ) : AuthRepository { 45 | 46 | override suspend fun token( 47 | username: String, 48 | password: String 49 | ): Either, Auth> = 50 | result { 51 | httpClient.submitForm( 52 | url = Routes.GET_TOKEN, 53 | formParameters = parameters { 54 | append("username", username.trim()) 55 | append("password", password) 56 | append("client_id", Credentials.Auth.clientId.get()) 57 | append("client_secret", Credentials.Auth.clientSecret.get()) 58 | append("grant_type", "password") 59 | } 60 | ).body() 61 | } 62 | 63 | override suspend fun refreshToken(refreshToken: String): HttpResponse = 64 | refreshToken( 65 | client = httpClient, 66 | clientId = Credentials.Auth.clientId.get(), 67 | clientSecret = Credentials.Auth.clientSecret.get(), 68 | refreshToken = refreshToken 69 | ) 70 | 71 | override suspend fun checkUsername(username: String): Either, UsernameAvailableResponse> = 72 | result { 73 | this.httpClient.get { 74 | url(Routes.checkUsername(username)) 75 | }.body() 76 | } 77 | 78 | override suspend fun requestOTP(phoneOrEmail: String): Either, RemoteResult> = 79 | resultSingleWithHeaders { 80 | this.httpClient.post { 81 | url(Routes.getOTP(phoneOrEmail)) 82 | } 83 | } 84 | 85 | override suspend fun signup( 86 | token: String, 87 | signUpReq: SignUpReq 88 | ): Either, Auth> = 89 | result { 90 | this.httpClient.post { 91 | url(Routes.signup(token)) 92 | contentType(ContentType.Application.Json) 93 | setBody(signUpReq) 94 | }.body() 95 | } 96 | 97 | override suspend fun registerFirebaseToken( 98 | tokenReq: FirebaseTokenReq 99 | ): Either, FirebaseToken> = 100 | result { 101 | this.httpClient.post { 102 | url(Routes.registerFirebaseToken()) 103 | contentType(ContentType.Application.Json) 104 | setBody(tokenReq) 105 | }.body() 106 | } 107 | 108 | } 109 | 110 | suspend fun refreshToken( 111 | client: HttpClient, 112 | clientId: String, 113 | clientSecret: String, 114 | refreshToken: String, 115 | block: HttpRequestBuilder.() -> Unit = {} 116 | ) = client.submitForm( 117 | url = Routes.GET_TOKEN, 118 | formParameters = parameters { 119 | append("grant_type", "refresh_token") 120 | append("client_id", clientId) 121 | append("client_secret", clientSecret) 122 | append("refresh_token", refreshToken) 123 | }, 124 | block = block 125 | ) 126 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/utils/ui-utils.kt: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.material3.SnackbarDuration 5 | import androidx.compose.material3.SnackbarHostState 6 | import androidx.compose.material3.SnackbarResult 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.collectAsState 9 | import androidx.compose.ui.graphics.Color 10 | import arrow.core.none 11 | import configs.AppThemes 12 | import kotlinx.coroutines.CoroutineScope 13 | import kotlinx.coroutines.launch 14 | import modules.common.features.preferences.PrefKeys 15 | import modules.common.features.preferences.PrefVM 16 | import kotlin.math.abs 17 | import kotlin.math.roundToInt 18 | import kotlin.random.Random 19 | import kotlin.random.nextInt 20 | 21 | fun Pair.show( 22 | message: String, 23 | actionLabel: String? = null, 24 | withDismissAction: Boolean = false, 25 | duration: SnackbarDuration = if (withDismissAction) { 26 | SnackbarDuration.Indefinite 27 | } else { 28 | SnackbarDuration.Short 29 | }, 30 | dismissAction: () -> Unit = {}, 31 | onAction: () -> Unit = {} 32 | ) { 33 | val (snackbar, scope) = this 34 | scope.launch { 35 | val result = snackbar.showSnackbar( 36 | message = message, 37 | actionLabel = actionLabel, 38 | withDismissAction = withDismissAction, 39 | duration = duration 40 | ) 41 | 42 | when (result) { 43 | SnackbarResult.ActionPerformed -> onAction() 44 | SnackbarResult.Dismissed -> { 45 | dismissAction() 46 | } 47 | } 48 | } 49 | } 50 | 51 | fun avatarUrl(str: String, size: Int = 128) = "https://ui-avatars.com/api/?name=$str&size=$size" 52 | 53 | fun parseColor(color: String) = try { 54 | Color(color.removePrefix("#").toLong(16) or 0x00000000FF000000) 55 | } catch (e: NumberFormatException) { 56 | Color.Gray 57 | } 58 | 59 | @Composable 60 | fun PrefVM.isDarkTheme(fetchBlocking: Boolean = false) = 61 | if (!fetchBlocking) { 62 | this.getPref(PrefKeys.theme).collectAsState(none()).value 63 | } else { 64 | this.getPrefBlocking(PrefKeys.theme) 65 | }.fold({ isSystemInDarkTheme() }, 66 | { AppThemes.DARK.name == it }) 67 | 68 | val randomColors = arrayOf( 69 | "#F44336", "#E91E63", "#9C27B0", "#673AB7", "#3F51B5", 70 | "#2196F3", "#03A9F4", "#00BCD4", "#009688", "#4CAF50", 71 | "#8BC34A", "#CDDC39", "#FFEB3B", "#FFC107", "#FF9800", 72 | "#FF5722", "#795548", "#9E9E9E", "#607D8B", "#B71C1C", 73 | "#880E4F", "#4A148C", "#311B92", "#1A237E", "#0D47A1", 74 | "#01579B", "#006064", "#004D40", "#1B5E20", "#33691E", 75 | "#827717", "#F57F17", "#FF6F00", "#E65100", "#BF360C", 76 | "#3E2723", "#212121", "#263238", "#D32F2F", "#C2185B", 77 | "#7B1FA2", "#512DA8", "#303F9F", "#1976D2", "#0288D1", 78 | "#0097A7", "#00796B", "#388E3C", "#689F38", "#AFB42B", 79 | "#FBC02D", "#FFA000", "#F57C00", "#E64A19", "#5D4037" 80 | ) 81 | 82 | fun randomColorFromList(num: Int? = null) = num?.let { 83 | randomColors[num % randomColors.size] 84 | } ?: randomColors[Random.nextInt(randomColors.indices)] 85 | 86 | fun randomVividColor(number: Float? = null): Color { 87 | // Generate a random hue from 0 to 360 88 | val num = number ?: Random.nextFloat() 89 | val hue = num * 360 90 | 91 | // Set high saturation. For vivid colors, saturation is often near 1.0 92 | val saturation = 0.8f + num * 0.2f // 80% to 100% 93 | 94 | // Set lightness. Avoid going too high to maintain vividness 95 | val lightness = 0.5f + num * 0.3f // 50% to 80% 96 | 97 | // Convert HSL to RGB 98 | return hslToRgb(hue, saturation, lightness) 99 | } 100 | 101 | fun numberToMediumRangeColor(number: Int): Color { 102 | // Normalize the number to a hue value (0-360) 103 | val hue = (number % 360).toFloat() 104 | 105 | // Set medium saturation and lightness values 106 | val saturation = 0.5f // 50% saturation for medium vividness 107 | val lightness = 0.5f // 50% lightness for medium brightness/darkness 108 | 109 | // Convert HSL to RGB 110 | return hslToRgb(hue, saturation, lightness) 111 | } 112 | 113 | fun hslToRgb(hue: Float, saturation: Float, lightness: Float): Color { 114 | val c = (1 - abs(2 * lightness - 1)) * saturation 115 | val x = c * (1 - abs((hue / 60.0) % 2 - 1)).toFloat() 116 | val m = lightness - c / 2 117 | 118 | val (r, g, b) = when { 119 | hue < 60 -> Triple(c, x, 0f) 120 | hue < 120 -> Triple(x, c, 0f) 121 | hue < 180 -> Triple(0f, c, x) 122 | hue < 240 -> Triple(0f, x, c) 123 | hue < 300 -> Triple(x, 0f, c) 124 | else -> Triple(c, 0f, x) 125 | } 126 | 127 | return Color( 128 | red = ((r + m) * 255).roundToInt(), 129 | green = ((g + m) * 255).roundToInt(), 130 | blue = ((b + m) * 255).roundToInt() 131 | ) 132 | } 133 | 134 | fun randomColor(): Color { 135 | return Color( 136 | red = Random.nextFloat(), 137 | green = Random.nextFloat(), 138 | blue = Random.nextFloat(), 139 | alpha = 1f // Full opacity 140 | ) 141 | } 142 | 143 | fun getColorFromNumber(number: Int): Color { 144 | // Use the number to generate a hex color code. 145 | // We use `0xFFFFFF` (16777215 in decimal) to ensure the number fits within the RGB color range. 146 | val hexColor = 0xFFFFFF and number 147 | 148 | // Convert the hex color to a Color value. 149 | return Color( 150 | red = (hexColor shr 16) and 0xFF, 151 | green = (hexColor shr 8) and 0xFF, 152 | blue = hexColor and 0xFF, 153 | alpha = 0xFF // Full opacity 154 | ) 155 | } -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/modules/common/views/layouts/generic-layout.kt: -------------------------------------------------------------------------------- 1 | package modules.common.views.layouts 2 | 3 | import androidx.compose.foundation.layout.PaddingValues 4 | import androidx.compose.foundation.layout.RowScope 5 | import androidx.compose.material.icons.Icons 6 | import androidx.compose.material.icons.filled.Add 7 | import androidx.compose.material3.ExperimentalMaterial3Api 8 | import androidx.compose.material3.ExtendedFloatingActionButton 9 | import androidx.compose.material3.FabPosition 10 | import androidx.compose.material3.FloatingActionButton 11 | import androidx.compose.material3.Icon 12 | import androidx.compose.material3.MaterialTheme 13 | import androidx.compose.material3.NavigationBar 14 | import androidx.compose.material3.NavigationBarItem 15 | import androidx.compose.material3.NavigationBarItemDefaults 16 | import androidx.compose.material3.Scaffold 17 | import androidx.compose.material3.SnackbarHost 18 | import androidx.compose.material3.SnackbarHostState 19 | import androidx.compose.material3.Text 20 | import androidx.compose.material3.TopAppBarDefaults 21 | import androidx.compose.material3.rememberTopAppBarState 22 | import androidx.compose.runtime.Composable 23 | import androidx.compose.runtime.getValue 24 | import androidx.compose.runtime.mutableStateOf 25 | import androidx.compose.runtime.remember 26 | import androidx.compose.runtime.rememberCoroutineScope 27 | import androidx.compose.runtime.setValue 28 | import androidx.compose.ui.Modifier 29 | import androidx.compose.ui.graphics.vector.ImageVector 30 | import androidx.compose.ui.text.style.TextAlign 31 | import androidx.compose.ui.unit.dp 32 | import kotlinx.coroutines.CoroutineScope 33 | import modules.common.animations.EnterAnimation 34 | import modules.common.views.components.AppBarTop 35 | import modules.common.views.components.AutoResizeText 36 | import modules.common.views.components.FontSizeRange 37 | 38 | @OptIn(ExperimentalMaterial3Api::class) 39 | @Composable 40 | fun BasicLayout( 41 | logo: @Composable () -> Unit, 42 | title: @Composable () -> Unit, 43 | actions: @Composable RowScope.() -> Unit = {}, 44 | fabVisibility: Boolean = false, 45 | fabText: String = "", 46 | fabClick: () -> Unit = {}, 47 | fabIcon: ImageVector= Icons.Default.Add, 48 | bottomNavigation: @Composable () -> Unit = {}, 49 | contentView: @Composable (paddingValues: PaddingValues, snackbar: Pair) -> Unit 50 | ) { 51 | val snackbarHostState = remember { SnackbarHostState() } 52 | Scaffold( 53 | snackbarHost = { 54 | SnackbarHost(hostState = snackbarHostState) 55 | }, 56 | topBar = { 57 | AppBarTop( 58 | logo = logo, 59 | scrollBehavior = TopAppBarDefaults 60 | .pinnedScrollBehavior(rememberTopAppBarState()), 61 | title = { title() }, 62 | actions = actions 63 | ) 64 | }, 65 | bottomBar = bottomNavigation, 66 | floatingActionButtonPosition = FabPosition.End, 67 | floatingActionButton = { 68 | if (fabVisibility) { 69 | if (fabText.isNotBlank()) { 70 | ExtendedFloatingActionButton( 71 | onClick = fabClick, 72 | containerColor = MaterialTheme.colorScheme.secondaryContainer, 73 | contentColor = MaterialTheme.colorScheme.secondary 74 | ) { 75 | Icon(fabIcon, fabText) 76 | Text(fabText) 77 | } 78 | } else { 79 | FloatingActionButton( 80 | onClick = fabClick, 81 | containerColor = MaterialTheme.colorScheme.secondaryContainer, 82 | contentColor = MaterialTheme.colorScheme.secondary 83 | ) { 84 | Icon(fabIcon, "") 85 | } 86 | } 87 | } 88 | } 89 | ) { 90 | EnterAnimation { 91 | contentView(it, Pair(snackbarHostState, rememberCoroutineScope())) 92 | } 93 | } 94 | } 95 | 96 | 97 | data class NavDef( 98 | val name: String, 99 | val icon: ImageVector 100 | ) 101 | 102 | @Composable 103 | fun GenericNavigation( 104 | modifier: Modifier = Modifier, 105 | navItems: Set, 106 | initialSelection: NavDef, 107 | onItemSelected: (item: NavDef) -> Unit 108 | ) { 109 | NavigationBar( 110 | modifier = modifier, 111 | containerColor = MaterialTheme.colorScheme.background, 112 | contentColor = MaterialTheme.colorScheme.primary, 113 | tonalElevation = 4.dp 114 | ) { 115 | var selected by remember { mutableStateOf(initialSelection) } 116 | navItems.forEach { item -> 117 | NavigationBarItem( 118 | colors = NavigationBarItemDefaults.colors(), 119 | icon = { 120 | Icon( 121 | imageVector = item.icon, 122 | contentDescription = null, 123 | tint = MaterialTheme.colorScheme.secondary 124 | ) 125 | }, 126 | label = { 127 | AutoResizeText( 128 | text = item.name, 129 | color = MaterialTheme.colorScheme.secondary, 130 | fontSizeRange = FontSizeRange( 131 | min = MaterialTheme.typography.labelSmall.fontSize, 132 | max = MaterialTheme.typography.labelLarge.fontSize 133 | ), 134 | maxLines = 1, 135 | textAlign = TextAlign.Center 136 | ) 137 | }, 138 | selected = selected == item, 139 | alwaysShowLabel = false, // This hides the title for the unselected items 140 | onClick = { 141 | // This if check gives us a "singleTop" behavior where we do not create a 142 | // second instance of the composable if we are already on that destination 143 | if (selected != item) { 144 | selected = item 145 | onItemSelected(item) 146 | } 147 | } 148 | ) 149 | 150 | } 151 | } 152 | } --------------------------------------------------------------------------------