Skip to content

Commit ac863e2

Browse files
committed
Merge remote-tracking branch 'pull/release-v4.49.0'
2 parents b93902f + fd480ea commit ac863e2

File tree

10 files changed

+130
-54
lines changed

10 files changed

+130
-54
lines changed

CHANGELOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,21 +25,21 @@
2525
- Move "Export" (export transactions) to account info page
2626
- Show coinfinty logo when requesting an address
2727
- Update decimal formatting for stablecoin transactions
28+
- Change block explorer to mempool.space
29+
- Integrate Bitrefill and add spending section
2830

2931
## v4.48.8
3032
- Bundle BitBox02 Nova firmware version v9.23.3
3133

3234
## v4.48.7
3335
- ios: fix Pocket user verification button
34-
- Change block explorer to mempool.space
3536

3637
## v4.48.6
3738
- Android: restore support for Android 6 and Android 5
3839

3940
## v4.48.5
4041
- Bundle BitBox02 firmware version v9.23.2
4142
- iOS: fix wrong timezone when confirming time on BitBox02 (it would always show the time in UTC)
42-
- Integrate Bitrefill and add spending section
4343

4444
## v4.48.4
4545
- macOS: fix potential USB communication issue with BitBox02 bootloaders <v1.1.2 and firmwares <v9.23.1

frontends/ios/BitBoxApp/BitBoxApp.xcodeproj/project.pbxproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -657,7 +657,7 @@
657657
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
658658
CODE_SIGN_STYLE = Automatic;
659659
DEVELOPMENT_ASSET_PATHS = "\"BitBoxApp/Preview Content\"";
660-
DEVELOPMENT_TEAM = MXZQ99HRD6;
660+
DEVELOPMENT_TEAM = XK248TQN88;
661661
ENABLE_PREVIEWS = YES;
662662
GENERATE_INFOPLIST_FILE = YES;
663663
INFOPLIST_FILE = BitBoxApp/Info.plist;
@@ -689,7 +689,7 @@
689689
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
690690
CODE_SIGN_STYLE = Automatic;
691691
DEVELOPMENT_ASSET_PATHS = "\"BitBoxApp/Preview Content\"";
692-
DEVELOPMENT_TEAM = MXZQ99HRD6;
692+
DEVELOPMENT_TEAM = XK248TQN88;
693693
ENABLE_PREVIEWS = YES;
694694
GENERATE_INFOPLIST_FILE = YES;
695695
INFOPLIST_FILE = BitBoxApp/Info.plist;

frontends/web/src/components/bottom-navigation/bottom-navigation.module.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
.container {
2+
align-items: center;
23
background-color: var(--background-secondary);
34
bottom: 0;
45
border-top: 2px solid var(--bottom-navigation-border-color);

frontends/web/src/components/bottom-navigation/bottom-navigation.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export const BottomNavigation = ({ activeAccounts, devices }: Props) => {
3434
const { pathname } = useLocation();
3535
const deviceID = Object.keys(devices)[0];
3636
const isBitBox02 = deviceID && devices[deviceID] === 'bitbox02';
37-
const versionInfo = useLoad(isBitBox02 ? () => getVersion(deviceID) : null, [deviceID]);
37+
const versionInfo = useLoad(isBitBox02 ? () => getVersion(deviceID) : null, [deviceID, isBitBox02]);
3838
const canUpgrade = versionInfo ? versionInfo.canUpgrade : false;
3939

4040
const onlyHasOneAccount = activeAccounts.length === 1;

frontends/web/src/components/dialog/dialog.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ export const Dialog = ({
9393

9494
// ESC closes dialog (fires onClose)
9595
useEsc(() => {
96-
if (open) {
96+
if (open && onClose) {
9797
deactivate(true);
9898
}
9999
});
@@ -109,6 +109,8 @@ export const Dialog = ({
109109
if (
110110
mouseDownTarget.current === e.currentTarget
111111
&& e.target === e.currentTarget
112+
&& open
113+
&& onClose
112114
) {
113115
deactivate(true);
114116
}
@@ -117,8 +119,10 @@ export const Dialog = ({
117119

118120
// Close button handler
119121
const handleCloseClick = useCallback(() => {
120-
deactivate(true);
121-
}, [deactivate]);
122+
if (open && onClose) {
123+
deactivate(true);
124+
}
125+
}, [deactivate, onClose, open]);
122126

123127
// Back button handler (mobile)
124128
const closeHandler = useCallback(() => {

frontends/web/src/components/terms/pocket-terms.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,7 @@ export const PocketTerms = ({ onAgreedTerms }: TProps) => {
5959

6060
<h2 className={style.title}>{t('exchange.pocket.terms.kyc.title')}</h2>
6161
<ul>
62-
<li><p>{t('exchange.pocket.terms.kyc.p1')}</p></li>
63-
<li><p>{t('exchange.pocket.terms.kyc.p2')}</p></li>
62+
<li><p>{t('exchange.pocket.terms.kyc.info')}</p></li>
6463
</ul>
6564
<p>
6665
<A href="https://pocketbitcoin.com/faq">

frontends/web/src/hooks/keyboard.ts

Lines changed: 111 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { useEffect, useRef } from 'react';
17+
import { useCallback, useEffect, useRef } from 'react';
1818

1919
/**
2020
* gets fired on each keydown and executes the provided callback.
@@ -71,59 +71,132 @@ export const useFocusTrap = (
7171
ref: React.RefObject<HTMLElement>,
7272
active: boolean,
7373
) => {
74+
const autofocusDelay = 50;
75+
7476
const previouslyFocused = useRef<HTMLElement | null>(null);
77+
const trapEnabled = useRef(false);
78+
const autofocusTimer = useRef<number | null>(null);
79+
const cancelledAutofocus = useRef(false);
7580

76-
// Focus trap handler
77-
const handleKeyDown = (e: KeyboardEvent) => {
78-
if (!active || !ref.current || e.key !== 'Tab') {
79-
return;
80-
}
81-
const node = ref.current;
82-
const focusables = node.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR);
83-
if (!focusables.length) {
84-
return;
85-
}
81+
const handleKeyDown = useCallback(
82+
(e: KeyboardEvent) => {
83+
if (!trapEnabled.current || !ref.current || e.key !== 'Tab') {
84+
return;
85+
}
8686

87-
const first = focusables[0];
88-
const last = focusables[focusables.length - 1];
87+
const focusables = Array.from(
88+
ref.current.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR)
89+
).filter((el) => el.offsetParent !== null); // skip hidden
8990

90-
if (e.shiftKey && document.activeElement === first) {
91-
e.preventDefault();
92-
last?.focus();
93-
} else if (!e.shiftKey && document.activeElement === last) {
94-
e.preventDefault();
95-
first?.focus();
96-
}
97-
};
91+
if (focusables.length === 0) {
92+
return;
93+
}
9894

99-
useKeydown(handleKeyDown);
95+
const first = focusables[0];
96+
const last = focusables[focusables.length - 1];
97+
const current = document.activeElement as HTMLElement;
98+
99+
if (e.shiftKey && current && current === first) {
100+
e.preventDefault();
101+
last?.focus();
102+
} else if (!e.shiftKey && current && current === last) {
103+
e.preventDefault();
104+
first?.focus();
105+
}
106+
},
107+
[ref]
108+
);
100109

101-
// Manage mount/unmount lifecycle
102110
useEffect(() => {
111+
// cleanup from previous runs
112+
if (autofocusTimer.current) {
113+
window.clearTimeout(autofocusTimer.current);
114+
autofocusTimer.current = null;
115+
}
116+
cancelledAutofocus.current = false;
117+
103118
if (!active || !ref.current) {
119+
trapEnabled.current = false;
104120
return;
105121
}
106122

107-
// Save previously focused element
123+
const node = ref.current;
124+
trapEnabled.current = true;
108125
previouslyFocused.current = document.activeElement as HTMLElement;
109126

110-
// Autofocus first element, but only if nothing inside already has focus
111-
if (!ref.current.contains(document.activeElement)) {
112-
const firstFocusable = (
113-
ref.current.querySelector<HTMLElement>('[autofocus]:not(:disabled)')
114-
?? ref.current.querySelector<HTMLElement>(FOCUSABLE_SELECTOR)
115-
);
116-
firstFocusable?.focus({ preventScroll: true });
127+
// If focus is already inside, don't schedule autofocus.
128+
if (node.contains(document.activeElement)) {
129+
// no autofocus needed
130+
} else {
131+
// If any focus enters the dialog while we're waiting, cancel the autofocus.
132+
const onFocusIn = (e: FocusEvent) => {
133+
if (node.contains(e.target as Node)) {
134+
cancelledAutofocus.current = true;
135+
if (autofocusTimer.current) {
136+
window.clearTimeout(autofocusTimer.current);
137+
autofocusTimer.current = null;
138+
}
139+
}
140+
};
141+
142+
document.addEventListener('focusin', onFocusIn, true);
143+
144+
// Delay longer than the 20ms your Dialog uses, default 50ms.
145+
autofocusTimer.current = window.setTimeout(() => {
146+
autofocusTimer.current = null;
147+
if (cancelledAutofocus.current) {
148+
return;
149+
}
150+
151+
// final guard: if still nothing inside has focus, focus first focusable
152+
if (!node.contains(document.activeElement)) {
153+
const firstFocusable =
154+
node.querySelector<HTMLElement>('[autofocus]:not(:disabled)') ??
155+
node.querySelector<HTMLElement>(FOCUSABLE_SELECTOR);
156+
firstFocusable?.focus({ preventScroll: true });
157+
}
158+
}, autofocusDelay);
159+
160+
// remove focusin listener when effect cleanup runs
161+
// (we'll remove it in the effect return)
162+
return () => {
163+
document.removeEventListener('focusin', onFocusIn, true);
164+
};
117165
}
118166

167+
// If we didn't return above (i.e. no focusin listener set), continue to set up keydown below.
168+
// Set up keydown listener on document
169+
const onKeyDown = (e: KeyboardEvent) => handleKeyDown(e);
170+
document.addEventListener('keydown', onKeyDown);
171+
172+
return () => {
173+
// cleanup in-case we returned earlier (also safe)
174+
document.removeEventListener('keydown', onKeyDown);
175+
};
176+
}, [active, ref, autofocusDelay, handleKeyDown]);
177+
178+
// global keydown listener must be attached separately too so it's always active while trapEnabled
179+
useEffect(() => {
180+
const onKeyDown = (e: KeyboardEvent) => handleKeyDown(e);
181+
document.addEventListener('keydown', onKeyDown);
182+
return () => document.removeEventListener('keydown', onKeyDown);
183+
}, [handleKeyDown]);
184+
185+
// cleanup on deactivate: restore previous focus, clear timers/listeners
186+
useEffect(() => {
119187
return () => {
120-
// Restore focus if element is still in DOM
121-
if (
122-
previouslyFocused.current &&
123-
document.body.contains(previouslyFocused.current)
124-
) {
125-
previouslyFocused.current.focus();
188+
trapEnabled.current = false;
189+
190+
if (autofocusTimer.current) {
191+
window.clearTimeout(autofocusTimer.current);
192+
autofocusTimer.current = null;
193+
}
194+
195+
const prev = previouslyFocused.current;
196+
if (prev && document.body.contains(prev)) {
197+
// restore focus
198+
prev.focus();
126199
}
127200
};
128-
}, [ref, active]);
201+
}, []);
129202
};

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -842,9 +842,8 @@
842842
"title": "Payment methods and fees"
843843
},
844844
"kyc": {
845+
"info": "Pocket may require identification documents. Please refer to their FAQs for more information.",
845846
"link": "Read Pocket FAQs",
846-
"p1": "No additional documents are needed for exchanges up to 1000 EUR/CHF within a 30-day period.",
847-
"p2": "For larger amounts, a call with Pocket is required to complete the KYC/AML process.",
848847
"title": "KYC/AML (Know Your Customer / Anti-Money Laundering)"
849848
},
850849
"security": {

frontends/web/src/routes/market/components/infocontent.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,11 +120,11 @@ const PocketInfo = ({ bankTransferFee }: TPocketInfoProps) => {
120120
<br />
121121
<p><b>{t('buy.exchange.infoContent.pocket.verification.title')}</b></p>
122122
<br />
123-
<p>{t('buy.exchange.infoContent.pocket.verification.info')}</p>
123+
<p>{t('exchange.pocket.terms.kyc.info')}</p>
124124
<br />
125125
<p>
126-
<A href="https://pocketbitcoin.com/faq/are-there-any-limits-with-pocket">
127-
{t('buy.exchange.infoContent.pocket.verification.link')}
126+
<A href="https://pocketbitcoin.com/faq">
127+
{t('exchange.pocket.terms.kyc.link')}
128128
</A>
129129
</p>
130130
<br />

frontends/web/src/routes/settings/more.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export const More = ({ devices }: Props) => {
4242
useOnlyVisitableOnMobile('/settings/general');
4343
const deviceID = Object.keys(devices)[0];
4444
const isBitBox02 = deviceID && devices[deviceID] === 'bitbox02';
45-
const versionInfo = useLoad(isBitBox02 ? () => getVersion(deviceID) : null, [deviceID]);
45+
const versionInfo = useLoad(isBitBox02 ? () => getVersion(deviceID) : null, [deviceID, isBitBox02]);
4646
const canUpgrade = versionInfo ? versionInfo.canUpgrade : false;
4747

4848
return (

0 commit comments

Comments
 (0)