Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-02 11:51:03 +03:00
parent 8a83664e9a
commit 728187e890
11 changed files with 231 additions and 139 deletions

36
.gitattributes vendored Normal file
View file

@ -0,0 +1,36 @@
#
# https://help.github.com/articles/dealing-with-line-endings/
#
# Normalize all text files to LF in the repository and the working tree on
# every OS, regardless of the contributor's core.autocrlf setting.
* text=auto eol=lf
# Windows script files must stay CRLF.
*.bat text eol=crlf
*.cmd text eol=crlf
# Unix scripts and the Gradle wrapper must stay LF.
*.sh text eol=lf
gradlew text eol=lf
# Binary files must be left untouched (never EOL-normalized or diffed as text).
# Images
*.png binary
*.webp binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
# Fonts
*.ttf binary
*.otf binary
# JVM / Android artifacts
*.jar binary
*.aar binary
*.so binary
# Signing keys & keystores
*.jks binary
*.keystore binary
# Archives & databases
*.zip binary
*.db binary

1
.gitignore vendored
View file

@ -48,4 +48,5 @@ find-latest-release-branch.output
# Claude # Claude
/.claude/worktrees/ /.claude/worktrees/
/.claude/settings.local.json
CLAUDE.local.md CLAUDE.local.md

5
.worktreeinclude Normal file
View file

@ -0,0 +1,5 @@
# Gitignored files copied into new Claude Code worktrees.
# Syntax: .gitignore patterns. Only gitignored matches are copied.
.claude/settings.local.json
app/google-services.json
local.properties

View file

@ -1,5 +1,5 @@
<resources> <resources>
<bool name="largeHeap">true</bool> <bool name="largeHeap">true</bool>
</resources> </resources>

View file

@ -1,12 +1,12 @@
<resources> <resources>
<!-- Declare custom theme attributes that allow changing which styles are <!-- Declare custom theme attributes that allow changing which styles are
used for button bars depending on the API level. used for button bars depending on the API level.
?android:attr/buttonBarStyle is new as of API 11 so this is ?android:attr/buttonBarStyle is new as of API 11 so this is
necessary to support previous API levels. --> necessary to support previous API levels. -->
<declare-styleable name="ButtonBarContainerTheme"> <declare-styleable name="ButtonBarContainerTheme">
<attr name="metaButtonBarStyle" format="reference" /> <attr name="metaButtonBarStyle" format="reference" />
<attr name="metaButtonBarButtonStyle" format="reference" /> <attr name="metaButtonBarButtonStyle" format="reference" />
</declare-styleable> </declare-styleable>
</resources> </resources>

View file

@ -1,5 +1,5 @@
<resources> <resources>
<bool name="largeHeap">true</bool> <bool name="largeHeap">true</bool>
</resources> </resources>

View file

@ -1,3 +1,4 @@
import java.io.ByteArrayOutputStream
import java.security.MessageDigest import java.security.MessageDigest
plugins { plugins {
@ -61,15 +62,15 @@ abstract class VerifyDesignTokensTask : DefaultTask() {
require(actual == expected) { require(actual == expected) {
"Design tokens are out of date!\n" + "Design tokens are out of date!\n" +
" ds-tokens hash: $actual\n" + " computed from ds-tokens: $actual\n" +
" generated hash: $expected\n" + " committed .tokens-hash: $expected\n" +
"Run the token generator: cd core/ui/token-gen && npm run build" "Run the token generator: cd core/ui/token-gen && npm run build"
} }
stampFile.get().asFile.writeText(actual) stampFile.get().asFile.writeText(actual)
} }
private fun hashTreeHex(root: java.io.File, extension: String): String { private fun hashTreeHex(root: File, extension: String): String {
val digest = MessageDigest.getInstance("SHA-256") val digest = MessageDigest.getInstance("SHA-256")
val files = root.walkTopDown() val files = root.walkTopDown()
.filter { it.isFile && it.extension == extension } .filter { it.isFile && it.extension == extension }
@ -79,12 +80,27 @@ abstract class VerifyDesignTokensTask : DefaultTask() {
for (file in files) { for (file in files) {
digest.update(file.relativeTo(root).invariantSeparatorsPath.toByteArray()) digest.update(file.relativeTo(root).invariantSeparatorsPath.toByteArray())
digest.update(nul) digest.update(nul)
digest.update(file.readBytes()) digest.update(file.readBytes().stripCr())
digest.update(nul) digest.update(nul)
} }
return digest.digest() return digest.digest()
.joinToString("") { b: Byte -> b.toInt().and(0xFF).toString(16).padStart(2, '0') } .joinToString("") { b: Byte -> b.toInt().and(0xFF).toString(16).padStart(2, '0') }
} }
/**
* Strips CR (0x0D) bytes so the token hash ignores CRLF vs LF line endings. The ds-tokens
* submodule is not covered by this repo's .gitattributes, so its .json/.svg sources may be
* checked out with CRLF on some platforms; without this the verification is non-deterministic.
* Removes lone CRs too — safe for UTF-8 sources, where 0x0D never appears inside a
* multi-byte sequence. Must stay byte-for-byte identical to stripCr() in token-gen/hash-util.mjs.
*/
private fun ByteArray.stripCr(): ByteArray {
val cr: Byte = 0x0D
if (none { it == cr }) return this
val out = ByteArrayOutputStream(size)
for (b in this) if (b != cr) out.write(b.toInt())
return out.toByteArray()
}
} }
android { android {
namespace = "com.tangem.core.ui" namespace = "com.tangem.core.ui"

View file

@ -2,6 +2,7 @@ import crypto from 'crypto';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { stripCr, compareCodeUnits } from './hash-util.mjs';
// ── Paths ────────────────────────────────────────────────────────────────────── // ── Paths ──────────────────────────────────────────────────────────────────────
const __dirname = path.dirname(fileURLToPath(import.meta.url)); const __dirname = path.dirname(fileURLToPath(import.meta.url));
@ -252,16 +253,17 @@ object Icons
function computeIconsHash() { function computeIconsHash() {
const files = [...walkSvgs(iconsDir)]; const files = [...walkSvgs(iconsDir)];
files.sort((a, b) => { // Code-unit order (not localeCompare) to match the Kotlin verifier's invariantSeparatorsPath
const ra = path.relative(iconsDir, a).split(path.sep).join('/'); // sort deterministically across locales/ICU versions — see hash-util.mjs.
const rb = path.relative(iconsDir, b).split(path.sep).join('/'); files.sort((a, b) => compareCodeUnits(
return ra.localeCompare(rb); path.relative(iconsDir, a).split(path.sep).join('/'),
}); path.relative(iconsDir, b).split(path.sep).join('/'),
));
const hash = crypto.createHash('sha256'); const hash = crypto.createHash('sha256');
for (const file of files) { for (const file of files) {
hash.update(path.relative(iconsDir, file).split(path.sep).join('/')); hash.update(path.relative(iconsDir, file).split(path.sep).join('/'));
hash.update('\0'); hash.update('\0');
hash.update(fs.readFileSync(file)); hash.update(stripCr(fs.readFileSync(file)));
hash.update('\0'); hash.update('\0');
} }
return hash.digest('hex'); return hash.digest('hex');

View file

@ -5,6 +5,7 @@ import fs from 'fs';
import path from 'path'; import path from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { buildIcons } from './build-icons.mjs'; import { buildIcons } from './build-icons.mjs';
import { stripCr, compareCodeUnits } from './hash-util.mjs';
// ── Paths ────────────────────────────────────────────────────────────────────── // ── Paths ──────────────────────────────────────────────────────────────────────
const __dirname = path.dirname(fileURLToPath(import.meta.url)); const __dirname = path.dirname(fileURLToPath(import.meta.url));
@ -792,18 +793,18 @@ function computeTokensHash() {
} }
} }
walk(tokensDir); walk(tokensDir);
// Sort by relative path with forward slashes to match Gradle's invariantSeparatorsPath sorting // Sort by forward-slash relative path in UTF-16 code-unit order to match the Kotlin verifier's
files.sort((a, b) => { // sortedBy { …invariantSeparatorsPath } (String.compareTo) deterministically — see hash-util.mjs.
const ra = path.relative(tokensDir, a).split(path.sep).join('/'); files.sort((a, b) => compareCodeUnits(
const rb = path.relative(tokensDir, b).split(path.sep).join('/'); path.relative(tokensDir, a).split(path.sep).join('/'),
return ra.localeCompare(rb); path.relative(tokensDir, b).split(path.sep).join('/'),
}); ));
const hash = crypto.createHash('sha256'); const hash = crypto.createHash('sha256');
for (const file of files) { for (const file of files) {
hash.update(path.relative(tokensDir, file).split(path.sep).join('/')); hash.update(path.relative(tokensDir, file).split(path.sep).join('/'));
hash.update('\0'); hash.update('\0');
hash.update(fs.readFileSync(file)); hash.update(stripCr(fs.readFileSync(file)));
hash.update('\0'); hash.update('\0');
} }
return hash.digest('hex'); return hash.digest('hex');

View file

@ -0,0 +1,31 @@
// Shared hashing helpers for the design-token generators (build-tokens.mjs / build-icons.mjs).
// Kept in one place because the exact byte stream AND file ordering produced here must stay
// identical to the Kotlin verifier in core/ui/build.gradle.kts (VerifyDesignTokensTask):
// any drift silently breaks the token hash gate.
/**
* Strip every CR (0x0D) byte so the hash ignores CRLF vs LF line endings. The ds-tokens submodule
* is not covered by the parent repo's .gitattributes, so its .json/.svg sources may be checked out
* with CRLF on some platforms. Lone CRs are dropped too safe here because these sources are UTF-8
* (0x0D never appears inside a multi-byte sequence) and carry no bare CR.
* Must stay byte-for-byte identical to ByteArray.stripCr() in core/ui/build.gradle.kts.
*/
export function stripCr(buf) {
const out = Buffer.allocUnsafe(buf.length);
let n = 0;
for (let i = 0; i < buf.length; i++) {
if (buf[i] !== 0x0d) out[n++] = buf[i];
}
return out.subarray(0, n);
}
/**
* Compare two forward-slash relative paths by UTF-16 code unit identical to Kotlin's
* String.compareTo used by `sortedBy { …invariantSeparatorsPath }` in the verifier.
* Do NOT use String.prototype.localeCompare for file ordering: it is locale/ICU-version dependent
* (differs across machines) and case-insensitive at the primary level (disagrees with the Kotlin
* code-unit sort) either would reorder files and change the hash.
*/
export function compareCodeUnits(a, b) {
return a < b ? -1 : a > b ? 1 : 0;
}

200
gradlew.bat vendored
View file

@ -1,100 +1,100 @@
@rem @rem
@rem Copyright 2015 the original author or authors. @rem Copyright 2015 the original author or authors.
@rem @rem
@rem Licensed under the Apache License, Version 2.0 (the "License"); @rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License. @rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at @rem You may obtain a copy of the License at
@rem @rem
@rem https://www.apache.org/licenses/LICENSE-2.0 @rem https://www.apache.org/licenses/LICENSE-2.0
@rem @rem
@rem Unless required by applicable law or agreed to in writing, software @rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS, @rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and @rem See the License for the specific language governing permissions and
@rem limitations under the License. @rem limitations under the License.
@rem @rem
@if "%DEBUG%" == "" @echo off @if "%DEBUG%" == "" @echo off
@rem ########################################################################## @rem ##########################################################################
@rem @rem
@rem Gradle startup script for Windows @rem Gradle startup script for Windows
@rem @rem
@rem ########################################################################## @rem ##########################################################################
@rem Set local scope for the variables with windows NT shell @rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0 set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=. if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0 set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME% set APP_HOME=%DIRNAME%
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe @rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto init if "%ERRORLEVEL%" == "0" goto init
echo. echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo. echo.
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation. echo location of your Java installation.
goto fail goto fail
:findJavaFromJavaHome :findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=% set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto init if exist "%JAVA_EXE%" goto init
echo. echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo. echo.
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation. echo location of your Java installation.
goto fail goto fail
:init :init
@rem Get command-line arguments, handling Windows variants @rem Get command-line arguments, handling Windows variants
if not "%OS%" == "Windows_NT" goto win9xME_args if not "%OS%" == "Windows_NT" goto win9xME_args
:win9xME_args :win9xME_args
@rem Slurp the command line arguments. @rem Slurp the command line arguments.
set CMD_LINE_ARGS= set CMD_LINE_ARGS=
set _SKIP=2 set _SKIP=2
:win9xME_args_slurp :win9xME_args_slurp
if "x%~1" == "x" goto execute if "x%~1" == "x" goto execute
set CMD_LINE_ARGS=%* set CMD_LINE_ARGS=%*
:execute :execute
@rem Setup the command line @rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle @rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
:end :end
@rem End local scope for the variables with windows NT shell @rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd if "%ERRORLEVEL%"=="0" goto mainEnd
:fail :fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code! rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1 exit /b 1
:mainEnd :mainEnd
if "%OS%"=="Windows_NT" endlocal if "%OS%"=="Windows_NT" endlocal
:omega :omega