chore(release): bump to v0.2.1 / versionCode 9 — theme-crash, double-splash, dm-reopen fixes

- Android Theme parent → Theme.MaterialComponents.DayNight.NoActionBar.Bridge
  (fix BadgeDrawable crash in react-native-bottom-tabs after AccessibilityService toggle)
- Plugin with-material-theme-android keeps theme idempotent across prebuilds
- Plugin with-release-signing-android wires release signingConfig from key.properties
- Splash: align native splash image with JS BrandSplash (icon.png) to eliminate
  double-splash flicker on app start
- DM: reset partner/messages/replyTo state on userId change, disable cache for
  history query, switch spinner condition to isLoading||isFetching so reopens
  always load fresh and never show empty-state with stale partner

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
chahinebrini 2026-05-16 09:32:28 +02:00
parent 6ac6a26b9c
commit 4124463097
5 changed files with 149 additions and 7 deletions

View File

@ -10,6 +10,14 @@ Versioning: `version` follows SemVer, `versionCode` is monotonically increasing.
---
## [0.2.1] — versionCode 9 — 2026-05-16
### Fixed
- **Android-Crash nach AccessibilityService-Aktivierung**: BadgeDrawable in der Tab-Bar craste mit `IllegalArgumentException`, weil das Theme `AppCompat` statt `MaterialComponents` nutzte. Theme-Parent auf `Theme.MaterialComponents.DayNight.NoActionBar.Bridge` geändert via Config-Plugin — idempotent nach jedem prebuild.
- **Doppel-Splash auf Android**: Native Android-Splash und JS-Splash sahen unterschiedlich aus (zwei aufeinanderfolgende Splash-Screens). Splash-Image auf `icon.png` umgestellt, matched jetzt exakt den JS-Splash und Landing-Screen.
---
## [0.2.0] — versionCode 8 — 2026-05-16
### Added

View File

@ -4,7 +4,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
...config,
name: "ReBreak",
slug: "rebreak",
version: "0.2.0",
version: "0.2.1",
orientation: "portrait",
icon: "./assets/icon.png",
scheme: "rebreak",
@ -12,7 +12,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
newArchEnabled: true,
splash: {
image: "./assets/splash.png",
image: "./assets/icon.png",
resizeMode: "contain",
backgroundColor: "#0f172a",
},
@ -20,7 +20,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
ios: {
supportsTablet: true,
bundleIdentifier: "org.rebreak.app",
buildNumber: "8",
buildNumber: "9",
config: {
usesNonExemptEncryption: false,
},
@ -39,7 +39,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
android: {
package: "org.rebreak.app",
versionCode: 8,
versionCode: 9,
adaptiveIcon: {
// Foreground muss in der ~66%-Safe-Zone bleiben (Launcher-Mask clippt den
// Außenring) → adaptive-foreground.png ist das Logo auf transparentem
@ -88,6 +88,10 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
"./plugins/with-rebreak-protection-android",
// Rive-Asset (lyra-avatar.riv) als Android raw-resource bundlen
"./plugins/with-rive-asset-android",
// MaterialComponents-Theme-Fix für BadgeDrawable in react-native-bottom-tabs
"./plugins/with-material-theme-android",
// Release-Signing-Block in build.gradle (liest android/key.properties — nicht committen)
"./plugins/with-release-signing-android",
],
experiments: {

View File

@ -74,6 +74,14 @@ export default function DmScreen() {
);
const [sending, setSending] = useState(false);
// Reset aller conversation-spezifischen States wenn userId wechselt (Stack-Reuse)
useEffect(() => {
setMessages([]);
setPartner(null);
partnerRef.current = null;
setReplyTo(null);
}, [userId]);
// Lade meine User-ID
useEffect(() => {
supabase.auth.getSession().then(({ data }) => {
@ -81,8 +89,8 @@ export default function DmScreen() {
});
}, []);
// Lade DM-History
const { isLoading } = useQuery({
// Lade DM-History — staleTime:0 erzwingt immer frischen Fetch (kein Cache-Hit-Bug)
const { isLoading, isFetching } = useQuery({
queryKey: ['dm-history', userId],
queryFn: async () => {
console.log('[dm] fetching history for partner', userId, 'me', myUserId);
@ -124,6 +132,8 @@ export default function DmScreen() {
}
},
enabled: !!userId && !!myUserId,
staleTime: 0,
gcTime: 0,
});
// Realtime: neue DMs vom Partner
@ -270,7 +280,7 @@ export default function DmScreen() {
>
<View style={{ flex: 1, backgroundColor: chatBg }}>
<DmChatBackground />
{isLoading && messages.length === 0 ? (
{(isLoading || isFetching) && messages.length === 0 ? (
<View style={styles.loadingBox}>
<ActivityIndicator color={colors.brandOrange} />
</View>

View File

@ -0,0 +1,38 @@
/* eslint-disable @typescript-eslint/no-var-requires */
/**
* Expo Config-Plugin setzt den AppTheme-Parent auf
* Theme.MaterialComponents.DayNight.NoActionBar.Bridge damit react-native-bottom-tabs'
* BadgeDrawable nicht mit IllegalArgumentException crasht.
*
* Warum Bridge-Variante: bridged MaterialComponents ist ein Superset von AppCompat
* alle AppCompat-Abhängigkeiten im RN-Stack bleiben kompatibel, aber Material-APIs
* (BadgeDrawable, NavigationBarMenuView etc.) funktionieren.
*
* Warum Plugin statt Hand-Edit: `prebuild --clean` regeneriert styles.xml aus dem
* Expo-AppCompat-Default. Dieses Plugin patcht den Parent-Wert nach jedem prebuild
* idempotent per withAndroidStyles (offizielles Config-Plugins-API).
*/
const { withAndroidStyles } = require('@expo/config-plugins');
const MATERIAL_THEME = 'Theme.MaterialComponents.DayNight.NoActionBar.Bridge';
function withMaterialThemeAndroid(config) {
return withAndroidStyles(config, (cfg) => {
const styles = cfg.modResults.resources.style;
if (!Array.isArray(styles)) return cfg;
const appTheme = styles.find(
(s) => s.$ && s.$['name'] === 'AppTheme',
);
if (appTheme) {
appTheme.$['parent'] = MATERIAL_THEME;
}
return cfg;
});
}
module.exports = withMaterialThemeAndroid;

View File

@ -0,0 +1,82 @@
/* eslint-disable @typescript-eslint/no-var-requires */
/**
* Expo Config-Plugin fügt einen Release-Signing-Block in build.gradle ein,
* der key.properties aus android/key.properties liest.
*
* Warum Plugin statt Hand-Edit: `prebuild --clean` regeneriert build.gradle und
* überschreibt manuelle Änderungen. Dieses Plugin patcht den Signing-Block nach
* jedem prebuild idempotent via withAppBuildGradle.
*
* key.properties muss NACH prebuild in android/ abgelegt werden (nicht committen).
* Keystore muss in android/app/rebreak-release.keystore liegen (nicht committen).
*
* Format key.properties (in android/ wird von rootProject.file() gefunden):
* storePassword=<password>
* keyPassword=<password>
* keyAlias=rebreak
* storeFile=rebreak-release.keystore
*
* Hinweis: storeFile ist relativ zu android/app/ (Gradle-Modul-Root), also
* Keystore in android/app/rebreak-release.keystore ablegen.
*/
const { withAppBuildGradle } = require('@expo/config-plugins');
function withReleaseSigningAndroid(config) {
return withAppBuildGradle(config, (cfg) => {
let gradle = cfg.modResults.contents;
// Idempotenz: Plugin bereits angewandt
if (gradle.includes('REBREAK_RELEASE_SIGNING')) {
return cfg;
}
// 1. Properties-Loader vor android { Block einfügen
const propertiesLoader = `
// REBREAK_RELEASE_SIGNING — injected by with-release-signing-android plugin
def keystorePropertiesFile = rootProject.file("key.properties")
def keystoreProperties = new Properties()
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}
`;
gradle = gradle.replace(/\nandroid \{/, `${propertiesLoader}\nandroid {`);
// 2. Release-signingConfig in signingConfigs-Block einfügen (nach debug-Block)
// Expo generiert: signingConfigs { debug { ... } }
// Wir fügen release { ... } danach ein, direkt vor der schließenden }
const releaseSigningConfig = `
release {
if (keystorePropertiesFile.exists()) {
keyAlias keystoreProperties['keyAlias']
keyPassword keystoreProperties['keyPassword']
storeFile file(keystoreProperties['storeFile'])
storePassword keystoreProperties['storePassword']
}
}`;
// Findet den signingConfigs-Block und fügt release vor der letzten } ein
gradle = gradle.replace(
/(signingConfigs \{[\s\S]*?)( \}\n)( buildTypes)/,
`$1${releaseSigningConfig}\n }\n $3`
);
// 3. Im release buildType: signingConfig.debug → signingConfig.release
// Der release-Block liegt nach dem debug-Block. Wir ersetzen die zweite
// Occurrence von signingConfig signingConfigs.debug (im release-Block).
const parts = gradle.split('signingConfig signingConfigs.debug');
if (parts.length >= 3) {
// parts[0] = vor debug-Block, parts[1] = zwischen debug und release, parts[2] = nach release
gradle = parts[0]
+ 'signingConfig signingConfigs.debug'
+ parts[1]
+ 'signingConfig signingConfigs.release'
+ parts.slice(2).join('signingConfig signingConfigs.debug');
}
cfg.modResults.contents = gradle;
return cfg;
});
}
module.exports = withReleaseSigningAndroid;