Skip to content

Commit 7c273af

Browse files
committed
Merge branch 'bt-name'
2 parents c32c2b6 + 7bdea75 commit 7c273af

File tree

13 files changed

+101
-95
lines changed

13 files changed

+101
-95
lines changed

frontends/ios/BitBoxApp/BitBoxApp/Bluetooth.swift

+11-3
Original file line numberDiff line numberDiff line change
@@ -127,13 +127,13 @@ class BluetoothManager: NSObject, ObservableObject, CBCentralManagerDelegate, CB
127127

128128
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String: Any], rssi RSSI: NSNumber) {
129129
let identifier = peripheral.identifier
130+
print("BLE: discovered \(peripheral.name ?? "unknown device")")
130131
if state.discoveredPeripherals[identifier] == nil {
131132
state.discoveredPeripherals[identifier] = PeripheralMetadata(
132133
peripheral: peripheral,
133134
discoveredDate: Date(),
134135
connectionState: .discovered
135136
)
136-
print("BLE: discovered \(peripheral.name ?? "unknown device")")
137137
if let data = advertisementData["kCBAdvDataManufacturerData"] as? Data {
138138
let data = data.advanced(by: 2) // 2 bytes for manufacturer ID
139139
print("BLE: manufacturer data: \(data.hexEncodedString())")
@@ -149,9 +149,10 @@ class BluetoothManager: NSObject, ObservableObject, CBCentralManagerDelegate, CB
149149
print("BLE: skip auto-connect for device \(identifier.uuidString)")
150150
}
151151
}
152-
153-
updateBackendState()
154152
}
153+
154+
// We update the state for the frontend even if we had already registered the device as the name may have been updated.
155+
updateBackendState()
155156
}
156157

157158
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
@@ -164,6 +165,13 @@ class BluetoothManager: NSObject, ObservableObject, CBCentralManagerDelegate, CB
164165
peripheral.discoverServices(nil)
165166
}
166167

168+
func peripheralDidUpdateName(_ peripheral: CBPeripheral) {
169+
print("BLE: didUpdateName, new name: \(peripheral.name ?? "unknown device")")
170+
// The peripheral is already in our state, so we just update the frontend to show
171+
// the new name.
172+
updateBackendState()
173+
}
174+
167175
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
168176
let errorMessage = error?.localizedDescription ?? "unknown error"
169177
state.discoveredPeripherals[peripheral.identifier]?.connectionState = .error

frontends/web/src/api/bluetooth.ts

+9-10
Original file line numberDiff line numberDiff line change
@@ -21,22 +21,21 @@ export type TPeripheral = {
2121
identifier: string;
2222
name: string;
2323
} & (
24-
| {
25-
connectionState: 'discovered' | 'connecting' | 'connected';
26-
}
27-
| {
28-
connectionState: 'error';
29-
connectionError: string;
30-
}
24+
{
25+
connectionState: 'discovered' | 'connecting' | 'connected';
26+
} | {
27+
connectionState: 'error';
28+
connectionError: string;
29+
}
3130
);
3231

33-
export type TState = {
32+
type TBluetoothState = {
3433
bluetoothAvailable: boolean;
3534
scanning: boolean;
3635
peripherals: TPeripheral[];
3736
};
3837

39-
export const getState = (): Promise<TState> => {
38+
export const getState = (): Promise<TBluetoothState> => {
4039
return apiGet('bluetooth/state');
4140
};
4241

@@ -45,7 +44,7 @@ export const connect = (identifier: string): Promise<void> => {
4544
};
4645

4746
export const syncState = (
48-
cb: (state: TState) => void
47+
cb: (state: TBluetoothState) => void
4948
): TUnsubscribe => {
5049
return subscribeEndpoint('bluetooth/state', cb);
5150
};

frontends/web/src/components/actionable-item/actionable-item.tsx

+8-9
Original file line numberDiff line numberDiff line change
@@ -22,36 +22,35 @@ type TProps = {
2222
className?: string;
2323
disabled?: boolean;
2424
children: ReactNode;
25+
icon?: ReactNode;
2526
onClick?: () => void;
2627
}
2728

2829
export const ActionableItem = ({
2930
className = '',
3031
disabled,
3132
children,
33+
icon,
3234
onClick,
3335
}: TProps) => {
3436
const notButton = disabled || onClick === undefined;
3537

36-
const content = (
37-
<div className={styles.content}>
38-
{children}
39-
<ChevronRightDark />
40-
</div>
41-
);
42-
4338
return (
4439
<>
4540
{notButton ? (
4641
<div className={`${styles.container} ${className}`}>
47-
{content}
42+
{children}
43+
{icon && icon}
4844
</div>
4945
) : (
5046
<button
5147
type="button"
5248
className={`${styles.container} ${styles.isButton} ${className}`}
5349
onClick={onClick}>
54-
{content}
50+
{children}
51+
{icon ? icon : (
52+
<ChevronRightDark />
53+
)}
5554
</button>
5655
)}
5756
</>

frontends/web/src/components/bluetooth/bluetooth.tsx

+30-9
Original file line numberDiff line numberDiff line change
@@ -16,50 +16,71 @@
1616

1717
import { useTranslation } from 'react-i18next';
1818
import { useSync } from '@/hooks/api';
19-
import { connect, getState, syncState } from '@/api/bluetooth';
19+
import { connect, getState, syncState, TPeripheral } from '@/api/bluetooth';
2020
import { runningInIOS } from '@/utils/env';
21+
import { Status } from '@/components/status/status';
2122
import { ActionableItem } from '@/components/actionable-item/actionable-item';
2223
import { Badge } from '@/components/badge/badge';
24+
import { HorizontallyCenteredSpinner, SpinnerRingAnimated } from '@/components/spinner/SpinnerAnimation';
2325
import styles from './bluetooth.module.css';
2426

27+
const isConnectedOrConnecting = (peripheral: TPeripheral) => {
28+
return peripheral.connectionState === 'connecting' || peripheral.connectionState === 'connected';
29+
};
30+
2531
const _Bluetooth = () => {
2632
const { t } = useTranslation();
2733
const state = useSync(getState, syncState);
2834
if (!state) {
2935
return null;
3036
}
3137
if (!state.bluetoothAvailable) {
32-
return <>Please turn on Bluetooth</>;
38+
return (
39+
<Status type="warning">
40+
{t('bluetooth.enable')}
41+
</Status>
42+
);
3343
}
44+
const hasConnection = state.peripherals.some(isConnectedOrConnecting);
3445
return (
3546
<>
3647
<div className={styles.label}>
3748
{t('bluetooth.select')}
3849
</div>
3950
<div className={styles.container}>
40-
{ state.scanning ? 'scanning' : null }
4151
{state.peripherals.map(peripheral => {
52+
const onClick = !hasConnection ? () => connect(peripheral.identifier) : undefined;
53+
const connectingIcon = peripheral.connectionState === 'connecting' ? (
54+
<SpinnerRingAnimated />
55+
) : undefined;
4256
return (
4357
<ActionableItem
4458
key={peripheral.identifier}
45-
onClick={() => connect(peripheral.identifier)}>
59+
icon={connectingIcon}
60+
onClick={onClick}>
4661
<span>
4762
{ peripheral.name !== '' ? peripheral.name : peripheral.identifier }
4863
{' '}
64+
{ peripheral.connectionState === 'connected' ? (
65+
<Badge type="success">
66+
{t('bluetooth.connected')}
67+
</Badge>
68+
) : null }
4969
{ peripheral.connectionState === 'error' ? (
5070
<Badge type="danger">
51-
{t('bluetooth.connectionFailed')}
71+
<span style={{ whiteSpace: 'wrap' }}>
72+
{peripheral.connectionError}
73+
</span>
5274
</Badge>
5375
) : null }
54-
{ peripheral.connectionState === 'error' ? (
55-
<p>{ peripheral.connectionError }</p>
56-
) : peripheral.connectionState }
5776
</span>
58-
5977
</ActionableItem>
6078
);
6179
})}
6280
</div>
81+
{state.scanning && (
82+
<HorizontallyCenteredSpinner />
83+
)}
6384
</>
6485
);
6586
};
Loading
Loading

frontends/web/src/components/icon/icon.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ import starInactiveSVG from './assets/icons/star-inactive.svg';
8787
import syncSVG from './assets/icons/sync.svg';
8888
import syncLightSVG from './assets/icons/sync-light.svg';
8989
import selectedCheckLightSVG from './assets/icons/selected-check-light.svg';
90+
import spinnerRingDarkSVG from './assets/icons/spinner-ring-dark.svg';
91+
import spinnerRingLightSVG from './assets/icons/spinner-ring-light.svg';
9092
import usbSuccessSVG from './assets/icons/usb-success.svg';
9193
import statusInfoSVG from './assets/icons/icon-info.svg';
9294
import statusSuccessSVG from './assets/icons/icon-success.svg';
@@ -223,6 +225,8 @@ export const RedDot = (props: ImgProps) => (<img src={redDotSVG} draggable={fals
223225
export const Save = (props: ImgProps) => (<img src={saveSVG} draggable={false} {...props} />);
224226
export const SaveLight = (props: ImgProps) => (<img src={saveLightSVG} draggable={false} {...props} />);
225227
export const Shield = (props: ImgProps) => (<img src={shieldSVG} draggable={false} {...props} />);
228+
export const SpinnerRingDark = (props: ImgProps) => (<img src={spinnerRingDarkSVG} draggable={false} {...props} />);
229+
export const SpinnerRingLight = (props: ImgProps) => (<img src={spinnerRingLightSVG} draggable={false} {...props} />);
226230
export const Star = (props: ImgProps) => (<img src={starSVG} draggable={false} {...props} />);
227231
export const StarInactive = (props: ImgProps) => (<img src={starInactiveSVG} draggable={false} {...props} />);
228232
export const Sync = (props: ImgProps) => (<img src={syncSVG} draggable={false} {...props} />);

frontends/web/src/components/spinner/Spinner.module.css

+16-46
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,17 @@
1515
}
1616

1717
.spinner {
18-
display: inline-block;
18+
align-items: center;
19+
display: inline-flex;
20+
height: 64px;
21+
justify-content: center;
1922
position: relative;
2023
width: 64px;
21-
height: 64px;
24+
}
25+
26+
.spinner img {
27+
height: 36px;
28+
width: 36px;
2229
}
2330

2431
.spinnerText {
@@ -27,58 +34,21 @@
2734
text-align: center;
2835
}
2936

30-
.spinner div {
31-
position: absolute;
32-
top: 27px;
33-
width: 11px;
34-
height: 11px;
35-
border-radius: 50%;
36-
background: var(--color-blue);
37-
animation-timing-function: cubic-bezier(0, 1, 1, 0);
38-
}
39-
40-
.spinner div:nth-child(1) {
41-
left: 6px;
42-
animation: spinner1 0.6s infinite;
43-
}
44-
45-
.spinner div:nth-child(2) {
46-
left: 6px;
47-
animation: spinner2 0.6s infinite;
48-
}
49-
50-
.spinner div:nth-child(3) {
51-
left: 26px;
52-
animation: spinner2 0.6s infinite;
53-
}
54-
55-
.spinner div:nth-child(4) {
56-
left: 45px;
57-
animation: spinner3 0.6s infinite;
58-
}
59-
6037
.horizontallyCentered {
38+
align-items: center;
39+
display: flex;
40+
height: 64px;
41+
justify-content: center;
6142
left: 50%;
6243
position: absolute;
6344
transform: translateX(-50%);
6445
}
6546

66-
@keyframes spinner1 {
67-
0% { transform: scale(0); }
68-
100% { transform: scale(1); }
69-
}
70-
71-
@keyframes spinner3 {
72-
0% { transform: scale(1); }
73-
100% { transform: scale(0); }
47+
.horizontallyCentered img {
48+
height: 36px;
49+
width: 36px;
7450
}
7551

76-
@keyframes spinner2 {
77-
0% { transform: translate(0, 0); }
78-
100% { transform: translate(19px, 0); }
79-
}
80-
81-
8252
.overlay {
8353
position: absolute;
8454
top: 0;

frontends/web/src/components/spinner/Spinner.tsx

+5-5
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,14 @@
1818
import { useContext } from 'react';
1919
import { AppContext } from '@/contexts/AppContext';
2020
import { MenuDark, MenuLight } from '@/components/icon';
21-
import { SpinnerAnimation } from './SpinnerAnimation';
21+
import { SpinnerRingAnimated } from './SpinnerAnimation';
2222
import style from './Spinner.module.css';
2323

2424
type TProps = {
2525
text?: string;
2626
}
2727

28-
const Spinner = ({ text }: TProps) => {
28+
export const Spinner = ({ text }: TProps) => {
2929
const { toggleSidebar } = useContext(AppContext);
3030

3131
return (
@@ -38,15 +38,15 @@ const Spinner = ({ text }: TProps) => {
3838
</div>
3939
</div>
4040
</div>
41+
<div className={style.spinner}>
42+
<SpinnerRingAnimated />
43+
</div>
4144
{
4245
text && text.split('\n').map((line, i) => (
4346
<p key={`${line}-${i}`} className={style.spinnerText}>{line}</p>
4447
))
4548
}
46-
<SpinnerAnimation />
4749
<div className={style.overlay}></div>
4850
</div>
4951
);
5052
};
51-
52-
export { Spinner };

frontends/web/src/components/spinner/SpinnerAnimation.tsx

+8-11
Original file line numberDiff line numberDiff line change
@@ -14,25 +14,22 @@
1414
* limitations under the License.
1515
*/
1616

17+
import { SpinnerRingDark, SpinnerRingLight } from '../icon';
1718
import style from './Spinner.module.css';
1819

19-
const SpinnerAnimation = () => {
20+
export const SpinnerRingAnimated = () => {
2021
return (
21-
<div className={style.spinner}>
22-
<div></div>
23-
<div></div>
24-
<div></div>
25-
<div></div>
26-
</div>
22+
<>
23+
<SpinnerRingDark className="show-in-lightmode" />
24+
<SpinnerRingLight className="show-in-darkmode" />
25+
</>
2726
);
2827
};
2928

30-
const HorizontallyCenteredSpinner = () => {
29+
export const HorizontallyCenteredSpinner = () => {
3130
return (
3231
<div className={style.horizontallyCentered}>
33-
<SpinnerAnimation />
32+
<SpinnerRingAnimated />
3433
</div>
3534
);
3635
};
37-
38-
export { SpinnerAnimation, HorizontallyCenteredSpinner };

frontends/web/src/locales/en/app.json

+2
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,9 @@
367367
"button": "Blink"
368368
},
369369
"bluetooth": {
370+
"connected": "connected",
370371
"connectionFailed": "failed",
372+
"enable": "Please turn on Bluetooth",
371373
"select": "Select your BitBox"
372374
},
373375
"bootloader": {

0 commit comments

Comments
 (0)