Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
},
],
'./plugins/withInCallAudioModule.js',
['app-icon-badge', appIconBadgeConfig],
['./plugins/with-app-icon-badge.js', appIconBadgeConfig],
],
extra: {
...ClientEnv,
Expand Down
62 changes: 62 additions & 0 deletions plugins/generate-app-icon-badges.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
const { mkdir, rm, stat } = require('node:fs/promises');
const path = require('node:path');

const Jimp = require('jimp');
const { addBadge } = require('app-icon-badge');

const MAX_IMAGE_READ_ATTEMPTS = 200;
const IMAGE_READ_RETRY_DELAY_MS = 25;

const delay = (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));

const waitForReadableImage = async (destinationPath) => {
let previousSize = -1;

for (let attempt = 0; attempt < MAX_IMAGE_READ_ATTEMPTS; attempt += 1) {
try {
const file = await stat(destinationPath);
if (file.size > 0 && file.size === previousSize) {
const image = await Jimp.read(destinationPath);
if (image.bitmap.width > 0 && image.bitmap.height > 0) {
return;
}
}
previousSize = file.size;
} catch {
previousSize = -1;
}

await delay(IMAGE_READ_RETRY_DELAY_MS);
}

throw new Error(`Generated app icon is not a readable image: ${destinationPath}`);
};

const generateBadge = async ({ badges, destinationPath, isAdaptiveIcon, sourcePath }) => {
await mkdir(path.dirname(destinationPath), { recursive: true });
await rm(destinationPath, { force: true });
await addBadge({
badges,
dstPath: destinationPath,
icon: sourcePath,
isAdaptiveIcon,
});
await waitForReadableImage(destinationPath);
};

const main = async () => {
const payload = JSON.parse(process.argv[2] ?? '{}');
if (!Array.isArray(payload.tasks)) {
throw new Error('Expected an app icon badge task list');
}

for (const task of payload.tasks) {
await generateBadge(task);
}
Comment on lines +53 to +55

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Kody Rules low

Sequential task execution bottleneck occurs because the first await loop failure aborts all remaining independent badge tasks. Use await Promise.allSettled(payload.tasks.map(generateBadge)) to attempt all tasks, collect per-task errors, and report them together.

Kody rule violation: Use Promise.allSettled for batch operations with partial failures

Prompt for LLM

File plugins/generate-app-icon-badges.js:

Line 53 to 55:

Sequential task execution bottleneck occurs because the first `await` loop failure aborts all remaining independent badge tasks. Use `await Promise.allSettled(payload.tasks.map(generateBadge))` to attempt all tasks, collect per-task errors, and report them together.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

};

main().catch((error) => {
const message = error instanceof Error ? (error.stack ?? error.message) : String(error);
process.stderr.write(`${path.basename(__filename)}: ${message}\n`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Kody Rules high

Unstructured error logging occurs because process.stderr.write emits a plain text string without debugging identifiers during multi-task failures. Emit structured JSON output containing fields like { op, error, destinationPath, sourcePath } instead of a flat string.

Kody rule violation: Include error context in structured logs

Prompt for LLM

File plugins/generate-app-icon-badges.js:

Line 60:

Unstructured error logging occurs because `process.stderr.write` emits a plain text string without debugging identifiers during multi-task failures. Emit structured JSON output containing fields like `{ op, error, destinationPath, sourcePath }` instead of a flat string.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

process.exitCode = 1;
});
64 changes: 64 additions & 0 deletions plugins/with-app-icon-badge.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
const { execFileSync } = require('node:child_process');
const path = require('node:path');

const OUTPUT_DIRECTORY = '.expo/app-icon-badge';
const GENERATOR_SCRIPT = path.join(__dirname, 'generate-app-icon-badges.js');

const resolveProjectPath = (projectRoot, filePath) => path.resolve(projectRoot, filePath);

const createBadgeTask = ({ projectRoot, sourcePath, destinationPath, badges, isAdaptiveIcon = false }) => ({
badges,
destinationPath: resolveProjectPath(projectRoot, destinationPath),
isAdaptiveIcon,
sourcePath: resolveProjectPath(projectRoot, sourcePath),
});

const generateBadgedIcons = (projectRoot, tasks) => {
// The upstream config plugin starts image writes without awaiting them. A child
// process lets Expo block until every generated icon has been fully validated.
execFileSync(process.execPath, [GENERATOR_SCRIPT, JSON.stringify({ tasks })], {
cwd: projectRoot,
stdio: 'inherit',
});
Comment on lines +19 to +22

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Kody Rules high

Opaque build failures occur because the execFileSync call spawns an external child process without a try/catch, violating rule [28] for mapped application-level errors. Wrap the call in try/catch, log the projectRoot and task count, and rethrow an actionable error.

Also found in:

  • plugins/generate-app-icon-badges.js:36-36
  • plugins/generate-app-icon-badges.js:38-43
  • plugins/generate-app-icon-badges.js:37-37

Kody rule violation: Add try-catch blocks for external calls

Prompt for LLM

File plugins/with-app-icon-badge.js:

Line 19 to 22:

Opaque build failures occur because the `execFileSync` call spawns an external child process without a try/catch, violating rule [28] for mapped application-level errors. Wrap the call in try/catch, log the `projectRoot` and task count, and rethrow an actionable error.

**Also found in:**
- `plugins/generate-app-icon-badges.js:36-36`
- `plugins/generate-app-icon-badges.js:38-43`
- `plugins/generate-app-icon-badges.js:37-37`

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

};

const withAppIconBadge = (config, options = {}) => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kody code-review Kody Rules high

Render performance degradation occurs because .bind() or inline arrow functions in JSX props create new function instances on every render. Move function definitions outside the render method to resolve the performance impact.

Kody rule violation: Avoid using .bind() or arrow functions in JSX props

Prompt for LLM

File plugins/with-app-icon-badge.js:

Line 25:

Render performance degradation occurs because `.bind()` or inline arrow functions in JSX props create new function instances on every render. Move function definitions outside the render method to resolve the performance impact.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

const { badges = [], enabled = true } = options;
if (!enabled) {
return config;
}

const projectRoot = config._internal?.projectRoot ?? process.cwd();
const tasks = [];
const iconSource = config.icon;
const iosIconSource = config.ios?.icon;
const adaptiveIconSource = config.android?.adaptiveIcon?.foregroundImage;

if (typeof iconSource === 'string') {
const destinationPath = `${OUTPUT_DIRECTORY}/icon.png`;
tasks.push(createBadgeTask({ projectRoot, sourcePath: iconSource, destinationPath, badges }));
config.icon = destinationPath;
}

if (typeof iosIconSource === 'string') {
const destinationPath = iosIconSource === iconSource ? `${OUTPUT_DIRECTORY}/icon.png` : `${OUTPUT_DIRECTORY}/ios-icon.png`;
if (iosIconSource !== iconSource) {
tasks.push(createBadgeTask({ projectRoot, sourcePath: iosIconSource, destinationPath, badges }));
}
config.ios.icon = destinationPath;
}

if (typeof adaptiveIconSource === 'string') {
const destinationPath = `${OUTPUT_DIRECTORY}/foreground-image.png`;
tasks.push(createBadgeTask({ projectRoot, sourcePath: adaptiveIconSource, destinationPath, badges, isAdaptiveIcon: true }));
config.android.adaptiveIcon.foregroundImage = destinationPath;
}

if (tasks.length > 0) {
generateBadgedIcons(projectRoot, tasks);
}

return config;
};

module.exports = withAppIconBadge;
18 changes: 9 additions & 9 deletions src/lib/check-in-timer-utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import { CHECK_IN_TARGET_TYPE, type CheckInEligibilityContext, isCheckInTargetEligible } from '@/lib/check-in-eligibility';

export type CheckInTimerStatus = 'critical' | 'ok' | 'overdue' | 'unknown' | 'warning';
const STATUS_COLORS = {
critical: '#EF4444',
ok: '#22C55E',
overdue: '#F59E0B',
unknown: '#808080',
warning: '#F59E0B',
} as const;

export type CheckInTimerStatus = keyof typeof STATUS_COLORS;
export type CheckInTimerBadgeVariant = 'critical' | 'warning';

interface CheckInTimerTargetStatus {
Expand All @@ -15,14 +23,6 @@ export interface CheckInTimerBadge {
variant: CheckInTimerBadgeVariant;
}

const STATUS_COLORS: Record<CheckInTimerStatus, string> = {
critical: '#EF4444',
ok: '#22C55E',
overdue: '#F59E0B',
unknown: '#808080',
warning: '#F59E0B',
};

const STATUS_SEVERITY: Record<CheckInTimerStatus, number> = {
critical: 0,
overdue: 1,
Expand Down
Loading