feat: Rename to Toju and add translation
Some checks failed
Deploy Web Apps / deploy (push) Successful in 5m52s
Build Android APK / build-android-apk (push) Failing after 23m15s
Queue Release Build / prepare (push) Successful in 1m42s
Queue Release Build / build-linux (push) Failing after 9m33s
Queue Release Build / build-windows (push) Successful in 26m5s
Queue Release Build / finalize (push) Has been skipped
Some checks failed
Deploy Web Apps / deploy (push) Successful in 5m52s
Build Android APK / build-android-apk (push) Failing after 23m15s
Queue Release Build / prepare (push) Successful in 1m42s
Queue Release Build / build-linux (push) Failing after 9m33s
Queue Release Build / build-windows (push) Successful in 26m5s
Queue Release Build / finalize (push) Has been skipped
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import translationsEn from '../../../../../../public/i18n/en.json';
|
||||
import type { CallNotificationActionIntent, CallNotificationPayload } from '../../logic/call-notification.rules';
|
||||
import { resolveCallNotificationAction } from '../../logic/call-notification.rules';
|
||||
import type { MobileNotificationAdapter } from '../../contracts/mobile.contracts';
|
||||
@@ -6,6 +7,18 @@ import { loadCapacitorLocalNotificationsPlugin, loadCapacitorPushNotificationsPl
|
||||
const INCOMING_CALL_CHANNEL_ID = 'toju-incoming-call';
|
||||
const ACTIVE_CALL_CHANNEL_ID = 'toju-active-call';
|
||||
|
||||
function mobileLabel(key: string): string {
|
||||
const value = key.split('.').reduce<unknown>((current, part) => {
|
||||
if (current && typeof current === 'object') {
|
||||
return (current as Record<string, unknown>)[part];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}, translationsEn);
|
||||
|
||||
return typeof value === 'string' ? value : key;
|
||||
}
|
||||
|
||||
/** Capacitor local + push notification bridge with action buttons for in-call controls. */
|
||||
export class CapacitorMobileNotificationsAdapter implements MobileNotificationAdapter {
|
||||
private actionHandler: ((input: { callId: string; intent: CallNotificationActionIntent }) => void) | null = null;
|
||||
@@ -21,7 +34,7 @@ export class CapacitorMobileNotificationsAdapter implements MobileNotificationAd
|
||||
|
||||
await LocalNotifications.createChannel({
|
||||
id: INCOMING_CALL_CHANNEL_ID,
|
||||
name: 'Incoming calls',
|
||||
name: mobileLabel('mobile.notifications.incomingCallsChannel'),
|
||||
importance: 5,
|
||||
visibility: 1,
|
||||
sound: 'call.wav'
|
||||
@@ -29,7 +42,7 @@ export class CapacitorMobileNotificationsAdapter implements MobileNotificationAd
|
||||
|
||||
await LocalNotifications.createChannel({
|
||||
id: ACTIVE_CALL_CHANNEL_ID,
|
||||
name: 'Active calls',
|
||||
name: mobileLabel('mobile.notifications.activeCallsChannel'),
|
||||
importance: 4,
|
||||
visibility: 1
|
||||
});
|
||||
@@ -38,11 +51,17 @@ export class CapacitorMobileNotificationsAdapter implements MobileNotificationAd
|
||||
types: [
|
||||
{
|
||||
id: 'INCOMING_CALL_ACTIONS',
|
||||
actions: [{ id: 'answer', title: 'Answer' }, { id: 'hangup', title: 'Decline' }]
|
||||
actions: [
|
||||
{ id: 'answer', title: mobileLabel('mobile.notifications.answer') },
|
||||
{ id: 'hangup', title: mobileLabel('mobile.notifications.decline') }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'ACTIVE_CALL_ACTIONS',
|
||||
actions: [{ id: 'mute', title: 'Mute' }, { id: 'hangup', title: 'Hang up' }]
|
||||
actions: [
|
||||
{ id: 'mute', title: mobileLabel('mobile.notifications.mute') },
|
||||
{ id: 'hangup', title: mobileLabel('mobile.notifications.hangUp') }
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
@@ -148,4 +167,4 @@ export class CapacitorMobileNotificationsAdapter implements MobileNotificationAd
|
||||
onActionSelected(handler: (input: { callId: string; intent: CallNotificationActionIntent }) => void): void {
|
||||
this.actionHandler = handler;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
it
|
||||
} from 'vitest';
|
||||
|
||||
import translationsEn from '../../../../../public/i18n/en.json';
|
||||
import {
|
||||
buildMobileUpdateState,
|
||||
createInitialMobileUpdateState,
|
||||
@@ -13,9 +14,28 @@ import {
|
||||
} from './mobile-app-update.rules';
|
||||
import type { MobileAppUpdateInfoSnapshot } from './mobile-app-update.rules';
|
||||
|
||||
function instant(key: string, params?: Record<string, string | number>): string {
|
||||
const value = key.split('.').reduce<unknown>((current, part) => {
|
||||
if (current && typeof current === 'object') {
|
||||
return (current as Record<string, unknown>)[part];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}, translationsEn);
|
||||
|
||||
if (typeof value !== 'string') {
|
||||
return key;
|
||||
}
|
||||
|
||||
return Object.entries(params ?? {}).reduce(
|
||||
(message, [paramKey, paramValue]) => message.replace(`{{${paramKey}}}`, String(paramValue)),
|
||||
value
|
||||
);
|
||||
}
|
||||
|
||||
describe('mobile-app-update.rules', () => {
|
||||
it('creates an idle unsupported state for non-native shells', () => {
|
||||
const state = createInitialMobileUpdateState({ isSupported: false });
|
||||
const state = createInitialMobileUpdateState({ isSupported: false, translate: instant });
|
||||
|
||||
expect(state.status).toBe('unsupported');
|
||||
expect(state.isSupported).toBe(false);
|
||||
@@ -40,7 +60,8 @@ describe('mobile-app-update.rules', () => {
|
||||
};
|
||||
const state = buildMobileUpdateState(snapshot, {
|
||||
isSupported: true,
|
||||
lastCheckedAt: 1_700_000_000_000
|
||||
lastCheckedAt: 1_700_000_000_000,
|
||||
translate: instant
|
||||
});
|
||||
|
||||
expect(state.status).toBe('update-available');
|
||||
@@ -61,16 +82,16 @@ describe('mobile-app-update.rules', () => {
|
||||
platform: 'ios',
|
||||
updateAvailability: 'update-not-available'
|
||||
};
|
||||
const state = buildMobileUpdateState(snapshot, { isSupported: true });
|
||||
const state = buildMobileUpdateState(snapshot, { isSupported: true, translate: instant });
|
||||
|
||||
expect(state.status).toBe('up-to-date');
|
||||
expect(state.statusMessage).toContain('up to date');
|
||||
});
|
||||
|
||||
it('maps errors to a friendly status message', () => {
|
||||
expect(resolveMobileUpdateStatusMessage('error', new Error('Required app information could not be fetched')))
|
||||
expect(resolveMobileUpdateStatusMessage('error', new Error('Required app information could not be fetched'), null, instant))
|
||||
.toContain('store');
|
||||
|
||||
expect(getMobileUpdateStatusLabel('update-available')).toBe('Update available');
|
||||
expect(getMobileUpdateStatusLabel('update-available', instant)).toBe('Update available');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export type AppTranslateFn = (key: string, params?: Record<string, string | number>) => string;
|
||||
|
||||
export type MobileUpdateAvailability = 'unknown' | 'update-available' | 'update-not-available' | 'checking';
|
||||
|
||||
export type MobileUpdateStatus =
|
||||
@@ -33,8 +35,10 @@ export interface MobileUpdateState {
|
||||
}
|
||||
|
||||
export function createInitialMobileUpdateState(
|
||||
options: { isSupported: boolean; currentVersion?: string } = { isSupported: false }
|
||||
options: { isSupported: boolean; currentVersion?: string; translate?: AppTranslateFn } = { isSupported: false }
|
||||
): MobileUpdateState {
|
||||
const translate = options.translate ?? identityTranslate;
|
||||
|
||||
return {
|
||||
availableVersion: null,
|
||||
currentVersion: options.currentVersion ?? '0.0.0',
|
||||
@@ -45,8 +49,8 @@ export function createInitialMobileUpdateState(
|
||||
platform: null,
|
||||
status: options.isSupported ? 'idle' : 'unsupported',
|
||||
statusMessage: options.isSupported
|
||||
? 'Waiting for the first store update check.'
|
||||
: 'Store updates are only available in the packaged Android or iOS app.'
|
||||
? translate('mobile.update.messages.waitingForCheck')
|
||||
: translate('mobile.update.messages.unsupported')
|
||||
};
|
||||
}
|
||||
|
||||
@@ -72,8 +76,10 @@ export function buildMobileUpdateState(
|
||||
lastCheckedAt?: number | null;
|
||||
statusOverride?: MobileUpdateStatus;
|
||||
statusMessage?: string | null;
|
||||
translate?: AppTranslateFn;
|
||||
}
|
||||
): MobileUpdateState {
|
||||
const translate = options.translate ?? identityTranslate;
|
||||
const status = options.statusOverride ?? mapUpdateAvailabilityToStatus(snapshot.updateAvailability);
|
||||
|
||||
return {
|
||||
@@ -85,67 +91,83 @@ export function buildMobileUpdateState(
|
||||
lastCheckedAt: options.lastCheckedAt ?? null,
|
||||
platform: snapshot.platform,
|
||||
status,
|
||||
statusMessage: options.statusMessage ?? resolveMobileUpdateStatusMessage(status, null, snapshot)
|
||||
statusMessage: options.statusMessage ?? resolveMobileUpdateStatusMessage(status, null, snapshot, translate)
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveMobileUpdateStatusMessage(
|
||||
status: MobileUpdateStatus,
|
||||
error: unknown = null,
|
||||
snapshot: MobileAppUpdateInfoSnapshot | null = null
|
||||
snapshot: MobileAppUpdateInfoSnapshot | null = null,
|
||||
translate: AppTranslateFn = identityTranslate
|
||||
): string | null {
|
||||
if (status === 'error') {
|
||||
const message = error instanceof Error ? error.message : String(error ?? '');
|
||||
|
||||
if (message.includes('Required app information could not be fetched')) {
|
||||
return 'The app store could not return release information. Confirm the app is published and try again.';
|
||||
return translate('mobile.update.messages.storeInfoFailed');
|
||||
}
|
||||
|
||||
return message.trim() || 'Unable to check for mobile app updates.';
|
||||
return message.trim() || translate('mobile.update.messages.checkFailed');
|
||||
}
|
||||
|
||||
if (status === 'unsupported') {
|
||||
return 'Store updates are only available in the packaged Android or iOS app.';
|
||||
return translate('mobile.update.messages.unsupported');
|
||||
}
|
||||
|
||||
if (status === 'checking') {
|
||||
return 'Checking the app store for a newer release...';
|
||||
return translate('mobile.update.messages.checking');
|
||||
}
|
||||
|
||||
if (status === 'downloading') {
|
||||
return 'Downloading the update from the app store...';
|
||||
return translate('mobile.update.messages.downloading');
|
||||
}
|
||||
|
||||
if (status === 'update-available' && snapshot?.availableVersion) {
|
||||
return `MetoYou ${snapshot.availableVersion} is available in the app store.`;
|
||||
return translate('mobile.update.messages.updateAvailable', { version: snapshot.availableVersion });
|
||||
}
|
||||
|
||||
if (status === 'up-to-date' && snapshot?.currentVersion) {
|
||||
return `MetoYou ${snapshot.currentVersion} is up to date.`;
|
||||
return translate('mobile.update.messages.upToDate', { version: snapshot.currentVersion });
|
||||
}
|
||||
|
||||
if (status === 'idle') {
|
||||
return 'Waiting for the first store update check.';
|
||||
return translate('mobile.update.messages.waitingForCheck');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getMobileUpdateStatusLabel(status: MobileUpdateStatus): string {
|
||||
export function getMobileUpdateStatusLabel(
|
||||
status: MobileUpdateStatus,
|
||||
translate: AppTranslateFn = identityTranslate
|
||||
): string {
|
||||
switch (status) {
|
||||
case 'checking':
|
||||
return 'Checking';
|
||||
return translate('mobile.update.statusLabels.checking');
|
||||
case 'downloading':
|
||||
return 'Downloading';
|
||||
return translate('mobile.update.statusLabels.downloading');
|
||||
case 'update-available':
|
||||
return 'Update available';
|
||||
return translate('mobile.update.statusLabels.updateAvailable');
|
||||
case 'up-to-date':
|
||||
return 'Up to date';
|
||||
return translate('mobile.update.statusLabels.upToDate');
|
||||
case 'unsupported':
|
||||
return 'Unsupported';
|
||||
return translate('mobile.update.statusLabels.unsupported');
|
||||
case 'error':
|
||||
return 'Error';
|
||||
return translate('mobile.update.statusLabels.error');
|
||||
default:
|
||||
return 'Idle';
|
||||
return translate('mobile.update.statusLabels.idle');
|
||||
}
|
||||
}
|
||||
|
||||
function identityTranslate(key: string, params?: Record<string, string | number>): string {
|
||||
let value = key;
|
||||
|
||||
if (params) {
|
||||
for (const [paramKey, paramValue] of Object.entries(params)) {
|
||||
value = value.replace(`{{${paramKey}}}`, String(paramValue));
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -8,12 +8,14 @@ import {
|
||||
|
||||
import { ElectronBridgeService } from '../../../core/platform/electron/electron-bridge.service';
|
||||
import { ViewportService } from '../../../core/platform/viewport.service';
|
||||
import { initializeAppI18nForTests, provideAppI18nForTests } from '../../../core/i18n/app-i18n.testing';
|
||||
import { MobileAppUpdateService } from './mobile-app-update.service';
|
||||
import { MobilePlatformService } from './mobile-platform.service';
|
||||
|
||||
function createService(isMobile: boolean): MobileAppUpdateService {
|
||||
const injector = Injector.create({
|
||||
providers: [
|
||||
...provideAppI18nForTests(),
|
||||
MobileAppUpdateService,
|
||||
MobilePlatformService,
|
||||
{
|
||||
@@ -29,6 +31,8 @@ function createService(isMobile: boolean): MobileAppUpdateService {
|
||||
]
|
||||
});
|
||||
|
||||
initializeAppI18nForTests(injector);
|
||||
|
||||
return runInInjectionContext(injector, () => injector.get(MobileAppUpdateService));
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
|
||||
import { WebMobileAppUpdateAdapter } from '../adapters/web/web-mobile-app-update.adapter';
|
||||
import type { MobileAppUpdateAdapter } from '../contracts/mobile.contracts';
|
||||
import { AppI18nService } from '../../../core/i18n';
|
||||
import { resolveMobileAdapter } from '../logic/mobile-capacitor-adapter.rules';
|
||||
import {
|
||||
buildMobileUpdateState,
|
||||
@@ -21,6 +22,7 @@ const DEFAULT_POLL_INTERVAL_MS = 30 * 60_000;
|
||||
export class MobileAppUpdateService {
|
||||
readonly state = signal<MobileUpdateState>(createInitialMobileUpdateState());
|
||||
|
||||
private readonly appI18n = inject(AppI18nService);
|
||||
private readonly mobilePlatform = inject(MobilePlatformService);
|
||||
private adapter: MobileAppUpdateAdapter = new WebMobileAppUpdateAdapter();
|
||||
private adapterReady: Promise<MobileAppUpdateAdapter> | null = null;
|
||||
@@ -41,7 +43,8 @@ export class MobileAppUpdateService {
|
||||
const adapter = await this.ensureAdapter();
|
||||
|
||||
this.state.set(createInitialMobileUpdateState({
|
||||
isSupported: adapter.isSupported
|
||||
isSupported: adapter.isSupported,
|
||||
translate: (key, params) => this.appI18n.instant(key, params)
|
||||
}));
|
||||
|
||||
if (!adapter.isSupported) {
|
||||
@@ -62,7 +65,7 @@ export class MobileAppUpdateService {
|
||||
this.state.update((current) => ({
|
||||
...current,
|
||||
status: 'checking',
|
||||
statusMessage: resolveMobileUpdateStatusMessage('checking')
|
||||
statusMessage: resolveMobileUpdateStatusMessage('checking', null, null, (key, params) => this.appI18n.instant(key, params))
|
||||
}));
|
||||
|
||||
try {
|
||||
@@ -70,14 +73,18 @@ export class MobileAppUpdateService {
|
||||
|
||||
this.state.set(buildMobileUpdateState(snapshot, {
|
||||
isSupported: true,
|
||||
lastCheckedAt: Date.now()
|
||||
lastCheckedAt: Date.now(),
|
||||
translate: (key, params) => this.appI18n.instant(key, params)
|
||||
}));
|
||||
} catch (error) {
|
||||
this.state.set({
|
||||
...createInitialMobileUpdateState({ isSupported: true }),
|
||||
...createInitialMobileUpdateState({
|
||||
isSupported: true,
|
||||
translate: (key, params) => this.appI18n.instant(key, params)
|
||||
}),
|
||||
lastCheckedAt: Date.now(),
|
||||
status: 'error',
|
||||
statusMessage: resolveMobileUpdateStatusMessage('error', error)
|
||||
statusMessage: resolveMobileUpdateStatusMessage('error', error, null, (key, params) => this.appI18n.instant(key, params))
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -102,7 +109,7 @@ export class MobileAppUpdateService {
|
||||
this.state.update((current) => ({
|
||||
...current,
|
||||
status: 'downloading',
|
||||
statusMessage: resolveMobileUpdateStatusMessage('downloading')
|
||||
statusMessage: resolveMobileUpdateStatusMessage('downloading', null, null, (key, params) => this.appI18n.instant(key, params))
|
||||
}));
|
||||
|
||||
await adapter.performImmediateUpdate();
|
||||
@@ -118,7 +125,7 @@ export class MobileAppUpdateService {
|
||||
this.state.update((current) => ({
|
||||
...current,
|
||||
status: 'downloading',
|
||||
statusMessage: resolveMobileUpdateStatusMessage('downloading')
|
||||
statusMessage: resolveMobileUpdateStatusMessage('downloading', null, null, (key, params) => this.appI18n.instant(key, params))
|
||||
}));
|
||||
|
||||
await adapter.startFlexibleUpdate();
|
||||
|
||||
Reference in New Issue
Block a user