Skip to content

Commit f234577

Browse files
authored
Merge branch 'development' into fastlaneIOS
2 parents 37548bc + b3ed04e commit f234577

36 files changed

Lines changed: 2258 additions & 587 deletions

File tree

cmp-android/dependencies/demoDebugRuntimeClasspath.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,8 @@ org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.10.2
431431
org.jetbrains.kotlinx:kotlinx-coroutines-slf4j:1.10.2
432432
org.jetbrains.kotlinx:kotlinx-datetime-jvm:0.7.1
433433
org.jetbrains.kotlinx:kotlinx-datetime:0.7.1
434+
org.jetbrains.kotlinx:kotlinx-html-jvm:0.12.0
435+
org.jetbrains.kotlinx:kotlinx-html:0.12.0
434436
org.jetbrains.kotlinx:kotlinx-io-bytestring-jvm:0.8.0
435437
org.jetbrains.kotlinx:kotlinx-io-bytestring:0.8.0
436438
org.jetbrains.kotlinx:kotlinx-io-core-jvm:0.8.0

cmp-android/dependencies/demoReleaseRuntimeClasspath.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,8 @@ org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.10.2
429429
org.jetbrains.kotlinx:kotlinx-coroutines-slf4j:1.10.2
430430
org.jetbrains.kotlinx:kotlinx-datetime-jvm:0.7.1
431431
org.jetbrains.kotlinx:kotlinx-datetime:0.7.1
432+
org.jetbrains.kotlinx:kotlinx-html-jvm:0.12.0
433+
org.jetbrains.kotlinx:kotlinx-html:0.12.0
432434
org.jetbrains.kotlinx:kotlinx-io-bytestring-jvm:0.8.0
433435
org.jetbrains.kotlinx:kotlinx-io-bytestring:0.8.0
434436
org.jetbrains.kotlinx:kotlinx-io-core-jvm:0.8.0

cmp-android/dependencies/prodDebugRuntimeClasspath.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,8 @@ org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.10.2
431431
org.jetbrains.kotlinx:kotlinx-coroutines-slf4j:1.10.2
432432
org.jetbrains.kotlinx:kotlinx-datetime-jvm:0.7.1
433433
org.jetbrains.kotlinx:kotlinx-datetime:0.7.1
434+
org.jetbrains.kotlinx:kotlinx-html-jvm:0.12.0
435+
org.jetbrains.kotlinx:kotlinx-html:0.12.0
434436
org.jetbrains.kotlinx:kotlinx-io-bytestring-jvm:0.8.0
435437
org.jetbrains.kotlinx:kotlinx-io-bytestring:0.8.0
436438
org.jetbrains.kotlinx:kotlinx-io-core-jvm:0.8.0

cmp-android/dependencies/prodReleaseRuntimeClasspath.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,8 @@ org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.10.2
429429
org.jetbrains.kotlinx:kotlinx-coroutines-slf4j:1.10.2
430430
org.jetbrains.kotlinx:kotlinx-datetime-jvm:0.7.1
431431
org.jetbrains.kotlinx:kotlinx-datetime:0.7.1
432+
org.jetbrains.kotlinx:kotlinx-html-jvm:0.12.0
433+
org.jetbrains.kotlinx:kotlinx-html:0.12.0
432434
org.jetbrains.kotlinx:kotlinx-io-bytestring-jvm:0.8.0
433435
org.jetbrains.kotlinx:kotlinx-io-bytestring:0.8.0
434436
org.jetbrains.kotlinx:kotlinx-io-core-jvm:0.8.0
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/*
2+
* Copyright 2026 Mifos Initiative
3+
*
4+
* This Source Code Form is subject to the terms of the Mozilla Public
5+
* License, v. 2.0. If a copy of the MPL was not distributed with this
6+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
7+
*
8+
* See https://github.com/openMF/mifos-x-field-officer-app/blob/master/LICENSE.md
9+
*/
10+
package com.mifos.core.designsystem.component
11+
12+
import androidx.compose.foundation.background
13+
import androidx.compose.foundation.clickable
14+
import androidx.compose.foundation.layout.Box
15+
import androidx.compose.foundation.layout.Column
16+
import androidx.compose.foundation.layout.IntrinsicSize
17+
import androidx.compose.foundation.layout.Row
18+
import androidx.compose.foundation.layout.fillMaxWidth
19+
import androidx.compose.foundation.layout.height
20+
import androidx.compose.foundation.layout.padding
21+
import androidx.compose.foundation.layout.width
22+
import androidx.compose.material3.HorizontalDivider
23+
import androidx.compose.material3.VerticalDivider
24+
import androidx.compose.runtime.Composable
25+
import androidx.compose.ui.Alignment
26+
import androidx.compose.ui.Modifier
27+
import androidx.compose.ui.draw.clip
28+
import androidx.compose.ui.graphics.Color
29+
import androidx.compose.ui.graphics.Shape
30+
import androidx.compose.ui.unit.Dp
31+
import androidx.compose.ui.unit.dp
32+
import com.mifos.core.designsystem.theme.DesignToken
33+
import template.core.base.designsystem.theme.KptTheme
34+
35+
@Composable
36+
fun MifosTableRow(
37+
cells: List<@Composable () -> Unit>,
38+
widths: List<Dp>,
39+
modifier: Modifier = Modifier,
40+
edgeOffset: Dp = 0.dp,
41+
backgroundColor: Color = KptTheme.colorScheme.surfaceVariant,
42+
cornerShape: Shape = DesignToken.shapes.none,
43+
showTopBorder: Boolean = false,
44+
showBottomBorder: Boolean = true,
45+
showSideBorders: Boolean = true,
46+
borderColor: Color = KptTheme.colorScheme.outlineVariant,
47+
onClick: () -> Unit = {},
48+
) {
49+
Column(
50+
modifier = modifier.fillMaxWidth(),
51+
) {
52+
if (showTopBorder) {
53+
HorizontalDivider(
54+
modifier = Modifier
55+
.padding(horizontal = edgeOffset),
56+
thickness = 0.5.dp,
57+
color = borderColor,
58+
)
59+
}
60+
Row(
61+
modifier = Modifier
62+
.height(IntrinsicSize.Min)
63+
.padding(horizontal = edgeOffset)
64+
.clip(cornerShape)
65+
.background(backgroundColor)
66+
.clickable { onClick() },
67+
) {
68+
cells.forEachIndexed { index, content ->
69+
val cellWidth = widths.getOrElse(index) { DesignToken.sizes.tableCellWidthMedium }
70+
if (showSideBorders) {
71+
VerticalDivider(
72+
thickness = 0.5.dp,
73+
color = borderColor,
74+
)
75+
}
76+
Box(modifier = Modifier.width(cellWidth)) {
77+
content()
78+
if (showBottomBorder) {
79+
HorizontalDivider(
80+
modifier = Modifier.align(Alignment.BottomStart),
81+
thickness = 0.5.dp,
82+
color = borderColor,
83+
)
84+
}
85+
}
86+
if (showSideBorders && index == cells.lastIndex) {
87+
VerticalDivider(
88+
thickness = 0.5.dp,
89+
color = borderColor,
90+
)
91+
}
92+
}
93+
}
94+
}
95+
}

core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/icon/MifosIcons.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import androidx.compose.material.icons.filled.FilterList
3737
import androidx.compose.material.icons.filled.FlashOff
3838
import androidx.compose.material.icons.filled.FlashOn
3939
import androidx.compose.material.icons.filled.Info
40+
import androidx.compose.material.icons.filled.IosShare
4041
import androidx.compose.material.icons.filled.Link
4142
import androidx.compose.material.icons.filled.LocationOn
4243
import androidx.compose.material.icons.filled.Lock
@@ -252,4 +253,6 @@ object MifosIcons {
252253
val Copy = Icons.Outlined.ContentCopy
253254
val Currency = Icons.Outlined.CurrencyExchange
254255
val Camera = Icons.Outlined.Camera
256+
257+
val Export = Icons.Default.IosShare
255258
}

core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/theme/DesignToken.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ data class AppShapes(
240240
val topCornerDp8: Shape = RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp),
241241
val topCornerDp16: Shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp),
242242
val bottomCornerDp12: Shape = RoundedCornerShape(bottomStart = 12.dp, bottomEnd = 12.dp),
243+
val bottomCornerDp8: Shape = RoundedCornerShape(bottomStart = 8.dp, bottomEnd = 8.dp),
243244
val dp2: Shape = RoundedCornerShape(2.dp),
244245
val dp10: Shape = RoundedCornerShape(10.dp),
245246
val dp22: Shape = RoundedCornerShape(22.dp),

core/ui/build.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,11 @@ kotlin{
5252
implementation(libs.crop.krop.ui)
5353
implementation(libs.compottie.resources)
5454
implementation(libs.compottie.lite)
55+
implementation(libs.kotlinx.html)
56+
}
57+
desktopMain.dependencies {
58+
implementation(libs.openhtmltopdf.pdfbox)
59+
implementation(libs.openhtmltopdf.svg.support)
5560
}
5661
}
5762
}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
/*
2+
* Copyright 2026 Mifos Initiative
3+
*
4+
* This Source Code Form is subject to the terms of the Mozilla Public
5+
* License, v. 2.0. If a copy of the MPL was not distributed with this
6+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
7+
*
8+
* See https://github.com/openMF/mifos-x-field-officer-app/blob/master/LICENSE.md
9+
*/
10+
package com.mifos.core.ui.util.pdf
11+
12+
import android.content.Context
13+
import android.print.PrintAttributes
14+
import android.print.PrintManager
15+
import android.webkit.WebView
16+
import android.webkit.WebViewClient
17+
import androidx.core.content.getSystemService
18+
import kotlinx.coroutines.Dispatchers
19+
import kotlinx.coroutines.suspendCancellableCoroutine
20+
import kotlinx.coroutines.withContext
21+
import kotlin.coroutines.resume
22+
import kotlin.coroutines.resumeWithException
23+
24+
/**
25+
* Android implementation of PDF generator using WebView print adapter.
26+
*/
27+
actual class PdfGenerator {
28+
29+
private var context: Context? = null
30+
31+
fun setContext(context: Context) {
32+
this.context = context
33+
}
34+
35+
actual suspend fun generateAndSharePdf(
36+
htmlContent: String,
37+
fileName: String,
38+
pageConfig: PageConfig,
39+
) {
40+
withContext(Dispatchers.Main) {
41+
val appContext = context ?: throw Exception("Context not initialized")
42+
43+
val finalHtml = run {
44+
val orientation = if (pageConfig.orientation == Orientation.LANDSCAPE) {
45+
"landscape"
46+
} else {
47+
"portrait"
48+
}
49+
val size = when (pageConfig.size) {
50+
PageSize.A4 -> "A4"
51+
PageSize.LETTER -> "letter"
52+
}
53+
val pageCss =
54+
"@page { size: $size $orientation; margin: ${pageConfig.marginMm}mm; }"
55+
htmlContent.replace("/* PAGE_CONFIG_PLACEHOLDER */", pageCss)
56+
}
57+
58+
var webView: WebView? = WebView(appContext)
59+
60+
try {
61+
suspendCancellableCoroutine { continuation ->
62+
val currentWebView = webView ?: return@suspendCancellableCoroutine
63+
64+
continuation.invokeOnCancellation {
65+
currentWebView.stopLoading()
66+
currentWebView.destroy()
67+
webView = null
68+
}
69+
70+
currentWebView.settings.javaScriptEnabled = false
71+
currentWebView.webViewClient = object : WebViewClient() {
72+
override fun onPageFinished(view: WebView?, url: String?) {
73+
try {
74+
val printManager = appContext.getSystemService<PrintManager>()
75+
?: throw Exception("PrintManager not available")
76+
77+
val attributes = createPrintAttributes(pageConfig)
78+
val nativeAdapter = view?.createPrintDocumentAdapter(fileName)
79+
?: throw Exception("PrintDocumentAdapter is null")
80+
81+
printManager.print(fileName, nativeAdapter, attributes)
82+
83+
if (continuation.isActive) continuation.resume(Unit)
84+
} catch (e: Exception) {
85+
if (continuation.isActive) continuation.resumeWithException(e)
86+
}
87+
}
88+
89+
override fun onReceivedError(
90+
view: WebView?,
91+
errorCode: Int,
92+
description: String?,
93+
failingUrl: String?,
94+
) {
95+
if (continuation.isActive) {
96+
continuation.resumeWithException(Exception("WebView Error: $description"))
97+
}
98+
}
99+
}
100+
101+
currentWebView.loadDataWithBaseURL(null, finalHtml, "text/html", "UTF-8", null)
102+
}
103+
} finally {
104+
// If webView wasn't nulled by cancellation, clean it up now
105+
webView?.let { activeView ->
106+
activeView.post {
107+
activeView.stopLoading()
108+
activeView.destroy()
109+
}
110+
webView = null
111+
}
112+
}
113+
}
114+
}
115+
116+
private fun createPrintAttributes(pageConfig: PageConfig): PrintAttributes {
117+
val isLandscape = pageConfig.orientation == Orientation.LANDSCAPE
118+
val mediaSize = when (pageConfig.size) {
119+
PageSize.A4 -> if (isLandscape) {
120+
PrintAttributes.MediaSize.ISO_A4.asLandscape()
121+
} else {
122+
PrintAttributes.MediaSize.ISO_A4
123+
}
124+
125+
PageSize.LETTER -> if (isLandscape) {
126+
PrintAttributes.MediaSize.NA_LETTER.asLandscape()
127+
} else {
128+
PrintAttributes.MediaSize.NA_LETTER
129+
}
130+
}
131+
val marginMils = (pageConfig.marginMm * 39.3701).toInt()
132+
133+
return PrintAttributes.Builder()
134+
.setMediaSize(mediaSize)
135+
.setColorMode(PrintAttributes.COLOR_MODE_COLOR)
136+
.setMinMargins(
137+
PrintAttributes.Margins(
138+
marginMils,
139+
marginMils,
140+
marginMils,
141+
marginMils,
142+
),
143+
)
144+
.build()
145+
}
146+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/*
2+
* Copyright 2026 Mifos Initiative
3+
*
4+
* This Source Code Form is subject to the terms of the Mozilla Public
5+
* License, v. 2.0. If a copy of the MPL was not distributed with this
6+
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
7+
*
8+
* See https://github.com/openMF/mifos-x-field-officer-app/blob/master/LICENSE.md
9+
*/
10+
package com.mifos.core.ui.util.pdf
11+
12+
import androidx.compose.runtime.Composable
13+
import androidx.compose.runtime.remember
14+
import androidx.compose.ui.platform.LocalContext
15+
16+
/**
17+
* Android-specific helper to create PdfGenerator with Context.
18+
*/
19+
@Composable
20+
actual fun rememberPdfGenerator(): PdfGenerator {
21+
val context = LocalContext.current
22+
return remember(context) {
23+
PdfGenerator().apply {
24+
setContext(context)
25+
}
26+
}
27+
}

0 commit comments

Comments
 (0)