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
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ internal object MobileDesignColors {
val StartWindowBackground = Color(0xFFFFFFFF)
val Transparent = Color(0x00000000)
val PageBg = Color(0xFFFFFFFF)
val WelcomePageBg = Color(0xFFFFFFFF)
val PageBgFade = Color(0x00FFFFFF)
val PageBgOverlay = Color(0xB8FFFFFF)
val Ink = Color(0xFF171717)
Expand Down Expand Up @@ -75,6 +76,7 @@ internal object MobileDesignColors {
val StartWindowBackground = Color(0xFF000000)
val Transparent = Color(0x00000000)
val PageBg = Color(0xFF151514)
val WelcomePageBg = Color(0xFF252522)
val PageBgFade = Color(0x00151514)
val PageBgOverlay = Color(0xB8151514)
val Ink = Color(0xFFF4F3EF)
Expand Down Expand Up @@ -127,7 +129,7 @@ internal object MobileDesignColors {
val CodeProperty = Color(0xFF9CC8D0)
val CodeTargetBg = Color(0xFF5A4E24)
val BrandDot = Color(0xFF16B9CE)
val WelcomeDock = Color(0xFF171917)
val WelcomeDock = Color(0xFF000000)
val WelcomeButton = Color(0xFFFFFFFF)
val WelcomeButtonLabel = Color(0xFF171917)
val WelcomeSecondary = Color(0xFFB9BCB9)
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion src/apps/mobile/design-system/tokens/mobile-tokens.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
"light": "#FFFFFF",
"dark": "#151514"
},
"welcome_page_bg": {
"light": "#FFFFFF",
"dark": "#252522"
},
"page_bg_fade": {
"light": "#00FFFFFF",
"dark": "#00151514"
Expand Down Expand Up @@ -227,7 +231,7 @@
},
"welcome_dock": {
"light": "#171917",
"dark": "#171917"
"dark": "#000000"
},
"welcome_button": {
"light": "#FFFFFF",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export class MobileDesignColors {
static readonly startWindowBackground: MobileColorPair = new MobileColorPair("#FFFFFF", "#000000");
static readonly transparent: MobileColorPair = new MobileColorPair("#00000000", "#00000000");
static readonly pageBg: MobileColorPair = new MobileColorPair("#FFFFFF", "#151514");
static readonly welcomePageBg: MobileColorPair = new MobileColorPair("#FFFFFF", "#252522");
static readonly pageBgFade: MobileColorPair = new MobileColorPair("#00FFFFFF", "#00151514");
static readonly pageBgOverlay: MobileColorPair = new MobileColorPair("#B8FFFFFF", "#B8151514");
static readonly ink: MobileColorPair = new MobileColorPair("#171717", "#F4F3EF");
Expand Down Expand Up @@ -66,7 +67,7 @@ export class MobileDesignColors {
static readonly codeProperty: MobileColorPair = new MobileColorPair("#466D78", "#9CC8D0");
static readonly codeTargetBg: MobileColorPair = new MobileColorPair("#FFF1BE", "#5A4E24");
static readonly brandDot: MobileColorPair = new MobileColorPair("#16B9CE", "#16B9CE");
static readonly welcomeDock: MobileColorPair = new MobileColorPair("#171917", "#171917");
static readonly welcomeDock: MobileColorPair = new MobileColorPair("#171917", "#000000");
static readonly welcomeButton: MobileColorPair = new MobileColorPair("#FFFFFF", "#FFFFFF");
static readonly welcomeButtonLabel: MobileColorPair = new MobileColorPair("#171917", "#171917");
static readonly welcomeSecondary: MobileColorPair = new MobileColorPair("#B9BCB9", "#B9BCB9");
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export const PAGE_BG: ResourceColor = $r('app.color.page_bg');
export const WELCOME_PAGE_BG: ResourceColor = $r('app.color.welcome_page_bg');
/** PAGE_BG at zero alpha, per theme — the transparent end of a fade-out. */
export const PAGE_BG_FADE: ResourceColor = $r('app.color.page_bg_fade');
/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { RemoteI18n } from '../../i18n/RemoteI18n';
import { MobileDesignTypography, MobileDesignGeometry as G } from '../../generated/MobileDesignTokens';
import { MotionPreference } from '../../services/MotionPreference';
import { WindowSystemBarService } from '../../services/WindowSystemBarService';
import { WelcomeBrandFlow } from './WelcomeBrandFlow';
import { INK, PAGE_BG, MUTED, TRANSPARENT } from './Theme';
import { INK, PAGE_BG, WELCOME_PAGE_BG, MUTED, TRANSPARENT } from './Theme';

/** A welcome composition with the C staggered-slide phrase timeline. */
@ComponentV2
Expand All @@ -13,10 +14,22 @@ export struct WelcomeHome {
@Local elapsed: number = 0;
@Local pageHeight: number = 700;
@Local pageWidth: number = 390;
@Local topInset: number = 0;
private timer: number = -1;
private started: number = 0;
private immersiveCompact: boolean = false;

aboutToDisappear(): void { this.stop(); }
aboutToDisappear(): void {
this.stop();
if (this.immersiveCompact) {
const context = this.getUIContext().getHostContext();
if (context) {
void WindowSystemBarService.setLayoutFullScreen(context, false);
}
this.immersiveCompact = false;
this.topInset = 0;
}
}
private stop(): void { if (this.timer >= 0) clearInterval(this.timer); this.timer = -1; }
private play(): void {
this.stop();
Expand Down Expand Up @@ -57,7 +70,6 @@ export struct WelcomeHome {
private landscape(): boolean {
return this.pageWidth >= 840 && this.pageWidth / Math.max(1, this.pageHeight) >= 1.2;
}

build() {
Scroll() {
Column() {
Expand All @@ -80,21 +92,52 @@ export struct WelcomeHome {
}.width('100%').constraintSize({ minHeight: Math.max(540, this.pageHeight - G.welcomeHeaderHeight) })
.justifyContent(FlexAlign.Center).padding({ bottom: 64 })
} else {
Column() { this.Hero() }
.justifyContent(FlexAlign.Center).width('100%')
.height(Math.max(300, this.pageHeight - G.welcomeHeaderHeight - 228))
.padding({ bottom: 24 })
this.Actions()
Column() {
Column() { this.Hero() }
.justifyContent(FlexAlign.Center).width('100%')
.layoutWeight(1)
.constraintSize({ minHeight: 300 })
.padding({ bottom: 24 })
this.Actions()
}.width('100%').height(Math.max(0,
this.pageHeight - this.topInset - G.welcomeHeaderHeight))
}
}.width('100%')
}.width('100%').height('100%').scrollBar(BarState.Off).backgroundColor(PAGE_BG)
}.width('100%').height('100%').scrollBar(BarState.Off).backgroundColor(WELCOME_PAGE_BG)
.padding({ top: this.topInset })
.onSizeChange((_old: SizeOptions, size: SizeOptions) => {
this.pageHeight = Number(size.height);
this.pageWidth = Number(size.width);
this.updateWindowLayout();
})
.onVisibleAreaChange([0], (visible: boolean) => { if (visible) this.play(); else this.stop(); })
}

private updateWindowLayout(): void {
const compact = this.pageWidth < 600;
if (compact === this.immersiveCompact) {
return;
}
const context = this.getUIContext().getHostContext();
if (!context) {
return;
}
this.immersiveCompact = compact;
if (!compact) {
this.topInset = 0;
void WindowSystemBarService.setLayoutFullScreen(context, false);
return;
}
void WindowSystemBarService.setLayoutFullScreen(context, true).then(async (): Promise<void> => {
if (this.immersiveCompact) {
const topInsetPx: number = await WindowSystemBarService.topSystemInsetPx(context);
if (this.immersiveCompact) {
this.topInset = this.getUIContext().px2vp(topInsetPx);
}
}
});
}

@Builder
private Hero() {
Column({ space: this.wide() ? 16 : 20 }) {
Expand Down Expand Up @@ -126,6 +169,7 @@ export struct WelcomeHome {
.padding({ left: G.welcomeGutter, right: G.welcomeGutter, top: this.wide() ? 0 : G.welcomeGutter,
bottom: this.wide() ? 0 : G.welcomeDockBottom })
.backgroundColor(this.wide() ? TRANSPARENT : $r('app.color.welcome_dock'))
.expandSafeArea([SafeAreaType.SYSTEM], [SafeAreaEdge.BOTTOM])
.borderRadius({ topLeft: G.welcomeDockRadius, topRight: G.welcomeDockRadius })
}
@Builder
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ export class AppRootRuntime extends AppRootRuntimeComposition {
}

async aboutToAppear(onAccountRestored?: (signedIn: boolean) => void): Promise<void> {
// Account login can be tapped while the rest of the cold-start work is
// still running. Initialize its secure store first so the login path never
// races the asynchronous cache and onboarding setup below.
await this.settingsController.initializeCloudAccount(this.host.context());
await this.localeController.initialize(this.host.context());
if (this.host.offerTaskNotifications) {
await this.taskCompletionNotificationPort.offerOnboarding(
Expand All @@ -51,7 +55,6 @@ export class AppRootRuntime extends AppRootRuntimeComposition {
await this.remoteChatCache.init(this.host.context());
await this.remoteSessionListCache.init(this.host.context());
await this.restoreCachedRemoteSessions();
await this.settingsController.initializeCloudAccount(this.host.context());
if (onAccountRestored) onAccountRestored(this.settingsController.hasCloudAccountSession());
if (this.settingsController.hasCloudAccountSession()) {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export interface PreferredCloudTarget {
/** Owns the authenticated account and remote device selection. */
export class SettingsController {
private readonly cloud?: CloudAccountSettingsDependencies;
private cloudInitialization?: Promise<void>;
private cloudSession?: CloudAccountSession;
private cloudRelayUrl: string = '';
private selectedRelayUrl: string = DEFAULT_CLOUD_RELAY_URL;
Expand All @@ -89,6 +90,13 @@ export class SettingsController {
}

async initializeCloudAccount(context: Context): Promise<void> {
if (!this.cloudInitialization) {
this.cloudInitialization = this.initializeCloudAccountInternal(context);
}
await this.cloudInitialization;
}

private async initializeCloudAccountInternal(context: Context): Promise<void> {
const cloud = this.requireCloud();
await cloud.sessionStore.init(context);
await this.restoreCloudAccountSession();
Expand Down Expand Up @@ -160,8 +168,17 @@ export class SettingsController {
const relayUrl = this.selectedRelayUrl;
const generation = ++this.accountLoginVersion;
const cloud = this.requireCloud();
if (this.cloudInitialization) {
await this.cloudInitialization;
}
RemoteLogger.info('cloud account UI login requested');
const deviceSecret = await cloud.sessionStore.deviceSecret();
let deviceSecret: Uint8Array;
try {
deviceSecret = await cloud.sessionStore.deviceSecret();
} catch (err) {
RemoteLogger.error(`cloud account device identity failed: ${err instanceof Error ? err.message : String(err)}`);
throw err instanceof Error ? err : new Error(String(err));
}
if (generation !== this.accountLoginVersion) { deviceSecret.fill(0); throw new Error('Account login cancelled'); }
let session: CloudAccountSession;
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { preferences } from '@kit.ArkData';
import { Encoding } from './Encoding';
import { HarmonyUpgradeIdentityContract } from './HarmonyUpgradeIdentityContract';
import { OhosError } from './OhosError';
import { RemoteLogger } from './RemoteLogger';

const STORE_NAME: string = HarmonyUpgradeIdentityContract.CLOUD_ACCOUNT_STORE;
const CIPHER_KEY: string = 'github_device_session_v1_cipher';
Expand Down Expand Up @@ -49,10 +50,33 @@ export class CloudAccountSessionStore {
const previous = await this.load();
const secret = previous ? Encoding.base64ToBytes(previous.masterKey) : Encoding.randomBytes(32);
if (secret.length !== 32) throw new Error('Stored device identity is invalid.');
await this.writeSealed('device_private_key_v1_cipher', 'device_private_key_v1_iv', Encoding.bytesToBase64(secret));
try {
await this.writeSealed('device_private_key_v1_cipher', 'device_private_key_v1_iv', Encoding.bytesToBase64(secret));
} catch (err) {
// A reinstall clears Preferences but HarmonyOS may retain the HUKS alias.
// Only rotate the alias when there is no prior session record: existing
// encrypted account data must never be made unreadable as recovery.
if (previous) throw err instanceof Error ? err : new Error(OhosError.reason(err));
RemoteLogger.warn('cloud account device identity write failed on fresh install; recreating local key');
await this.recreateKeyForFreshInstall();
await this.writeSealed('device_private_key_v1_cipher', 'device_private_key_v1_iv', Encoding.bytesToBase64(secret));
}
return secret;
}

private async recreateKeyForFreshInstall(): Promise<void> {
try {
await huks.deleteKeyItem(HUKS_ALIAS, {});
} catch (err) {
const message = OhosError.reason(err);
if (message.indexOf('12000011') < 0 && message.indexOf('-13') < 0 &&
message.toLowerCase().indexOf('does not exist') < 0) {
throw err instanceof Error ? err : new Error(OhosError.reason(err));
}
}
await this.ensureKey();
}

async loadProfile(userId: string): Promise<GitHubAccountProfile | undefined> {
const text = await this.readSealed(`github_profile_${userId}_v1_cipher`, `github_profile_${userId}_v1_iv`);
return readCachedGitHubProfile(userId, text || '');
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Context } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';

/** Platform window adapter for surfaces that need to draw behind system bars. */
export class WindowSystemBarService {
static async setLayoutFullScreen(context: Context, enabled: boolean): Promise<void> {
try {
const appWindow = await window.getLastWindow(context);
await appWindow.setWindowLayoutFullScreen(enabled);
} catch (_err) {
// Window layout is best effort and must not block page rendering.
}
}

static async topSystemInsetPx(context: Context): Promise<number> {
try {
const appWindow = await window.getLastWindow(context);
return appWindow.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM).topRect.height;
} catch (_err) {
return 0;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
"name": "page_bg",
"value": "#FFFFFF"
},
{
"name": "welcome_page_bg",
"value": "#FFFFFF"
},
{
"name": "page_bg_fade",
"value": "#00FFFFFF"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
"name": "page_bg",
"value": "#151514"
},
{
"name": "welcome_page_bg",
"value": "#252522"
},
{
"name": "page_bg_fade",
"value": "#00151514"
Expand Down Expand Up @@ -222,7 +226,7 @@
},
{
"name": "welcome_dock",
"value": "#171917"
"value": "#000000"
},
{
"name": "welcome_button",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ enum MobileDesignColors {
static let startWindowBackground = dynamic(light: 0xFFFFFFFF, dark: 0xFF000000)
static let transparent = dynamic(light: 0x00000000, dark: 0x00000000)
static let pageBg = dynamic(light: 0xFFFFFFFF, dark: 0xFF151514)
static let welcomePageBg = dynamic(light: 0xFFFFFFFF, dark: 0xFF252522)
static let pageBgFade = dynamic(light: 0x00FFFFFF, dark: 0x00151514)
static let pageBgOverlay = dynamic(light: 0xB8FFFFFF, dark: 0xB8151514)
static let ink = dynamic(light: 0xFF171717, dark: 0xFFF4F3EF)
Expand Down Expand Up @@ -77,7 +78,7 @@ enum MobileDesignColors {
static let codeProperty = dynamic(light: 0xFF466D78, dark: 0xFF9CC8D0)
static let codeTargetBg = dynamic(light: 0xFFFFF1BE, dark: 0xFF5A4E24)
static let brandDot = dynamic(light: 0xFF16B9CE, dark: 0xFF16B9CE)
static let welcomeDock = dynamic(light: 0xFF171917, dark: 0xFF171917)
static let welcomeDock = dynamic(light: 0xFF171917, dark: 0xFF000000)
static let welcomeButton = dynamic(light: 0xFFFFFFFF, dark: 0xFFFFFFFF)
static let welcomeButtonLabel = dynamic(light: 0xFF171917, dark: 0xFF171917)
static let welcomeSecondary = dynamic(light: 0xFFB9BCB9, dark: 0xFFB9BCB9)
Expand Down
Loading
Loading