feat: architecture refactor — core layer, spreadsheet engine, envelope bidding, tests
CI / quality (push) Waiting to run
CI / quality (push) Waiting to run
- Add src/core/ (config, errors, events, http, observability, storage, query) - Add src/Spreadsheet/ engine with native POI backend and Excel bridge - Add Envelope C spreadsheet flow and tender bidding envelope modules - Consolidate components into src/components/, remove legacy Component/ and Hooks/ - Add registration state/validation, auth session guard, profile edit modules - Add unit tests across axios, redux, bidding, registration, encryption, core - Add CI workflow, docs (architecture, audit, DSC API spec), keystore example - Remove tracked keystore binary; ignore scripts.zip Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
9acec328a7
commit
1d7646d938
Binary file not shown.
@@ -72,7 +72,22 @@ def enableProguardInReleaseBuilds = false
|
||||
*/
|
||||
def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+'
|
||||
|
||||
android {
|
||||
/**
|
||||
* Release signing credentials live in android/keystore.properties (gitignored).
|
||||
* See android/keystore.properties.example. In CI, write the file from secrets
|
||||
* before building. Local dev builds without it fall back to the debug key and
|
||||
* MUST NOT be distributed.
|
||||
*/
|
||||
def keystorePropertiesFile = rootProject.file("keystore.properties")
|
||||
def keystoreProperties = new Properties()
|
||||
def hasReleaseSigning = keystorePropertiesFile.exists()
|
||||
if (hasReleaseSigning) {
|
||||
keystorePropertiesFile.withInputStream { keystoreProperties.load(it) }
|
||||
} else {
|
||||
logger.warn("WARNING: android/keystore.properties not found — release build will be DEBUG-SIGNED and must not be distributed.")
|
||||
}
|
||||
|
||||
android {
|
||||
ndkVersion rootProject.ext.ndkVersion
|
||||
buildToolsVersion rootProject.ext.buildToolsVersion
|
||||
compileSdk rootProject.ext.compileSdkVersion
|
||||
@@ -92,25 +107,50 @@ android {
|
||||
keyAlias 'androiddebugkey'
|
||||
keyPassword 'android'
|
||||
}
|
||||
if (hasReleaseSigning) {
|
||||
release {
|
||||
storeFile file(keystoreProperties['storeFile'])
|
||||
storePassword keystoreProperties['storePassword']
|
||||
keyAlias keystoreProperties['keyAlias']
|
||||
keyPassword keystoreProperties['keyPassword']
|
||||
}
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
debug {
|
||||
signingConfig signingConfigs.debug
|
||||
}
|
||||
release {
|
||||
// Caution! In production, you need to generate your own keystore file.
|
||||
// see http://reactnative.dev/docs/signed-apk-android.
|
||||
signingConfig signingConfigs.debug
|
||||
signingConfig hasReleaseSigning ? signingConfigs.release : signingConfigs.debug
|
||||
minifyEnabled enableProguardInReleaseBuilds
|
||||
proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
|
||||
}
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
resources {
|
||||
excludes += [
|
||||
"META-INF/DEPENDENCIES",
|
||||
"META-INF/LICENSE",
|
||||
"META-INF/LICENSE.txt",
|
||||
"META-INF/license.txt",
|
||||
"META-INF/NOTICE",
|
||||
"META-INF/NOTICE.txt",
|
||||
"META-INF/notice.txt",
|
||||
"META-INF/*.kotlin_module",
|
||||
"META-INF/versions/**",
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// The version of react-native is set by the React Native Gradle Plugin
|
||||
implementation("com.facebook.react:react-android")
|
||||
|
||||
// Native Apache POI backend for metadata-preserving BOQ editing.
|
||||
implementation("org.apache.poi:poi-ooxml:5.2.5")
|
||||
|
||||
if (hermesEnabled.toBoolean()) {
|
||||
implementation("com.facebook.react:hermes-android")
|
||||
} else {
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
|
||||
@@ -16,7 +15,6 @@
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:allowBackup="false"
|
||||
android:theme="@style/AppTheme"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:supportsRtl="true">
|
||||
<activity
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.procure
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.WindowManager
|
||||
import com.facebook.react.ReactActivity
|
||||
import com.facebook.react.ReactActivityDelegate
|
||||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled
|
||||
@@ -7,13 +9,26 @@ import com.facebook.react.defaults.DefaultReactActivityDelegate
|
||||
|
||||
class MainActivity : ReactActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
// FLAG_SECURE: the app displays sealed-bid amounts, Aadhaar OTPs and other
|
||||
// regulated PII. Block screenshots, screen recording and the app-switcher
|
||||
// thumbnail in release builds; debug builds stay capturable for QA.
|
||||
if (!BuildConfig.DEBUG) {
|
||||
window.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_SECURE,
|
||||
WindowManager.LayoutParams.FLAG_SECURE
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the main component registered from JavaScript.
|
||||
* Returns the name of the main component registered from JavaScript.
|
||||
*/
|
||||
override fun getMainComponentName(): String = "procure"
|
||||
|
||||
/**
|
||||
* Returns the instance of the [ReactActivityDelegate].
|
||||
* Returns the instance of the [ReactActivityDelegate].
|
||||
*/
|
||||
override fun createReactActivityDelegate(): ReactActivityDelegate =
|
||||
DefaultReactActivityDelegate(this, mainComponentName, fabricEnabled)
|
||||
|
||||
@@ -11,6 +11,7 @@ import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
|
||||
import com.facebook.react.defaults.DefaultReactNativeHost
|
||||
import com.facebook.react.soloader.OpenSourceMergedSoMapping
|
||||
import com.facebook.soloader.SoLoader
|
||||
import com.procure.excel.ExcelPackage
|
||||
|
||||
class MainApplication : Application(), ReactApplication {
|
||||
|
||||
@@ -18,8 +19,7 @@ class MainApplication : Application(), ReactApplication {
|
||||
object : DefaultReactNativeHost(this) {
|
||||
override fun getPackages(): List<ReactPackage> =
|
||||
PackageList(this).packages.apply {
|
||||
// Packages that cannot be autolinked yet can be added manually here, for example:
|
||||
// add(MyReactNativePackage())
|
||||
add(ExcelPackage())
|
||||
}
|
||||
|
||||
override fun getJSMainModuleName(): String = "index"
|
||||
|
||||
@@ -0,0 +1,407 @@
|
||||
package com.procure.excel
|
||||
|
||||
import android.util.Base64
|
||||
import com.facebook.react.bridge.Promise
|
||||
import com.facebook.react.bridge.ReactApplicationContext
|
||||
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
||||
import com.facebook.react.bridge.ReactMethod
|
||||
import org.apache.poi.ss.usermodel.BorderStyle
|
||||
import org.apache.poi.ss.usermodel.Cell
|
||||
import org.apache.poi.ss.usermodel.CellType
|
||||
import org.apache.poi.ss.usermodel.DataValidation
|
||||
import org.apache.poi.ss.usermodel.DataValidationConstraint.ValidationType
|
||||
import org.apache.poi.ss.usermodel.DateUtil
|
||||
import org.apache.poi.ss.usermodel.HorizontalAlignment
|
||||
import org.apache.poi.ss.usermodel.Row
|
||||
import org.apache.poi.ss.usermodel.Sheet
|
||||
import org.apache.poi.ss.usermodel.SheetVisibility
|
||||
import org.apache.poi.ss.usermodel.VerticalAlignment
|
||||
import org.apache.poi.ss.usermodel.Workbook
|
||||
import org.apache.poi.ss.util.CellRangeAddress
|
||||
import org.apache.poi.xssf.usermodel.XSSFCellStyle
|
||||
import org.apache.poi.xssf.usermodel.XSSFColor
|
||||
import org.apache.poi.xssf.usermodel.XSSFFont
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
||||
/**
|
||||
* ExcelModule — the native Apache POI bridge.
|
||||
*
|
||||
* Holds one workbook per session. `open` parses xlsx bytes and returns a
|
||||
* WorkbookDTO (values, formulas, styles, merges, hidden lines, protection,
|
||||
* lock flags, validations). `save` applies ONLY edited values back onto the
|
||||
* retained workbook — preserving every other feature — and returns the new
|
||||
* bytes. Formula recalculation is deferred to Excel/HyperFormula.
|
||||
*/
|
||||
class ExcelModule(private val reactContext: ReactApplicationContext) :
|
||||
ReactContextBaseJavaModule(reactContext) {
|
||||
|
||||
private var workbook: Workbook? = null
|
||||
|
||||
override fun getName(): String = "ExcelModule"
|
||||
|
||||
/* ------------------------------- open ------------------------------- */
|
||||
|
||||
@ReactMethod
|
||||
fun open(base64: String, promise: Promise) {
|
||||
try {
|
||||
close()
|
||||
val bytes = Base64.decode(base64, Base64.DEFAULT)
|
||||
val wb = XSSFWorkbook(ByteArrayInputStream(bytes))
|
||||
workbook = wb
|
||||
promise.resolve(serializeWorkbook(wb).toString())
|
||||
} catch (e: Throwable) {
|
||||
promise.reject("E_OPEN", "Failed to open workbook: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun serializeWorkbook(wb: Workbook): JSONObject {
|
||||
val root = JSONObject()
|
||||
root.put("activeSheet", wb.activeSheetIndex.coerceAtLeast(0))
|
||||
val sheets = JSONArray()
|
||||
for (i in 0 until wb.numberOfSheets) {
|
||||
sheets.put(serializeSheet(wb, i))
|
||||
}
|
||||
root.put("sheets", sheets)
|
||||
return root
|
||||
}
|
||||
|
||||
private fun serializeSheet(wb: Workbook, index: Int): JSONObject {
|
||||
val sheet = wb.getSheetAt(index)
|
||||
val out = JSONObject()
|
||||
out.put("name", wb.getSheetName(index))
|
||||
out.put("index", index)
|
||||
out.put(
|
||||
"state",
|
||||
when (wb.getSheetVisibility(index)) {
|
||||
SheetVisibility.HIDDEN -> "hidden"
|
||||
SheetVisibility.VERY_HIDDEN -> "veryHidden"
|
||||
else -> "visible"
|
||||
},
|
||||
)
|
||||
out.put("protected", sheet.protect)
|
||||
|
||||
// Frozen panes.
|
||||
var frozenRows = 0
|
||||
var frozenCols = 0
|
||||
try {
|
||||
sheet.paneInformation?.let { pane ->
|
||||
if (pane.isFreezePane) {
|
||||
frozenRows = pane.horizontalSplitPosition.toInt()
|
||||
frozenCols = pane.verticalSplitPosition.toInt()
|
||||
}
|
||||
}
|
||||
} catch (_: Throwable) { /* ignore pane read errors */ }
|
||||
out.put("frozenRows", frozenRows)
|
||||
out.put("frozenCols", frozenCols)
|
||||
|
||||
val lastRow = sheet.lastRowNum
|
||||
var maxCol = 0
|
||||
|
||||
val hiddenRows = JSONArray()
|
||||
val rowHeights = JSONObject()
|
||||
val cells = JSONArray()
|
||||
|
||||
// Precompute per-cell data validations.
|
||||
val dvMap = buildValidationMap(sheet)
|
||||
|
||||
for (r in 0..lastRow) {
|
||||
val row = sheet.getRow(r) ?: continue
|
||||
if (row.zeroHeight) hiddenRows.put(r)
|
||||
rowHeights.put(r.toString(), pointsToPx(row.heightInPoints))
|
||||
for (cell in row) {
|
||||
val c = cell.columnIndex
|
||||
if (c > maxCol) maxCol = c
|
||||
val dto = serializeCell(cell, dvMap["$r,$c"]) ?: continue
|
||||
cells.put(dto)
|
||||
}
|
||||
}
|
||||
|
||||
val colCount = maxCol + 1
|
||||
out.put("rowCount", lastRow + 1)
|
||||
out.put("colCount", colCount)
|
||||
|
||||
val hiddenCols = JSONArray()
|
||||
val colWidths = JSONObject()
|
||||
for (c in 0 until colCount) {
|
||||
if (sheet.isColumnHidden(c)) hiddenCols.put(c)
|
||||
try {
|
||||
colWidths.put(c.toString(), charUnitsToPx(sheet.getColumnWidth(c)))
|
||||
} catch (_: Throwable) { /* default width */ }
|
||||
}
|
||||
|
||||
out.put("hiddenRows", hiddenRows)
|
||||
out.put("hiddenColumns", hiddenCols)
|
||||
out.put("columnWidths", colWidths)
|
||||
out.put("rowHeights", rowHeights)
|
||||
|
||||
val merges = JSONArray()
|
||||
for (region: CellRangeAddress in sheet.mergedRegions) {
|
||||
merges.put(region.formatAsString())
|
||||
}
|
||||
out.put("merges", merges)
|
||||
out.put("cells", cells)
|
||||
return out
|
||||
}
|
||||
|
||||
private fun serializeCell(cell: Cell, dv: JSONObject?): JSONObject? {
|
||||
val style = cell.cellStyle
|
||||
val locked = style?.locked ?: true
|
||||
val numFmt = style?.dataFormatString
|
||||
val comment = try {
|
||||
cell.cellComment?.string?.string
|
||||
} catch (_: Throwable) {
|
||||
null
|
||||
}
|
||||
val styleJson = serializeStyle(cell)
|
||||
|
||||
val isFormula = cell.cellType == CellType.FORMULA
|
||||
var value: Any? = null
|
||||
var formula: String? = null
|
||||
var type = "text"
|
||||
|
||||
try {
|
||||
when (cell.cellType) {
|
||||
CellType.FORMULA -> {
|
||||
formula = cell.cellFormula
|
||||
when (cell.cachedFormulaResultType) {
|
||||
CellType.NUMERIC -> {
|
||||
value = cell.numericCellValue
|
||||
type = if (DateUtil.isCellDateFormatted(cell)) "date" else typeFromNumFmt(numFmt)
|
||||
}
|
||||
CellType.BOOLEAN -> { value = cell.booleanCellValue; type = "boolean" }
|
||||
CellType.STRING -> { value = cell.stringCellValue; type = "text" }
|
||||
CellType.ERROR -> { value = "#ERROR!"; type = "text" }
|
||||
else -> { value = null }
|
||||
}
|
||||
}
|
||||
CellType.NUMERIC -> {
|
||||
value = cell.numericCellValue
|
||||
type = if (DateUtil.isCellDateFormatted(cell)) "date" else typeFromNumFmt(numFmt)
|
||||
}
|
||||
CellType.STRING -> { value = cell.stringCellValue; type = "text" }
|
||||
CellType.BOOLEAN -> { value = cell.booleanCellValue; type = "boolean" }
|
||||
CellType.ERROR -> { value = "#ERROR!"; type = "text" }
|
||||
else -> { value = null }
|
||||
}
|
||||
} catch (_: Throwable) {
|
||||
value = null
|
||||
}
|
||||
|
||||
// Prefer a validation-derived dropdown type.
|
||||
if (dv != null && dv.optString("kind") == "list") type = "dropdown"
|
||||
|
||||
val hasContent = value != null || formula != null
|
||||
val hasMeta = styleJson != null || comment != null || dv != null || !locked
|
||||
if (!hasContent && !hasMeta) return null
|
||||
|
||||
val obj = JSONObject()
|
||||
obj.put("r", cell.rowIndex)
|
||||
obj.put("c", cell.columnIndex)
|
||||
obj.put("v", value ?: JSONObject.NULL)
|
||||
obj.put("f", formula ?: JSONObject.NULL)
|
||||
obj.put("t", type)
|
||||
if (numFmt != null && numFmt != "General") obj.put("numFmt", numFmt)
|
||||
obj.put("locked", locked)
|
||||
if (comment != null) obj.put("comment", comment)
|
||||
if (styleJson != null) obj.put("style", styleJson)
|
||||
if (dv != null) obj.put("dv", dv)
|
||||
return obj
|
||||
}
|
||||
|
||||
private fun serializeStyle(cell: Cell): JSONObject? {
|
||||
val style = cell.cellStyle ?: return null
|
||||
val json = JSONObject()
|
||||
var any = false
|
||||
|
||||
try {
|
||||
val font = cell.sheet.workbook.getFontAt(style.fontIndexAsInt)
|
||||
if (font.bold) { json.put("bold", true); any = true }
|
||||
if (font.italic) { json.put("italic", true); any = true }
|
||||
if (font.underline.toInt() != 0) { json.put("underline", true); any = true }
|
||||
if (font.strikeout) { json.put("strike", true); any = true }
|
||||
if (font.fontHeightInPoints.toInt() != 11) { json.put("fontSize", font.fontHeightInPoints.toInt()); any = true }
|
||||
if (font is XSSFFont) {
|
||||
font.xssfColor?.let { hexFromColor(it)?.let { hex -> json.put("fontColor", hex); any = true } }
|
||||
}
|
||||
} catch (_: Throwable) { /* font read best-effort */ }
|
||||
|
||||
try {
|
||||
if (style is XSSFCellStyle) {
|
||||
val fill = style.fillForegroundColorColor
|
||||
if (fill is XSSFColor) hexFromColor(fill)?.let { json.put("bgColor", it); any = true }
|
||||
}
|
||||
} catch (_: Throwable) { /* fill read best-effort */ }
|
||||
|
||||
try {
|
||||
when (style.alignment) {
|
||||
HorizontalAlignment.LEFT -> json.put("hAlign", "left")
|
||||
HorizontalAlignment.CENTER -> json.put("hAlign", "center")
|
||||
HorizontalAlignment.RIGHT -> json.put("hAlign", "right")
|
||||
HorizontalAlignment.JUSTIFY -> json.put("hAlign", "justify")
|
||||
else -> {}
|
||||
}
|
||||
when (style.verticalAlignment) {
|
||||
VerticalAlignment.TOP -> json.put("vAlign", "top")
|
||||
VerticalAlignment.CENTER -> json.put("vAlign", "middle")
|
||||
VerticalAlignment.BOTTOM -> json.put("vAlign", "bottom")
|
||||
else -> {}
|
||||
}
|
||||
if (style.wrapText) json.put("wrap", true)
|
||||
if (style.alignment != HorizontalAlignment.GENERAL || style.wrapText) any = true
|
||||
} catch (_: Throwable) { /* alignment best-effort */ }
|
||||
|
||||
try {
|
||||
if (style.borderTop != BorderStyle.NONE) { json.put("borderTop", true); any = true }
|
||||
if (style.borderRight != BorderStyle.NONE) { json.put("borderRight", true); any = true }
|
||||
if (style.borderBottom != BorderStyle.NONE) { json.put("borderBottom", true); any = true }
|
||||
if (style.borderLeft != BorderStyle.NONE) { json.put("borderLeft", true); any = true }
|
||||
} catch (_: Throwable) { /* border best-effort */ }
|
||||
|
||||
return if (any) json else null
|
||||
}
|
||||
|
||||
private fun buildValidationMap(sheet: Sheet): Map<String, JSONObject> {
|
||||
val map = HashMap<String, JSONObject>()
|
||||
try {
|
||||
for (dv: DataValidation in sheet.dataValidations) {
|
||||
val constraint = dv.validationConstraint
|
||||
val json = JSONObject()
|
||||
when (constraint.validationType) {
|
||||
ValidationType.LIST -> {
|
||||
json.put("kind", "list")
|
||||
val values = constraint.explicitListValues
|
||||
if (values != null) {
|
||||
val arr = JSONArray()
|
||||
values.forEach { arr.put(it) }
|
||||
json.put("list", arr)
|
||||
}
|
||||
}
|
||||
ValidationType.INTEGER -> json.put("kind", "whole")
|
||||
ValidationType.DECIMAL -> json.put("kind", "decimal")
|
||||
ValidationType.TEXT_LENGTH -> json.put("kind", "textLength")
|
||||
ValidationType.DATE -> json.put("kind", "date")
|
||||
else -> continue
|
||||
}
|
||||
constraint.formula1?.toDoubleOrNull()?.let { json.put("min", it) }
|
||||
constraint.formula2?.toDoubleOrNull()?.let { json.put("max", it) }
|
||||
dv.errorBoxText?.let { json.put("errorMessage", it) }
|
||||
|
||||
for (region in dv.regions.cellRangeAddresses) {
|
||||
for (r in region.firstRow..region.lastRow) {
|
||||
for (c in region.firstColumn..region.lastColumn) {
|
||||
map["$r,$c"] = json
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (_: Throwable) { /* validations best-effort */ }
|
||||
return map
|
||||
}
|
||||
|
||||
/* ------------------------------- save ------------------------------- */
|
||||
|
||||
@ReactMethod
|
||||
fun save(saveRequestJson: String, promise: Promise) {
|
||||
val wb = workbook
|
||||
if (wb == null) {
|
||||
promise.reject("E_NO_WORKBOOK", "No workbook is open.")
|
||||
return
|
||||
}
|
||||
try {
|
||||
val request = JSONObject(saveRequestJson)
|
||||
val edits = request.optJSONArray("edits") ?: JSONArray()
|
||||
for (i in 0 until edits.length()) {
|
||||
applyEdit(wb, edits.getJSONObject(i))
|
||||
}
|
||||
// Ask consumers (Excel) to recalculate; HyperFormula already did in-app.
|
||||
wb.setForceFormulaRecalculation(true)
|
||||
|
||||
val bos = ByteArrayOutputStream()
|
||||
wb.write(bos)
|
||||
val base64 = Base64.encodeToString(bos.toByteArray(), Base64.NO_WRAP)
|
||||
promise.resolve(base64)
|
||||
} catch (e: Throwable) {
|
||||
promise.reject("E_SAVE", "Failed to save workbook: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyEdit(wb: Workbook, edit: JSONObject) {
|
||||
val sheetIndex = edit.getInt("sheet")
|
||||
if (sheetIndex < 0 || sheetIndex >= wb.numberOfSheets) return
|
||||
val sheet = wb.getSheetAt(sheetIndex)
|
||||
val r = edit.getInt("r")
|
||||
val c = edit.getInt("c")
|
||||
val row: Row = sheet.getRow(r) ?: sheet.createRow(r)
|
||||
val cell: Cell = row.getCell(c) ?: row.createCell(c)
|
||||
|
||||
when {
|
||||
edit.optBoolean("clear", false) -> cell.setBlank()
|
||||
!edit.isNull("f") -> cell.cellFormula = edit.getString("f")
|
||||
else -> {
|
||||
when (val v = edit.opt("v")) {
|
||||
is Boolean -> cell.setCellValue(v)
|
||||
is Int -> cell.setCellValue(v.toDouble())
|
||||
is Long -> cell.setCellValue(v.toDouble())
|
||||
is Double -> cell.setCellValue(v)
|
||||
is String -> cell.setCellValue(v)
|
||||
null, JSONObject.NULL -> cell.setBlank()
|
||||
else -> cell.setCellValue(v.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------ close ------------------------------- */
|
||||
|
||||
@ReactMethod
|
||||
fun close(promise: Promise) {
|
||||
close()
|
||||
promise.resolve(null)
|
||||
}
|
||||
|
||||
private fun close() {
|
||||
try {
|
||||
workbook?.close()
|
||||
} catch (_: Throwable) {
|
||||
} finally {
|
||||
workbook = null
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------ helpers ----------------------------- */
|
||||
|
||||
private fun hexFromColor(color: XSSFColor?): String? {
|
||||
if (color == null) return null
|
||||
return try {
|
||||
val argb = color.argbHex ?: return null // "FFRRGGBB"
|
||||
if (argb.length == 8) "#${argb.substring(2)}" else "#$argb"
|
||||
} catch (_: Throwable) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun typeFromNumFmt(fmt: String?): String {
|
||||
if (fmt != null) {
|
||||
val stripped = fmt.replace(Regex("\\[[^\\]]*\\]"), "").replace(Regex("\"[^\"]*\""), "")
|
||||
if (fmt.contains("%")) return "percentage"
|
||||
if (Regex("[dmy]", RegexOption.IGNORE_CASE).containsMatchIn(stripped) && !Regex("[#0]").containsMatchIn(stripped)) return "date"
|
||||
if (Regex("[$€£₹¥]").containsMatchIn(fmt)) return "currency"
|
||||
}
|
||||
return "number"
|
||||
}
|
||||
|
||||
private fun charUnitsToPx(width256: Int): Int {
|
||||
val chars = width256 / 256.0
|
||||
return (chars * 7.5 + 8).toInt().coerceIn(48, 420)
|
||||
}
|
||||
|
||||
private fun pointsToPx(points: Float): Int {
|
||||
return (points * 1.3333).toInt().coerceIn(28, 240)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.procure.excel
|
||||
|
||||
import com.facebook.react.ReactPackage
|
||||
import com.facebook.react.bridge.NativeModule
|
||||
import com.facebook.react.bridge.ReactApplicationContext
|
||||
import com.facebook.react.uimanager.ViewManager
|
||||
|
||||
/**
|
||||
* Registers [ExcelModule] with React Native. Added manually in
|
||||
* MainApplication.getPackages(). Works under both the old and new architecture
|
||||
* (the JS ExcelBridge resolves it via TurboModuleRegistry first, then the
|
||||
* legacy NativeModules registry).
|
||||
*/
|
||||
class ExcelPackage : ReactPackage {
|
||||
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> =
|
||||
listOf(ExcelModule(reactContext))
|
||||
|
||||
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> =
|
||||
emptyList()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Allow ALL cleartext (http://) traffic for every build, including release.
|
||||
Needed so the app can reach http://eproc-backend.lincpay/ over plain HTTP. -->
|
||||
<!--
|
||||
Cleartext (http://) is DENIED by default. It is permitted only for the
|
||||
specific backend hosts below that are not yet served over HTTPS.
|
||||
TODO(backend): once eproc-backend.lincpay terminates TLS, delete its
|
||||
domain-config so the entire app is HTTPS-only, and add certificate pinning.
|
||||
-->
|
||||
<network-security-config>
|
||||
<base-config cleartextTrafficPermitted="true" />
|
||||
<base-config cleartextTrafficPermitted="false" />
|
||||
|
||||
<!-- Production REST backend — currently HTTP-only. Remove when HTTPS lands. -->
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<domain includeSubdomains="true">eproc-backend.lincpay</domain>
|
||||
</domain-config>
|
||||
|
||||
<!-- Local development hosts (Metro / LAN backends / emulator loopback). -->
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<domain includeSubdomains="false">localhost</domain>
|
||||
<domain includeSubdomains="false">10.0.2.2</domain>
|
||||
<domain includeSubdomains="false">192.168.6.194</domain>
|
||||
<domain includeSubdomains="false">192.168.29.226</domain>
|
||||
</domain-config>
|
||||
</network-security-config>
|
||||
|
||||
Reference in New Issue
Block a user