Skip to content
Draft
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 package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,6 @@
},
"prettier": "@nordicsemiconductor/pc-nrfconnect-shared/config/prettier.config.js",
"devDependencies": {
"@nordicsemiconductor/pc-nrfconnect-shared": "^246.0.0"
"@nordicsemiconductor/pc-nrfconnect-shared": "github:nordicsemi/pc-nrfconnect-shared#feat/auth-ipc"
}
}
Binary file added resources/Logomark.png

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

logo looks too small for retina. Is it feasible to use svg or add @2x version? if not quick to do then just ignore

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
367 changes: 367 additions & 0 deletions resources/cloud_qr.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file not shown.
17,026 changes: 17,026 additions & 0 deletions resources/devices/firmware/nrf54l15dk_quickstart_bluetooth.hex

Large diffs are not rendered by default.

Binary file added resources/devices/images/cloud_button_state.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added resources/devices/images/cloud_led_toggle.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 17 additions & 0 deletions src/common/cleanShellOutput.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* Copyright (c) 2026 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: LicenseRef-Nordic-4-Clause
*/

// TODO: Replace this with a library that can handle ANSI removal.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

if we can skip adding new dependency, better not to add :) or this regex is not complete?

const ansiPattern = [
'[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)',
'(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))',
].join('|');

export const cleanShellOutput = (raw: string) =>
raw
.replace(new RegExp(ansiPattern, 'g'), '')
.replace(/uart:~\$ ?/g, '') // Zephyr shell prompt
.replace(/^> ?/gm, ''); // command-echo prompt
4 changes: 3 additions & 1 deletion src/common/steps/5xFamilyVerify/Verify.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
getSelectedDeviceUnsafely,
} from '../../../features/device/deviceSlice';
import { Back } from '../../Back';
import { cleanShellOutput } from '../../cleanShellOutput';
import Main from '../../Main';
import { Next, Skip } from '../../Next';
import runVerification from './serialport';
Expand Down Expand Up @@ -58,7 +59,8 @@ export default ({ vComIndex, regex }: { vComIndex: number; regex: RegExp }) => {

useEffect(() => {
if (!validResponse) {
const [, match] = (response || '').match(regex) ?? [];
const stripped = cleanShellOutput(response || '');
const [, match] = stripped.match(regex) ?? [];
Comment on lines +62 to +63

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

is this safe for all other devices?

if (match) {
setValidResponse(match);
}
Expand Down
1 change: 1 addition & 0 deletions src/features/device/deviceSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export interface Firmware {
core?: DeviceCore;
coreLabel?: string;
file: string;
elfFile?: string;
link?: { label: string; href: string };
}

Expand Down
2 changes: 1 addition & 1 deletion src/features/flows/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import type React from 'react';

import NRF52 from './nRF52';
import NRF54L15 from './nRF54L15';
import NRF54L15 from './nRF54L15_cloud';
import NRF54LM20 from './nRF54LM20';
import NRF54LS05 from './nRF54LS05';
import NRF54LV10 from './nRF54LV10';
Expand Down
225 changes: 225 additions & 0 deletions src/features/flows/nRF54L15_cloud/evaluate/Authenticate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
/*
* Copyright (c) 2026 Nordic Semiconductor ASA
*
* SPDX-License-Identifier: LicenseRef-Nordic-4-Clause
*/

import React, { useEffect, useState } from 'react';
import {
Button,
Dropdown,
type DropdownItem,
Spinner,
} from '@nordicsemiconductor/pc-nrfconnect-shared';
import {
type AuthState,
inMain as auth,
} from '@nordicsemiconductor/pc-nrfconnect-shared/ipc/auth';

import Logomark from '../../../../../resources/Logomark.png';
import { useAppDispatch, useAppSelector } from '../../../../app/store';
import { Back } from '../../../../common/Back';
import Main from '../../../../common/Main';
import { Next, Skip } from '../../../../common/Next';
import { connectMemfault, selectOrganization } from './authEffects';
import {
getMemfault,
nextSubStep,
prevSubStep,
resetMemfault,
setSelectedProjectSlug,
} from './cloudEvaluateSlice';

const emptyItem: DropdownItem<string> = { label: '', value: '' };

export default () => {
const dispatch = useAppDispatch();
const memfault = useAppSelector(getMemfault);
const [authState, setAuthState] = useState<AuthState | null>(null);
const [authError, setAuthError] = useState<string | null>(null);

const status = authState?.status;
const account = authState?.account ?? null;

const isSignedIn = status === 'signedIn' || status === 'signingOut';
const isAuthenticating = status === 'signingIn';
const needsReauth = status === 'interactionRequired';

useEffect(() => {
auth.getAuthStatus().then(setAuthState);
auth.registerOnStateChanged(setAuthState);
}, []);

const login = async () => {
setAuthError(null);
const res = await auth.startLogin();
if (!res.status) {
setAuthError(res.error);
return;
}
dispatch(resetMemfault());
};

const logout = async () => {
await auth.singleSignOut();
dispatch(resetMemfault());
};

useEffect(() => {
if (status === 'signedIn' && memfault.status === 'idle') {
dispatch(connectMemfault());
}
}, [status, memfault.status, dispatch]);

const orgItems: DropdownItem<string>[] = memfault.organizations.map(o => ({
label: o.name,
value: o.slug,
}));
const projectItems: DropdownItem<string>[] = memfault.projects.map(p => ({
label: p.name,
value: p.slug,
}));
const selectedOrgItem =
orgItems.find(i => i.value === memfault.selectedOrgSlug) ?? emptyItem;
const selectedProjectItem =
projectItems.find(i => i.value === memfault.selectedProjectSlug) ??
emptyItem;

const signInPrompt = needsReauth
? 'Your session expired. Please sign in again.'
: 'Sign in to continue.';

return (
<Main>
<Main.Content heading="Register your device" subHeading="">
<div className="tw-flex tw-flex-col tw-gap-3">
<div className="tw-flex tw-flex-col tw-gap-2">
<p>
Connect your device to cloud to capture crashes,
push OTA updates, and debug remotely.
</p>
<ol className="tw-list-inside tw-list-disc">
<li>Over-the-air firmware updates</li>
<li>Remote crash analysis and debugging</li>
<li>
Access to DevZone, technical documentation, and
learning resources
</li>
</ol>
</div>
{isSignedIn ? (
<div className="tw-flex tw-flex-col tw-gap-2 tw-border tw-border-gray-200 tw-p-3">
<div className="tw-flex tw-flex-row tw-items-center tw-justify-between">
<p>
Logged in as{' '}
<b>{account?.name ?? account?.username}</b>
</p>
<Button
variant="secondary"
size="sm"
onClick={logout}
disabled={status === 'signingOut'}
>
{status === 'signingOut'
? 'Logging out…'
: 'Log out'}
</Button>
</div>

{memfault.status === 'loading' && (
<div className="tw-flex tw-flex-row tw-items-center tw-gap-2">
<Spinner size="sm" />
<span>
Loading organizations and projects…
</span>
</div>
)}

{memfault.status === 'error' && (
<p className="tw-text-red">
{memfault.message ??
'Failed to load organizations and projects'}
</p>
)}

{memfault.status === 'success' && (
<div className="tw-flex tw-flex-row tw-gap-2">
<Dropdown
label="Organization"
items={orgItems}
onSelect={item =>
dispatch(
selectOrganization(item.value),
)
}
selectedItem={selectedOrgItem}
size="sm"
/>
<Dropdown
label="Project"
items={projectItems}
onSelect={item =>
dispatch(
setSelectedProjectSlug(
item.value,
),
)
}
selectedItem={selectedProjectItem}
size="sm"
/>
</div>
)}
</div>
) : (
<>
<p className={needsReauth ? 'tw-text-red' : ''}>
{signInPrompt}
</p>
<Button
variant="secondary"
size="lg"
onClick={login}
className="tw-w-fit"
disabled={isAuthenticating}
>
<div className="tw-flex tw-flex-row tw-items-center tw-justify-center tw-gap-2">
<img src={Logomark} alt="myNordic logo" />
<span>
Sign in with myNordic to register your
device
</span>
{isAuthenticating && <Spinner size="sm" />}
</div>
</Button>
{authError && (
<p className="tw-text-red">{authError}</p>
)}
</>
)}
</div>
</Main.Content>
<Main.Footer>
<Back onClick={() => dispatch(prevSubStep())} />
{memfault.status === 'error' ? (
<>
<Skip onClick={() => dispatch(nextSubStep())} />
<Next
label="Retry"
onClick={() => dispatch(connectMemfault())}
/>
</>
) : (
<Next
disabled={
status !== 'signedIn' ||
memfault.status !== 'success' ||
!memfault.selectedProjectSlug
}
onClick={() => dispatch(nextSubStep())}
/>
)}
</Main.Footer>
</Main>
);
};
Loading
Loading