Compare commits
752 commits
Author | SHA1 | Date | |
---|---|---|---|
|
5f18540ca7 | ||
|
751c7d6f45 | ||
|
0b1c3dd9f7 | ||
|
ae89a59794 | ||
|
10b3432e0e | ||
|
c67eea8155 | ||
|
9421511f74 | ||
|
9ec0ef51e4 | ||
|
1cada11c50 | ||
|
d2cebde6ad | ||
|
1a940971bc | ||
|
28316b4d73 | ||
|
4670eea38a | ||
|
b2552bdc71 | ||
|
dbd4066f7e | ||
|
4cdd952f90 | ||
|
ddee4cdaaf | ||
|
0aa6b96e4b | ||
|
8d49aff279 | ||
|
18a187b120 | ||
|
1f77a852a8 | ||
|
9d899d672d | ||
|
e7b81e5517 | ||
|
f8af06e2ae | ||
|
8b81472fa4 | ||
|
4ad2b15070 | ||
|
dd118af993 | ||
|
d375bd9780 | ||
|
c967f6701a | ||
|
4753d984ca | ||
|
28e2e343b8 | ||
|
b81879e9bd | ||
|
040f91028a | ||
|
1c8aa08de8 | ||
|
d7743a740f | ||
|
a2ec407720 | ||
|
c90bf68a66 | ||
|
7ab73669dc | ||
|
72134fef84 | ||
|
c0eec1be8e | ||
|
77f9cf7e16 | ||
|
88be0332e4 | ||
|
a4f8e42d8c | ||
|
e519360d89 | ||
|
62a5efc82c | ||
|
ec2bc5e627 | ||
|
0bdfc6fa85 | ||
|
ef5887f28b | ||
|
3f82cb4449 | ||
|
9574554780 | ||
|
4b249edaa4 | ||
|
e3fcbbb713 | ||
|
63a3c61534 | ||
|
a6d66574cd | ||
|
2376ef8be9 | ||
|
155f021692 | ||
|
3a26d6dab2 | ||
|
1da481542a | ||
|
472307c271 | ||
|
09394ff4f9 | ||
|
16936fca27 | ||
|
54166c0592 | ||
|
bb6d443670 | ||
|
e4e16a8f40 | ||
|
35deca58e0 | ||
|
898443f3a5 | ||
|
15fc708a0a | ||
|
15c618b59a | ||
|
ccdb492ba0 | ||
|
0449965ef5 | ||
|
758c2acf3a | ||
|
b75aa7b269 | ||
|
aa695f2705 | ||
|
863ac46bc8 | ||
|
1946fa0dde | ||
|
a62a21b28b | ||
|
c1ae300254 | ||
|
5e4d58b207 | ||
|
3504d0dc30 | ||
|
fe795e648b | ||
|
af8d7d3477 | ||
|
c604ac4197 | ||
|
4c0fd89530 | ||
|
d14b4265f8 | ||
|
307e950d15 | ||
|
10f145d012 | ||
|
88b8274758 | ||
|
a65776933d | ||
|
05491387ff | ||
|
e23f233f25 | ||
|
7eb420c561 | ||
|
96e553f3d5 | ||
|
2f3cf1b4e9 | ||
|
6a4392de02 | ||
|
62bb33a9ff | ||
|
85cd7b4aed | ||
|
9507a48314 | ||
|
07b93d521d | ||
|
7c6bf01372 | ||
|
b01aa58e3b | ||
|
136dd20f9e | ||
|
0bfeda0d75 | ||
|
1a848328e3 | ||
|
93e6269611 | ||
|
5f8dbc52d1 | ||
|
e4d3ecba98 | ||
|
bbe4449dd9 | ||
|
bf9087eae6 | ||
|
b3ff1b7c3f | ||
|
00dcc25142 | ||
|
4614c51041 | ||
|
8d694ceb7b | ||
|
49f6068b21 | ||
|
be8437e107 | ||
|
cc71dfce8c | ||
|
2e1f20c080 | ||
|
11dceb19fa | ||
|
7bb3dd6aef | ||
|
7e7492d314 | ||
|
c4b1e67f9d | ||
|
79f624e906 | ||
|
fabfc5c156 | ||
|
a3d234bee1 | ||
|
850ac2c653 | ||
|
1c26cb420e | ||
|
0f23c4d0a7 | ||
|
9adef2b3c1 | ||
|
a9b003e762 | ||
|
37b03b12e7 | ||
|
4e7c5a28ae | ||
|
9766d2387a | ||
|
3939ef32f9 | ||
|
17a5a78fd8 | ||
|
f7d673d93b | ||
|
dc3e88c005 | ||
|
85cb6c1287 | ||
|
8488dfb9e7 | ||
|
3dde81f3a8 | ||
|
a0dc0a31e7 | ||
|
2dc26ac26a | ||
|
b881370f83 | ||
|
4f31aff503 | ||
|
2ee13dcf4f | ||
|
bf65d2d302 | ||
|
3599ef50ad | ||
|
80fb4a74a8 | ||
|
4f326452ab | ||
|
37a88fd60d | ||
|
3fcf3c0840 | ||
|
685332ce22 | ||
|
0892332d23 | ||
|
11cb7dbed5 | ||
|
7205f70c30 | ||
|
aa9e647c28 | ||
|
8439ff9893 | ||
|
f83476428e | ||
|
4be2668c81 | ||
|
867581003c | ||
|
3f1ea9432b | ||
|
2bcbe9903e | ||
|
7963083237 | ||
|
b7e03c1ed1 | ||
|
b74fb5f389 | ||
|
ddd141fc11 | ||
|
d9a70f5879 | ||
|
e645c911d7 | ||
|
06fbb8c945 | ||
|
c008a644cc | ||
|
ab7e7cf1d5 | ||
|
d38968086e | ||
|
787fcb797d | ||
|
599b5d3b60 | ||
|
c3f7d5b184 | ||
|
3423730a41 | ||
|
2bb7b0c53f | ||
|
b0a7053fc0 | ||
|
a8c0da4768 | ||
|
b357053e65 | ||
|
5b20ac352e | ||
|
8c2640e5a9 | ||
|
a6575b7b73 | ||
|
db58bcf70d | ||
|
b8d1d686f0 | ||
|
5d088a67c1 | ||
|
fa43d03a36 | ||
|
5a7fb86742 | ||
|
4ff759b538 | ||
|
0b33af59f0 | ||
|
f9d8594509 | ||
|
7f97c340f8 | ||
|
925dc17042 | ||
|
164f16657a | ||
|
34db010bde | ||
|
7ccf19212f | ||
|
133312e065 | ||
|
46a78e8dfb | ||
|
e5ab5f6565 | ||
|
ad71dccd72 | ||
|
159a8b2e16 | ||
|
d3fd8c050f | ||
|
52b3cb9b34 | ||
|
98b643a023 | ||
|
0eb3393f1f | ||
|
47a08448a2 | ||
|
254550d92e | ||
|
9ed7ceabdb | ||
|
6afc6624eb | ||
|
2317d0a4cf | ||
|
dcd2023815 | ||
|
4b37eaba98 | ||
|
8619e80dc0 | ||
|
e06d1ce57c | ||
|
157bd3529a | ||
|
e92eb7eae0 | ||
|
b9227cdbc6 | ||
|
a047c0219e | ||
|
2ca8eca810 | ||
|
93f901e94f | ||
|
670ad6a833 | ||
|
b232a13243 | ||
|
d610063809 | ||
|
1b328cd130 | ||
|
0b5e640630 | ||
|
527219f697 | ||
|
c400771d7a | ||
|
021ed454f1 | ||
|
47673a4ae0 | ||
|
7e66e42862 | ||
|
e25833f0d3 | ||
|
0e698069f4 | ||
|
3a8a7d6da8 | ||
|
1b561c8a91 | ||
|
4b93827b7f | ||
|
3de6976997 | ||
|
9c1be484c1 | ||
|
f04b50c58b | ||
|
f974658472 | ||
|
208157430f | ||
|
b73f04b4e6 | ||
|
0429721d66 | ||
|
5fb3991cb2 | ||
|
e4093a357d | ||
|
fda596211a | ||
|
fb1a30191d | ||
|
2f3ac6e972 | ||
|
8719ded414 | ||
|
680d9d4495 | ||
|
45f095badf | ||
|
66bb0b0e1c | ||
|
33acf30d68 | ||
|
2c4bb95475 | ||
|
9435fb769f | ||
|
6321627578 | ||
|
18cb2faef6 | ||
|
2c68583495 | ||
|
f8629e2555 | ||
|
6c11e2a5b8 | ||
|
827c2ad3db | ||
|
4be2bb03be | ||
|
12d8596180 | ||
|
2a4b14d63e | ||
|
062b8844d4 | ||
|
e4cea4f451 | ||
|
82f13fbded | ||
|
a1d5941a75 | ||
|
4d9a2f79f9 | ||
|
fef62f2fd8 | ||
|
c1adabb021 | ||
|
b42290ceee | ||
|
0aa2ed20f3 | ||
|
c092ea4523 | ||
|
d338f813cb | ||
|
238ee798ab | ||
|
bbf746b011 | ||
|
fdd2b66d8e | ||
|
44fc028159 | ||
|
632500b734 | ||
|
a4a513f703 | ||
|
39b141507c | ||
|
dde4520094 | ||
|
a7843e127f | ||
|
f4125cb1e9 | ||
|
115b0a2a4f | ||
|
7035bec229 | ||
|
80ef3252a1 | ||
|
6cd6079493 | ||
|
9a8158a384 | ||
|
9976734665 | ||
|
1c15ae0a0c | ||
|
64d8238872 | ||
|
336785e1a0 | ||
|
d68b806b60 | ||
|
1acf4c9af2 | ||
|
2d51238d6f | ||
|
3b90c49d79 | ||
|
226d499603 | ||
|
f155b6b577 | ||
|
4be7f78be8 | ||
|
5d81a4cf57 | ||
|
1b11200a0a | ||
|
f229beb5e0 | ||
|
e176783a3f | ||
|
5c7460d6b1 | ||
|
571b056854 | ||
|
54db4b366e | ||
|
02dd22b8d4 | ||
|
8b94a9db10 | ||
|
175a5f27aa | ||
|
907e54938c | ||
|
c43d36d84d | ||
|
f900f46deb | ||
|
472b6c97ff | ||
|
ae80cb9118 | ||
|
f389af09ba | ||
|
fc5eccfcd4 | ||
|
307306f5ec | ||
|
73081033ed | ||
|
8b531350af | ||
|
4c09a52e02 | ||
|
879f4f4081 | ||
|
ea870729d6 | ||
|
b8a8986a8e | ||
|
caf41400a0 | ||
|
30ed2a3d96 | ||
|
a8374ab25b | ||
|
d3fd15dcf8 | ||
|
e8c181359d | ||
|
307f6881c9 | ||
|
e38e70bb0b | ||
|
92fda5d969 | ||
|
e69c105ccf | ||
|
002efdc4e7 | ||
|
63ab4da34b | ||
|
c2ff24591e | ||
|
19dd1047d1 | ||
|
38d92a7a5c | ||
|
59f2835cb8 | ||
|
9863dfd47b | ||
|
70e32c9d69 | ||
|
2b393ba997 | ||
|
88a1ec4260 | ||
|
5eabded72b | ||
|
d957ee7197 | ||
|
a4df48a0c5 | ||
|
6d7e48eb1a | ||
|
0481c8d6a9 | ||
|
85e47ac83d | ||
|
0d64347813 | ||
|
4849042dc6 | ||
|
d2e186bbf7 | ||
|
588da24f0b | ||
|
a8858833ef | ||
|
06ec5feb4b | ||
|
e9c3e3143f | ||
|
83f545ed4b | ||
|
7b8b3a0be2 | ||
|
6dde0c4b4e | ||
|
e810baf9c8 | ||
|
facd7b7783 | ||
|
89e7b23c05 | ||
|
56c983e1dc | ||
|
016e9f4214 | ||
|
c932d0da5a | ||
|
cdd923db7c | ||
|
4611c46d1e | ||
|
59c9edeebd | ||
|
05eff5f2b4 | ||
|
8894bcf965 | ||
|
a6306c53d8 | ||
|
d05d51237a | ||
|
5c70faf17d | ||
|
18c5e38d6c | ||
|
ca1be7d443 | ||
|
ec027a12df | ||
|
6b013e5bb7 | ||
|
950848181e | ||
|
559468b221 | ||
|
3adb90abff | ||
|
6765dd7246 | ||
|
6698645f48 | ||
|
cdd76db18f | ||
|
715991b106 | ||
|
7882716c73 | ||
|
9f912c51ed | ||
|
8ae9ac6155 | ||
|
ea4acc2556 | ||
|
26c9449f2d | ||
|
b21cf6e0ec | ||
|
ce18286d45 | ||
|
d46d16140a | ||
|
5aef6382b9 | ||
|
16d418a6c1 | ||
|
4fc57adaac | ||
|
8c504a1bd1 | ||
|
e197fd70d4 | ||
|
342a127f99 | ||
|
a41032cfda | ||
|
0675e6ea62 | ||
|
19ba071af3 | ||
|
34b7525cba | ||
|
86c0d9d53d | ||
|
d8d97b2b39 | ||
|
2503cb7882 | ||
|
ca912377bc | ||
|
cea31518dc | ||
|
d09e8ff68c | ||
|
346581b3e2 | ||
|
10f7e44232 | ||
|
faf86028ab | ||
|
76c4023592 | ||
|
bd42acb7c7 | ||
|
3d9e9ddf88 | ||
|
0c88e0e9db | ||
|
0371e3352f | ||
|
4607d4a796 | ||
|
4a44989a8f | ||
|
0f6582e050 | ||
|
279afa517f | ||
|
e1202c6854 | ||
|
021a1fd352 | ||
|
d51010dd85 | ||
|
8f26859f76 | ||
|
49c5e67f45 | ||
|
a4a6fa5ef4 | ||
|
96b1331a60 | ||
|
791cbd5f94 | ||
|
073280225d | ||
|
077f3a3a04 | ||
|
1b73ab9b06 | ||
|
9acaac9646 | ||
|
19bddcb152 | ||
|
ddf00d5d44 | ||
|
721f0d3ecb | ||
|
6561bb0524 | ||
|
8b32825e73 | ||
|
2cdd01f2c2 | ||
|
fc7eb4ece2 | ||
|
486bc43202 | ||
|
fe37bcb9fd | ||
|
75a26d155c | ||
|
38ab7665bc | ||
|
c3ae3c8104 | ||
|
ae41b9bd0c | ||
|
f6a6d7c41e | ||
|
85ee40b39a | ||
|
2810e2e0a6 | ||
|
fcc3bc81ed | ||
|
cbd05c4408 | ||
|
7eb1828150 | ||
|
75a9bf6f37 | ||
|
4b890b2000 | ||
|
1a4b3d82fc | ||
|
c7c4988cd8 | ||
|
8f34249be5 | ||
|
bc82dc7905 | ||
|
5f82d1dc6d | ||
|
754a279a76 | ||
|
cf16417e00 | ||
|
7b3eb806bb | ||
|
c43103cb71 | ||
|
115ac98172 | ||
|
40c5cc7295 | ||
|
a5fb1bf6f5 | ||
|
786a06b2ee | ||
|
27a45ea857 | ||
|
f86afd4092 | ||
|
be5a61e991 | ||
|
f315b03b0e | ||
|
fb1c221635 | ||
|
a24285e06e | ||
|
6b3a181714 | ||
|
f506140c92 | ||
|
9799aaecc6 | ||
|
8d66e515b7 | ||
|
74a6033e65 | ||
|
b6806ac412 | ||
|
5b367c5ffb | ||
|
2697a45ff2 | ||
|
5c7b8ad3cc | ||
|
c5cfa9d467 | ||
|
df0e82483e | ||
|
8f54885991 | ||
|
238a5c2d09 | ||
|
999123e497 | ||
|
ea421715d6 | ||
|
845ac41928 | ||
|
38e26ccbd6 | ||
|
a8c4fdd20c | ||
|
6d78b5a141 | ||
|
ef21fcfde8 | ||
|
6efe21f6b0 | ||
|
7e5f5f50c6 | ||
|
aff43a4f1e | ||
|
0de78149ab | ||
|
6cdd627272 | ||
|
647ddcd28b | ||
|
1d3c62a5ab | ||
|
906ca1a55d | ||
|
e740172d57 | ||
|
018796ac3f | ||
|
5bb5430232 | ||
|
190b0f2435 | ||
|
d6586cbfb7 | ||
|
66fe1dc7ae | ||
|
a2db75539c | ||
|
756f37eeb4 | ||
|
e514af1aa6 | ||
|
ca5eff1730 | ||
|
10bf267c36 | ||
|
7643ce6821 | ||
|
f9dce1b120 | ||
|
0b9df766df | ||
|
b1aa371631 | ||
|
7e4447f5cf | ||
|
1c8e9e88cf | ||
|
4be89fbc9b | ||
|
49ab9e635c | ||
|
5cba7cc2c7 | ||
|
6193e9bac0 | ||
|
0a0dd366bd | ||
|
bba96e5308 | ||
|
16a8e7ae61 | ||
|
70c69eb7ca | ||
|
32e2fc6ca3 | ||
|
efc768f642 | ||
|
fc27e52bb8 | ||
|
9a0d76cc9c | ||
|
c59f4c1daa | ||
|
3b149c7cf0 | ||
|
52441c1d63 | ||
|
adbe625905 | ||
|
26a735ed62 | ||
|
405dfee7dc | ||
|
94cc243d20 | ||
|
fdfb55d3e2 | ||
|
421b30a130 | ||
|
5045b8566f | ||
|
a92837dc35 | ||
|
bd9340c756 | ||
|
d247f3eaff | ||
|
65c8798dba | ||
|
4e050b33cf | ||
|
5ec064eb6f | ||
|
4169c8e499 | ||
|
3532840b5a | ||
|
02340a3e9f | ||
|
da7885febb | ||
|
f1bc844977 | ||
|
2546f9015a | ||
|
342f461bdf | ||
|
3932f4f90d | ||
|
57b74f10e2 | ||
|
1df15e3a00 | ||
|
983034c788 | ||
|
4e480b20c4 | ||
|
010b99a11b | ||
|
555ba1d9ea | ||
|
30ab6d7883 | ||
|
b500d6ab58 | ||
|
4633d4b4ea | ||
|
d0fdb6b28d | ||
|
56f3ac22c2 | ||
|
ca24a1eaa8 | ||
|
79088297b7 | ||
|
3a6303ebd4 | ||
|
f5854f48ee | ||
|
e30c0b17ea | ||
|
1154a6a523 | ||
|
8ba1f3d1d4 | ||
|
bbf3324b57 | ||
|
2dc3efd391 | ||
|
0ee3da9dc0 | ||
|
5e1b8b1c4e | ||
|
40d8b86859 | ||
|
0bc7617148 | ||
|
584f39f0aa | ||
|
714702aac0 | ||
|
84227e1457 | ||
|
0b096f77d6 | ||
|
8083899b06 | ||
|
008b7d98a8 | ||
|
13254b0045 | ||
|
fcbc563916 | ||
|
73637a9ff6 | ||
|
2ccd73e2f1 | ||
|
ea44f87490 | ||
|
9133bfbedb | ||
|
6b326dc70d | ||
|
4ca37246ab | ||
|
2a5003e9e7 | ||
|
5265ae8bd0 | ||
|
da5078290d | ||
|
1c01d133e0 | ||
|
1db664545b | ||
|
534fe22d2d | ||
|
6749f501e9 | ||
|
e1e17dddde | ||
|
c0490804bb | ||
|
a2b8409710 | ||
|
81140623c3 | ||
|
58591566d3 | ||
|
8da871a071 | ||
|
a1deef2474 | ||
|
eb9f210b5b | ||
|
8e28fc2395 | ||
|
eb298f8669 | ||
|
0415b8f9aa | ||
|
d9840b3202 | ||
|
4f4131c7cd | ||
|
43a856e8e1 | ||
|
8d07f33051 | ||
|
7d60b6fc8c | ||
|
9555e5927e | ||
|
8ddfec67b8 | ||
|
99ba338735 | ||
|
fd15e83fe8 | ||
|
8f46905895 | ||
|
40a05a7a3a | ||
|
5b7254b626 | ||
|
e407c0dcff | ||
|
3e06e029f2 | ||
|
e68b6936e6 | ||
|
8b45f11441 | ||
|
b971433245 | ||
|
13bb11b062 | ||
|
e88ce6f505 | ||
|
f430ed5087 | ||
|
297e8d8e42 | ||
|
28f893f91f | ||
|
93216d0145 | ||
|
8ea5583458 | ||
|
01df140eef | ||
|
61097732cb | ||
|
92c8bdc202 | ||
|
22acb13463 | ||
|
cbfc16e29b | ||
|
17f95d5634 | ||
|
03d695eb91 | ||
|
f13bb4dc53 | ||
|
636fc21f9c | ||
|
23f6905191 | ||
|
d50de5975a | ||
|
68262504b5 | ||
|
28d9fa1d53 | ||
|
841f49ceb9 | ||
|
156010dbbd | ||
|
e93259e39e | ||
|
c5c2ce8a61 | ||
|
86662cad0d | ||
|
cbda4cafab | ||
|
7cb22e9f50 | ||
|
c4bca5e1c5 | ||
|
32a5627132 | ||
|
6ac5356683 | ||
|
9bdfcac63f | ||
|
42fce50ffe | ||
|
0964c1843a | ||
|
8a0c7a18ce | ||
|
3a92b661b3 | ||
|
6a8b794963 | ||
|
7fd8097e42 | ||
|
4094207244 | ||
|
88b7994700 | ||
|
57ce1f87a1 | ||
|
20365b6a16 | ||
|
8634df94f3 | ||
|
06e401e6da | ||
|
abddacac72 | ||
|
93883b2d9b | ||
|
f3c5fb4eed | ||
|
b1a71b5d0c | ||
|
f67d852f28 | ||
|
a76c847a10 | ||
|
d3aed6fe72 | ||
|
a14499a6ba | ||
|
26504d90e4 | ||
|
06f8ba9248 | ||
|
cc8bedadb3 | ||
|
00e94f1a86 | ||
|
6550e955f6 | ||
|
7862e202e7 | ||
|
6b4f2d6adc | ||
|
e97f711e32 | ||
|
06e0c7b4fe | ||
|
8046955af8 | ||
|
0aaea9ce86 | ||
|
1df7e8f580 | ||
|
89b0883837 | ||
|
2b8846c2f7 | ||
|
2a9f8858c3 | ||
|
ba5e566b90 | ||
|
79f05ec12a | ||
|
268a2fe6a2 | ||
|
1b9812a5b6 | ||
|
fb85f3e252 | ||
|
e56ab8b498 | ||
|
750308725e | ||
|
1a5d670b72 | ||
|
9ce4188a72 | ||
|
fed5da66b9 | ||
|
b08fcf390e | ||
|
541d6aa206 | ||
|
5e2e0b58c2 | ||
|
8de33f9b86 | ||
|
b66710b5b9 | ||
|
cefe725e14 | ||
|
0549b86330 | ||
|
fa4225619f | ||
|
5be8d6733d | ||
|
17f0b80aae | ||
|
62c5eda2b2 | ||
|
2ee82f8dc9 | ||
|
ed8b8e32ae | ||
|
2c552b7963 | ||
|
cccd4bc688 | ||
|
7e4cda4b26 | ||
|
d954b542db | ||
|
66aa5ecc78 | ||
|
4a00a45d32 | ||
|
587d1d09f4 | ||
|
821403f970 | ||
|
ec5ae381e1 | ||
|
d44e76fdde | ||
|
ab92d66b28 | ||
|
fd752ffea7 | ||
|
83708b9ba8 | ||
|
f37e2e213d | ||
|
5844065e37 | ||
|
44f6d3bfe0 | ||
|
3f755b4bcf | ||
|
e26f1a4476 | ||
|
df8c9793d8 | ||
|
52b0d69f32 | ||
|
9ba2852057 | ||
|
06abd83f5b | ||
|
a18ba6084c | ||
|
f89200b52d | ||
|
0209c36228 | ||
|
a605d5f3a5 | ||
|
cb1d827b83 | ||
|
32e0ecf3c9 | ||
|
5500856abf | ||
|
9c01d06212 | ||
|
70731e8076 | ||
|
f1a7fc8c40 | ||
|
7a218a4fa6 | ||
|
5f2378a9de | ||
|
9583fac4c6 | ||
|
fac654c263 | ||
|
17eba2d925 | ||
|
2f4b688bfd | ||
|
0c43cc7b24 |
|
@ -47,6 +47,24 @@
|
|||
"device": "emulator",
|
||||
"app": "android.debug"
|
||||
},
|
||||
"android.debug.device": {
|
||||
"device": {
|
||||
"device": {
|
||||
"adbName": ".*"
|
||||
},
|
||||
"type": "android.attached"
|
||||
},
|
||||
"app": "android.debug"
|
||||
},
|
||||
"android.release.device": {
|
||||
"device": {
|
||||
"device": {
|
||||
"adbName": ".*"
|
||||
},
|
||||
"type": "android.attached"
|
||||
},
|
||||
"app": "android.release"
|
||||
},
|
||||
"android.release": {
|
||||
"device": "emulator",
|
||||
"app": "android.release"
|
||||
|
|
124
.github/workflows/build-ios-release-pullrequest.yml
vendored
|
@ -22,12 +22,40 @@ jobs:
|
|||
branch_name: ${{ steps.get_latest_commit_details.outputs.branch_name }}
|
||||
env:
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
MATCH_READONLY: "true"
|
||||
|
||||
steps:
|
||||
- name: Checkout Project
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Ensures the full Git history is available
|
||||
|
||||
- name: Setup Caching
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/Library/Caches/CocoaPods
|
||||
ios/Pods
|
||||
~/.npm
|
||||
node_modules
|
||||
vendor/bundle
|
||||
key: ${{ runner.os }}-ios-${{ hashFiles('**/package-lock.json', '**/Podfile.lock', '**/Gemfile.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-ios-
|
||||
|
||||
- name: Clear All Caches
|
||||
if: github.ref == 'refs/heads/master'
|
||||
run: |
|
||||
echo "Clearing Xcode DerivedData..."
|
||||
rm -rf ~/Library/Developer/Xcode/DerivedData
|
||||
echo "Clearing CocoaPods Cache..."
|
||||
rm -rf ~/Library/Caches/CocoaPods
|
||||
echo "Clearing npm Cache..."
|
||||
npm cache clean --force
|
||||
echo "Clearing Ruby Gems Cache..."
|
||||
rm -rf ~/.gem
|
||||
echo "Clearing Bundler Cache..."
|
||||
rm -rf ~/.bundle/cache
|
||||
|
||||
- name: Ensure Correct Branch
|
||||
if: github.ref != 'refs/heads/master'
|
||||
|
@ -67,15 +95,32 @@ jobs:
|
|||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'npm'
|
||||
|
||||
- uses: maxim-lobanov/setup-xcode@v1
|
||||
with:
|
||||
xcode-version: 16.0
|
||||
xcode-version: latest
|
||||
|
||||
- name: Install iOS Simulator Runtime
|
||||
run: |
|
||||
echo "Available iOS simulator runtimes:"
|
||||
xcrun simctl list runtimes
|
||||
|
||||
# Try to download the latest iOS 16.x simulator if not present
|
||||
if (! xcrun simctl list runtimes | grep -q "iOS 16"); then
|
||||
echo "Installing iOS 16.4 simulator..."
|
||||
sudo xcode-select -s /Applications/Xcode.app
|
||||
xcodebuild -downloadPlatform iOS
|
||||
fi
|
||||
|
||||
echo "Available iOS simulator runtimes after install:"
|
||||
xcrun simctl list runtimes
|
||||
|
||||
- name: Set Up Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 3.1.6
|
||||
bundler-cache: true
|
||||
|
||||
- name: Install Dependencies with Bundler
|
||||
run: |
|
||||
|
@ -88,6 +133,7 @@ jobs:
|
|||
- name: Install CocoaPods Dependencies
|
||||
run: |
|
||||
bundle exec fastlane ios install_pods
|
||||
echo "CocoaPods dependencies installed successfully"
|
||||
|
||||
- name: Generate Build Number Based on Timestamp
|
||||
id: generate_build_number
|
||||
|
@ -133,8 +179,26 @@ jobs:
|
|||
- name: Build App
|
||||
id: build_app
|
||||
run: |
|
||||
bundle exec fastlane ios build_app_lane --verbose
|
||||
echo "ipa_output_path=$IPA_OUTPUT_PATH" >> $GITHUB_OUTPUT # Set the IPA output path for future jobs
|
||||
bundle exec fastlane ios build_app_lane
|
||||
|
||||
# Ensure IPA path is set for subsequent steps
|
||||
if [ -f "./ios/build/ipa_path.txt" ]; then
|
||||
IPA_PATH=$(cat ./ios/build/ipa_path.txt)
|
||||
echo "IPA_OUTPUT_PATH=$IPA_PATH" >> $GITHUB_ENV
|
||||
echo "ipa_output_path=$IPA_PATH" >> $GITHUB_OUTPUT
|
||||
echo "Found IPA at: $IPA_PATH"
|
||||
else
|
||||
echo "Warning: ipa_path.txt not found, trying to locate IPA file manually..."
|
||||
IPA_PATH=$(find ./ios -name "*.ipa" | head -n 1)
|
||||
if [ -n "$IPA_PATH" ]; then
|
||||
echo "IPA_OUTPUT_PATH=$IPA_PATH" >> $GITHUB_ENV
|
||||
echo "ipa_output_path=$IPA_PATH" >> $GITHUB_OUTPUT
|
||||
echo "Found IPA at: $IPA_PATH"
|
||||
else
|
||||
echo "Error: No IPA file found"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
- name: Upload Bugsnag Sourcemaps
|
||||
if: success()
|
||||
|
@ -142,8 +206,8 @@ jobs:
|
|||
env:
|
||||
BUGSNAG_API_KEY: ${{ secrets.BUGSNAG_API_KEY }}
|
||||
BUGSNAG_RELEASE_STAGE: production
|
||||
PROJECT_VERSION: ${{ needs.build.outputs.project_version }}
|
||||
NEW_BUILD_NUMBER: ${{ needs.build.outputs.new_build_number }}
|
||||
PROJECT_VERSION: ${{ env.PROJECT_VERSION }}
|
||||
NEW_BUILD_NUMBER: ${{ env.NEW_BUILD_NUMBER }}
|
||||
|
||||
- name: Upload Build Logs
|
||||
if: always()
|
||||
|
@ -151,13 +215,32 @@ jobs:
|
|||
with:
|
||||
name: build_logs
|
||||
path: ./ios/build_logs/
|
||||
retention-days: 7
|
||||
|
||||
- name: Verify IPA File Before Upload
|
||||
run: |
|
||||
echo "Checking IPA file at: $IPA_OUTPUT_PATH"
|
||||
if [ -f "$IPA_OUTPUT_PATH" ]; then
|
||||
echo "âś… IPA file exists"
|
||||
ls -la "$IPA_OUTPUT_PATH"
|
||||
else
|
||||
echo "❌ IPA file not found at: $IPA_OUTPUT_PATH"
|
||||
echo "Current directory contents:"
|
||||
find ./ios -name "*.ipa"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Upload IPA as Artifact
|
||||
if: success()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: BlueWallet_${{env.PROJECT_VERSION}}_${{env.NEW_BUILD_NUMBER}}.ipa
|
||||
path: ${{ env.IPA_OUTPUT_PATH }} # Directly from Fastfile `IPA_OUTPUT_PATH`
|
||||
name: BlueWallet_IPA
|
||||
path: ${{ env.IPA_OUTPUT_PATH }}
|
||||
retention-days: 7
|
||||
|
||||
- name: Delete Temporary Keychain
|
||||
if: always()
|
||||
run: bundle exec fastlane ios delete_temp_keychain
|
||||
|
||||
testflight-upload:
|
||||
needs: build
|
||||
|
@ -177,6 +260,7 @@ jobs:
|
|||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: 3.1.6
|
||||
bundler-cache: true
|
||||
|
||||
- name: Install Dependencies with Bundler
|
||||
run: |
|
||||
|
@ -186,18 +270,11 @@ jobs:
|
|||
- name: Download IPA from Artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: BlueWallet_${{ needs.build.outputs.project_version }}_${{ needs.build.outputs.new_build_number }}.ipa
|
||||
name: BlueWallet_IPA
|
||||
path: ./
|
||||
|
||||
- name: Create App Store Connect API Key JSON
|
||||
run: echo '${{ secrets.APP_STORE_CONNECT_API_KEY_CONTENT }}' > ./appstore_api_key.json
|
||||
|
||||
- name: Verify IPA File Download
|
||||
run: |
|
||||
echo "Current directory:"
|
||||
pwd
|
||||
echo "Files in current directory:"
|
||||
ls -la ./
|
||||
|
||||
- name: Set IPA Path Environment Variable
|
||||
run: echo "IPA_OUTPUT_PATH=$(pwd)/BlueWallet_${{ needs.build.outputs.project_version }}_${{ needs.build.outputs.new_build_number }}.ipa" >> $GITHUB_ENV
|
||||
|
@ -205,19 +282,23 @@ jobs:
|
|||
- name: Verify IPA Path Before Upload
|
||||
run: |
|
||||
if [ ! -f "$IPA_OUTPUT_PATH" ]; then
|
||||
echo "IPA file not found at path: $IPA_OUTPUT_PATH"
|
||||
echo "❌ IPA file not found at path: $IPA_OUTPUT_PATH"
|
||||
ls -la $(pwd)
|
||||
exit 1
|
||||
else
|
||||
echo "âś… Found IPA at: $IPA_OUTPUT_PATH"
|
||||
fi
|
||||
|
||||
- name: Print Environment Variables for Debugging
|
||||
run: |
|
||||
echo "LATEST_COMMIT_MESSAGE: $LATEST_COMMIT_MESSAGE"
|
||||
echo "BRANCH_NAME: $BRANCH_NAME"
|
||||
echo "PROJECT_VERSION: $PROJECT_VERSION"
|
||||
echo "NEW_BUILD_NUMBER: $NEW_BUILD_NUMBER"
|
||||
echo "IPA_OUTPUT_PATH: $IPA_OUTPUT_PATH"
|
||||
|
||||
- name: Upload to TestFlight
|
||||
run: |
|
||||
ls -la $IPA_OUTPUT_PATH
|
||||
bundle exec fastlane ios upload_to_testflight_lane
|
||||
run: bundle exec fastlane ios upload_to_testflight_lane
|
||||
env:
|
||||
APP_STORE_CONNECT_API_KEY_PATH: $(pwd)/appstore_api_key.p8
|
||||
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
|
||||
|
@ -228,18 +309,19 @@ jobs:
|
|||
APP_STORE_CONNECT_API_KEY_KEY_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_KEY_ID }}
|
||||
APP_STORE_CONNECT_API_KEY_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_API_KEY_ISSUER_ID }}
|
||||
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
|
||||
IPA_OUTPUT_PATH: ${{ env.IPA_OUTPUT_PATH }}
|
||||
|
||||
- name: Post PR Comment
|
||||
if: success() && github.event_name == 'pull_request'
|
||||
uses: actions/github-script@v6
|
||||
env:
|
||||
BUILD_NUMBER: ${{ needs.build.outputs.new_build_number }}
|
||||
PROJECT_VERSION: ${{ needs.build.outputs.project_version }}
|
||||
LATEST_COMMIT_MESSAGE: ${{ needs.build.outputs.latest_commit_message }}
|
||||
with:
|
||||
script: |
|
||||
const buildNumber = process.env.BUILD_NUMBER;
|
||||
const message = `The build ${buildNumber} has been uploaded to TestFlight.`;
|
||||
const version = process.env.PROJECT_VERSION;
|
||||
const message = `âś… Build ${version} (${buildNumber}) has been uploaded to TestFlight and will be available for testing soon.`;
|
||||
const prNumber = context.payload.pull_request.number;
|
||||
const repo = context.repo;
|
||||
github.rest.issues.createComment({
|
||||
|
|
5
.github/workflows/build-release-apk.yml
vendored
|
@ -4,7 +4,7 @@ on:
|
|||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
types: [opened, synchronize, reopened]
|
||||
types: [opened, synchronize, reopened, labeled, unlabeled]
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
@ -97,11 +97,12 @@ jobs:
|
|||
with:
|
||||
name: signed-apk
|
||||
path: ${{ env.APK_PATH }}
|
||||
if-no-files-found: error
|
||||
|
||||
browserstack:
|
||||
runs-on: ubuntu-latest
|
||||
needs: buildReleaseApk
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
if: ${{ github.event_name == 'pull_request' && contains(github.event.pull_request.labels.*.name, 'browserstack') }}
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
|
|
17
App.tsx
|
@ -1,5 +1,3 @@
|
|||
import 'react-native-gesture-handler'; // should be on top
|
||||
|
||||
import { NavigationContainer } from '@react-navigation/native';
|
||||
import React from 'react';
|
||||
import { useColorScheme } from 'react-native';
|
||||
|
@ -9,23 +7,26 @@ import { SettingsProvider } from './components/Context/SettingsProvider';
|
|||
import { BlueDarkTheme, BlueDefaultTheme } from './components/themes';
|
||||
import MasterView from './navigation/MasterView';
|
||||
import { navigationRef } from './NavigationService';
|
||||
import { useLogger } from '@react-navigation/devtools';
|
||||
import { StorageProvider } from './components/Context/StorageProvider';
|
||||
|
||||
const App = () => {
|
||||
const colorScheme = useColorScheme();
|
||||
|
||||
useLogger(navigationRef);
|
||||
|
||||
return (
|
||||
<LargeScreenProvider>
|
||||
<NavigationContainer ref={navigationRef} theme={colorScheme === 'dark' ? BlueDarkTheme : BlueDefaultTheme}>
|
||||
<SafeAreaProvider>
|
||||
<NavigationContainer ref={navigationRef} theme={colorScheme === 'dark' ? BlueDarkTheme : BlueDefaultTheme}>
|
||||
<SafeAreaProvider>
|
||||
<LargeScreenProvider>
|
||||
<StorageProvider>
|
||||
<SettingsProvider>
|
||||
<MasterView />
|
||||
</SettingsProvider>
|
||||
</StorageProvider>
|
||||
</SafeAreaProvider>
|
||||
</NavigationContainer>
|
||||
</LargeScreenProvider>
|
||||
</LargeScreenProvider>
|
||||
</SafeAreaProvider>
|
||||
</NavigationContainer>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -40,9 +40,16 @@ export const BlueCard = props => {
|
|||
return <View {...props} style={{ padding: 20 }} />;
|
||||
};
|
||||
|
||||
export const BlueText = props => {
|
||||
export const BlueText = ({ bold = false, ...props }) => {
|
||||
const { colors } = useTheme();
|
||||
const style = StyleSheet.compose({ color: colors.foregroundColor, writingDirection: I18nManager.isRTL ? 'rtl' : 'ltr' }, props.style);
|
||||
const style = StyleSheet.compose(
|
||||
{
|
||||
color: colors.foregroundColor,
|
||||
writingDirection: I18nManager.isRTL ? 'rtl' : 'ltr',
|
||||
fontWeight: bold ? 'bold' : 'normal',
|
||||
},
|
||||
props.style,
|
||||
);
|
||||
return <Text {...props} style={style} />;
|
||||
};
|
||||
|
||||
|
@ -75,6 +82,7 @@ export const BlueFormMultiInput = props => {
|
|||
multiline
|
||||
underlineColorAndroid="transparent"
|
||||
numberOfLines={4}
|
||||
editable={!props.editable}
|
||||
style={{
|
||||
paddingHorizontal: 8,
|
||||
paddingVertical: 16,
|
||||
|
|
12
Gemfile
|
@ -2,9 +2,15 @@ source "https://rubygems.org"
|
|||
|
||||
# You may use http://rbenv.org/ or https://rvm.io/ to install and use this version
|
||||
ruby "3.1.6"
|
||||
gem 'rubyzip', '2.3.2'
|
||||
gem 'cocoapods', '>= 1.13', '!= 1.15.0', '!= 1.15.1'
|
||||
gem 'rubyzip', '2.4.1'
|
||||
gem 'cocoapods', '~> 1.14.3'
|
||||
gem 'activesupport', '>= 6.1.7.5', '!= 7.1.0'
|
||||
gem "fastlane", ">= 2.225.0"
|
||||
gem "fastlane", "~> 2.226.0"
|
||||
gem 'xcodeproj', '< 1.26.0'
|
||||
gem 'concurrent-ruby', '< 1.3.4'
|
||||
|
||||
# Required for App Store Connect API
|
||||
gem "jwt"
|
||||
|
||||
plugins_path = File.join(File.dirname(__FILE__), 'fastlane', 'Pluginfile')
|
||||
eval_gemfile(plugins_path) if File.exist?(plugins_path)
|
||||
|
|
113
Gemfile.lock
|
@ -5,7 +5,7 @@ GEM
|
|||
base64
|
||||
nkf
|
||||
rexml
|
||||
activesupport (7.2.2)
|
||||
activesupport (7.2.2.1)
|
||||
base64
|
||||
benchmark (>= 0.3)
|
||||
bigdecimal
|
||||
|
@ -24,31 +24,32 @@ GEM
|
|||
json (>= 1.5.1)
|
||||
artifactory (3.0.17)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.3.0)
|
||||
aws-partitions (1.1002.0)
|
||||
aws-sdk-core (3.212.0)
|
||||
aws-eventstream (1.3.1)
|
||||
aws-partitions (1.1058.0)
|
||||
aws-sdk-core (3.219.0)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
base64
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
aws-sdk-kms (1.95.0)
|
||||
aws-sdk-core (~> 3, >= 3.210.0)
|
||||
aws-sdk-kms (1.99.0)
|
||||
aws-sdk-core (~> 3, >= 3.216.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.170.0)
|
||||
aws-sdk-core (~> 3, >= 3.210.0)
|
||||
aws-sdk-s3 (1.182.0)
|
||||
aws-sdk-core (~> 3, >= 3.216.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.10.1)
|
||||
aws-sigv4 (1.11.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
babosa (1.0.4)
|
||||
base64 (0.2.0)
|
||||
benchmark (0.4.0)
|
||||
bigdecimal (3.1.8)
|
||||
bigdecimal (3.1.9)
|
||||
claide (1.1.0)
|
||||
cocoapods (1.16.2)
|
||||
cocoapods (1.14.3)
|
||||
addressable (~> 2.8)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
cocoapods-core (= 1.16.2)
|
||||
cocoapods-core (= 1.14.3)
|
||||
cocoapods-deintegrate (>= 1.0.3, < 2.0)
|
||||
cocoapods-downloader (>= 2.1, < 3.0)
|
||||
cocoapods-plugins (>= 1.0.0, < 2.0)
|
||||
|
@ -62,8 +63,8 @@ GEM
|
|||
molinillo (~> 0.8.0)
|
||||
nap (~> 1.0)
|
||||
ruby-macho (>= 2.3.0, < 3.0)
|
||||
xcodeproj (>= 1.27.0, < 2.0)
|
||||
cocoapods-core (1.16.2)
|
||||
xcodeproj (>= 1.23.0, < 2.0)
|
||||
cocoapods-core (1.14.3)
|
||||
activesupport (>= 5.0, < 8)
|
||||
addressable (~> 2.8)
|
||||
algoliasearch (~> 1.0)
|
||||
|
@ -86,10 +87,10 @@ GEM
|
|||
colored2 (3.1.2)
|
||||
commander (4.6.0)
|
||||
highline (~> 2.0.0)
|
||||
concurrent-ruby (1.3.4)
|
||||
connection_pool (2.4.1)
|
||||
concurrent-ruby (1.3.3)
|
||||
connection_pool (2.5.0)
|
||||
declarative (0.0.20)
|
||||
digest-crc (0.6.5)
|
||||
digest-crc (0.7.0)
|
||||
rake (>= 12.0.0, < 14.0.0)
|
||||
domain_name (0.6.20240107)
|
||||
dotenv (2.8.1)
|
||||
|
@ -118,8 +119,8 @@ GEM
|
|||
faraday-em_synchrony (1.0.0)
|
||||
faraday-excon (1.1.0)
|
||||
faraday-httpclient (1.0.1)
|
||||
faraday-multipart (1.0.4)
|
||||
multipart-post (~> 2)
|
||||
faraday-multipart (1.1.0)
|
||||
multipart-post (~> 2.0)
|
||||
faraday-net_http (1.0.2)
|
||||
faraday-net_http_persistent (1.2.0)
|
||||
faraday-patron (1.0.0)
|
||||
|
@ -127,8 +128,8 @@ GEM
|
|||
faraday-retry (1.0.3)
|
||||
faraday_middleware (1.2.1)
|
||||
faraday (~> 1.0)
|
||||
fastimage (2.3.1)
|
||||
fastlane (2.225.0)
|
||||
fastimage (2.4.0)
|
||||
fastlane (2.226.0)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
addressable (>= 2.8, < 3.0.0)
|
||||
artifactory (~> 3.0)
|
||||
|
@ -168,17 +169,25 @@ GEM
|
|||
tty-spinner (>= 0.8.0, < 1.0.0)
|
||||
word_wrap (~> 1.0.0)
|
||||
xcodeproj (>= 1.13.0, < 2.0.0)
|
||||
xcpretty (~> 0.3.0)
|
||||
xcpretty (~> 0.4.0)
|
||||
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
|
||||
fastlane-plugin-browserstack (0.3.3)
|
||||
rest-client (~> 2.0, >= 2.0.2)
|
||||
fastlane-plugin-bugsnag (2.3.1)
|
||||
git
|
||||
xml-simple
|
||||
fastlane-plugin-bugsnag_sourcemaps_upload (0.2.0)
|
||||
fastlane-sirp (1.0.0)
|
||||
sysrandom (~> 1.0)
|
||||
ffi (1.17.0)
|
||||
ffi (1.17.1)
|
||||
fourflusher (2.3.1)
|
||||
fuzzy_match (2.0.4)
|
||||
gh_inspector (1.1.3)
|
||||
git (3.0.0)
|
||||
activesupport (>= 5.0)
|
||||
addressable (~> 2.8)
|
||||
process_executer (~> 1.3)
|
||||
rchardet (~> 1.9)
|
||||
google-apis-androidpublisher_v3 (0.54.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-apis-core (0.11.3)
|
||||
|
@ -217,36 +226,40 @@ GEM
|
|||
signet (>= 0.16, < 2.a)
|
||||
highline (2.0.3)
|
||||
http-accept (1.7.0)
|
||||
http-cookie (1.0.7)
|
||||
http-cookie (1.0.8)
|
||||
domain_name (~> 0.5)
|
||||
httpclient (2.8.3)
|
||||
i18n (1.14.6)
|
||||
httpclient (2.9.0)
|
||||
mutex_m
|
||||
i18n (1.14.7)
|
||||
concurrent-ruby (~> 1.0)
|
||||
jmespath (1.6.2)
|
||||
json (2.8.1)
|
||||
jwt (2.9.3)
|
||||
json (2.10.1)
|
||||
jwt (2.10.1)
|
||||
base64
|
||||
logger (1.6.1)
|
||||
logger (1.6.6)
|
||||
mime-types (3.6.0)
|
||||
logger
|
||||
mime-types-data (~> 3.2015)
|
||||
mime-types-data (3.2024.1105)
|
||||
mime-types-data (3.2025.0220)
|
||||
mini_magick (4.13.2)
|
||||
mini_mime (1.1.5)
|
||||
minitest (5.25.1)
|
||||
minitest (5.25.4)
|
||||
molinillo (0.8.0)
|
||||
multi_json (1.15.0)
|
||||
multipart-post (2.4.1)
|
||||
nanaimo (0.4.0)
|
||||
mutex_m (0.3.0)
|
||||
nanaimo (0.3.0)
|
||||
nap (1.1.0)
|
||||
naturally (2.2.1)
|
||||
netrc (0.11.0)
|
||||
nkf (0.2.0)
|
||||
optparse (0.5.0)
|
||||
optparse (0.6.0)
|
||||
os (1.1.4)
|
||||
plist (3.7.1)
|
||||
plist (3.7.2)
|
||||
process_executer (1.3.0)
|
||||
public_suffix (4.0.7)
|
||||
rake (13.2.1)
|
||||
rchardet (1.9.0)
|
||||
representable (3.2.0)
|
||||
declarative (< 0.1.0)
|
||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||
|
@ -257,12 +270,12 @@ GEM
|
|||
mime-types (>= 1.16, < 4.0)
|
||||
netrc (~> 0.8)
|
||||
retriable (3.1.2)
|
||||
rexml (3.3.9)
|
||||
rouge (2.0.7)
|
||||
rexml (3.4.1)
|
||||
rouge (3.28.0)
|
||||
ruby-macho (2.5.1)
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (2.3.2)
|
||||
securerandom (0.3.1)
|
||||
rubyzip (2.4.1)
|
||||
securerandom (0.4.1)
|
||||
security (0.1.5)
|
||||
signet (0.19.0)
|
||||
addressable (~> 2.8)
|
||||
|
@ -288,31 +301,37 @@ GEM
|
|||
uber (0.1.0)
|
||||
unicode-display_width (2.6.0)
|
||||
word_wrap (1.0.0)
|
||||
xcodeproj (1.27.0)
|
||||
xcodeproj (1.25.1)
|
||||
CFPropertyList (>= 2.3.3, < 4.0)
|
||||
atomos (~> 0.1.3)
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
colored2 (~> 3.1)
|
||||
nanaimo (~> 0.4.0)
|
||||
nanaimo (~> 0.3.0)
|
||||
rexml (>= 3.3.6, < 4.0)
|
||||
xcpretty (0.3.0)
|
||||
rouge (~> 2.0.7)
|
||||
xcpretty (0.4.0)
|
||||
rouge (~> 3.28.0)
|
||||
xcpretty-travis-formatter (1.0.1)
|
||||
xcpretty (~> 0.2, >= 0.0.7)
|
||||
xml-simple (1.1.9)
|
||||
rexml
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
activesupport (>= 6.1.7.5, != 7.1.0)
|
||||
cocoapods (>= 1.13, != 1.15.1, != 1.15.0)
|
||||
fastlane (>= 2.225.0)
|
||||
cocoapods (~> 1.14.3)
|
||||
concurrent-ruby (< 1.3.4)
|
||||
fastlane (~> 2.226.0)
|
||||
fastlane-plugin-browserstack
|
||||
fastlane-plugin-bugsnag
|
||||
fastlane-plugin-bugsnag_sourcemaps_upload
|
||||
rubyzip (= 2.3.2)
|
||||
jwt
|
||||
rubyzip (= 2.4.1)
|
||||
xcodeproj (< 1.26.0)
|
||||
|
||||
RUBY VERSION
|
||||
ruby 3.3.5p100
|
||||
ruby 3.1.6p260
|
||||
|
||||
BUNDLED WITH
|
||||
2.5.18
|
||||
2.3.27
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
import 'react-native-gesture-handler'; // should be on top
|
||||
|
||||
import React, { lazy, Suspense } from 'react';
|
||||
import MainRoot from './navigation';
|
||||
import { useStorage } from './hooks/context/useStorage';
|
||||
const CompanionDelegates = lazy(() => import('./components/CompanionDelegates'));
|
||||
|
||||
const MasterView = () => {
|
||||
const { walletsInitialized } = useStorage();
|
||||
|
||||
return (
|
||||
<>
|
||||
<MainRoot />
|
||||
{walletsInitialized && (
|
||||
<Suspense>
|
||||
<CompanionDelegates />
|
||||
</Suspense>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MasterView;
|
|
@ -14,10 +14,6 @@ export function dispatch(action: NavigationAction) {
|
|||
}
|
||||
}
|
||||
|
||||
export function navigateToWalletsList() {
|
||||
navigate('WalletsList');
|
||||
}
|
||||
|
||||
export function reset() {
|
||||
if (navigationRef.isReady()) {
|
||||
navigationRef.current?.reset({
|
||||
|
|
|
@ -73,6 +73,10 @@ def enableProguardInReleaseBuilds = false
|
|||
def jscFlavor = 'org.webkit:android-jsc-intl:+'
|
||||
|
||||
android {
|
||||
androidResources {
|
||||
noCompress += ["bundle"]
|
||||
}
|
||||
|
||||
ndkVersion rootProject.ext.ndkVersion
|
||||
buildToolsVersion rootProject.ext.buildToolsVersion
|
||||
compileSdkVersion rootProject.ext.compileSdkVersion
|
||||
|
@ -83,7 +87,7 @@ android {
|
|||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 1
|
||||
versionName "7.0.6"
|
||||
versionName "7.1.5"
|
||||
testBuildType System.getProperty('testBuildType', 'debug')
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
}
|
||||
|
@ -133,7 +137,7 @@ dependencies {
|
|||
androidTestImplementation('com.wix:detox:0.1.1')
|
||||
implementation 'androidx.appcompat:appcompat:1.7.0'
|
||||
implementation fileTree(dir: "libs", include: ["*.jar"])
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.2.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
|
||||
}
|
||||
apply plugin: 'com.google.gms.google-services' // Google Services plugin
|
||||
apply plugin: "com.bugsnag.android.gradle"
|
||||
apply plugin: "com.bugsnag.android.gradle"
|
||||
|
|
|
@ -16,13 +16,6 @@
|
|||
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.ACTION_OPEN_DOCUMENT" />
|
||||
<uses-permission android:name="android.permission.ACTION_GET_CONTENT" />
|
||||
<uses-permission android:name="android.permission.ACTION_CREATE_DOCUMENT" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK"/>
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
<uses-permission android:name="android.permission.ACTION_SCREEN_OFF"/>
|
||||
<uses-permission android:name="android.permission.ACTION_SCREEN_ON"/>
|
||||
|
||||
<application
|
||||
android:name=".MainApplication"
|
||||
|
@ -58,7 +51,15 @@
|
|||
<meta-data
|
||||
android:name="firebase_analytics_collection_enabled"
|
||||
android:value="false" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
<receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationActions" />
|
||||
<receiver android:name="com.dieam.reactnativepushnotification.modules.RNPushNotificationPublisher" />
|
||||
<receiver
|
||||
|
@ -106,14 +107,12 @@
|
|||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="file" android:mimeType="application/octet-stream" android:pathPattern=".*\\.psbt" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="file" android:mimeType="image/jpeg" />
|
||||
<data android:scheme="file" android:mimeType="image/png" />
|
||||
<data android:scheme="file" android:mimeType="image/jpg" />
|
||||
|
@ -123,7 +122,6 @@
|
|||
<intent-filter tools:ignore="AppLinkUrlError">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="bitcoin" />
|
||||
<data android:scheme="lightning" />
|
||||
<data android:scheme="bluewallet" />
|
||||
|
|
|
@ -125,6 +125,13 @@
|
|||
"symbol": "ÂŁ",
|
||||
"country": "United Kingdom (British Pound)"
|
||||
},
|
||||
"HKD": {
|
||||
"endPointKey": "HKD",
|
||||
"locale": "zh-HK",
|
||||
"source": "CoinGecko",
|
||||
"symbol": "HK$",
|
||||
"country": "Hong Kong (Hong Kong Dollar)"
|
||||
},
|
||||
"HRK": {
|
||||
"endPointKey": "HRK",
|
||||
"locale": "hr-HR",
|
||||
|
@ -286,6 +293,13 @@
|
|||
"symbol": "zł",
|
||||
"country": "Poland (Polish Zloty)"
|
||||
},
|
||||
"PYG": {
|
||||
"endPointKey": "PYG",
|
||||
"locale": "es-PY",
|
||||
"source": "CoinDesk",
|
||||
"symbol": "₲",
|
||||
"country": "Paraguay (Paraguayan Guarani)"
|
||||
},
|
||||
"QAR": {
|
||||
"endPointKey": "QAR",
|
||||
"locale": "ar-QA",
|
||||
|
@ -303,7 +317,7 @@
|
|||
"RSD": {
|
||||
"endPointKey": "RSD",
|
||||
"locale": "sr-RS",
|
||||
"source": "CoinGecko",
|
||||
"source": "CoinDesk",
|
||||
"symbol": "DIN",
|
||||
"country": "Serbia (Serbian Dinar)"
|
||||
},
|
||||
|
|
|
@ -4,32 +4,46 @@ import android.appwidget.AppWidgetManager
|
|||
import android.appwidget.AppWidgetProvider
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.work.WorkManager
|
||||
|
||||
class BitcoinPriceWidget : AppWidgetProvider() {
|
||||
|
||||
companion object {
|
||||
private const val TAG = "BitcoinPriceWidget"
|
||||
private const val SHARED_PREF_NAME = "group.io.bluewallet.bluewallet"
|
||||
}
|
||||
|
||||
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
|
||||
super.onUpdate(context, appWidgetManager, appWidgetIds)
|
||||
Log.d("BitcoinPriceWidget", "onUpdate called")
|
||||
WidgetUpdateWorker.scheduleWork(context)
|
||||
for (widgetId in appWidgetIds) {
|
||||
Log.d(TAG, "Updating widget with ID: $widgetId")
|
||||
WidgetUpdateWorker.scheduleWork(context)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onEnabled(context: Context) {
|
||||
super.onEnabled(context)
|
||||
Log.d("BitcoinPriceWidget", "onEnabled called")
|
||||
Log.d(TAG, "onEnabled called")
|
||||
WidgetUpdateWorker.scheduleWork(context)
|
||||
}
|
||||
|
||||
override fun onDisabled(context: Context) {
|
||||
super.onDisabled(context)
|
||||
Log.d("BitcoinPriceWidget", "onDisabled called")
|
||||
Log.d(TAG, "onDisabled called")
|
||||
clearCache(context)
|
||||
WorkManager.getInstance(context).cancelUniqueWork(WidgetUpdateWorker.WORK_NAME)
|
||||
}
|
||||
|
||||
private fun clearCache(context: Context) {
|
||||
val sharedPref = context.getSharedPreferences("group.io.bluewallet.bluewallet", Context.MODE_PRIVATE)
|
||||
sharedPref.edit().clear().apply() // Clear all preferences in the group
|
||||
Log.d("BitcoinPriceWidget", "Cache cleared from group.io.bluewallet.bluewallet")
|
||||
override fun onDeleted(context: Context, appWidgetIds: IntArray) {
|
||||
super.onDeleted(context, appWidgetIds)
|
||||
Log.d(TAG, "onDeleted called for widgets: ${appWidgetIds.joinToString()}")
|
||||
}
|
||||
|
||||
private fun clearCache(context: Context) {
|
||||
val sharedPref = context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE)
|
||||
sharedPref.edit().clear().apply()
|
||||
Log.d(TAG, "Cache cleared from $SHARED_PREF_NAME")
|
||||
}
|
||||
|
||||
}
|
|
@ -2,6 +2,7 @@ package io.bluewallet.bluewallet
|
|||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import com.bugsnag.android.Bugsnag
|
||||
import com.facebook.react.PackageList
|
||||
import com.facebook.react.ReactApplication
|
||||
|
@ -11,11 +12,20 @@ import com.facebook.react.ReactPackage
|
|||
import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.load
|
||||
import com.facebook.react.defaults.DefaultReactHost.getDefaultReactHost
|
||||
import com.facebook.react.defaults.DefaultReactNativeHost
|
||||
import com.facebook.react.soloader.OpenSourceMergedSoMapping
|
||||
import com.facebook.soloader.SoLoader
|
||||
import com.facebook.react.modules.i18nmanager.I18nUtil
|
||||
|
||||
class MainApplication : Application(), ReactApplication {
|
||||
|
||||
private lateinit var sharedPref: SharedPreferences
|
||||
private val preferenceChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { prefs, key ->
|
||||
if (key == "preferredCurrency") {
|
||||
prefs.edit().remove("previous_price").apply()
|
||||
WidgetUpdateWorker.scheduleWork(this)
|
||||
}
|
||||
}
|
||||
|
||||
override val reactNativeHost: ReactNativeHost =
|
||||
object : DefaultReactNativeHost(this) {
|
||||
override fun getPackages(): List<ReactPackage> =
|
||||
|
@ -35,27 +45,29 @@ class MainApplication : Application(), ReactApplication {
|
|||
override val reactHost: ReactHost
|
||||
get() = getDefaultReactHost(applicationContext, reactNativeHost)
|
||||
|
||||
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
sharedPref = getSharedPreferences("group.io.bluewallet.bluewallet", Context.MODE_PRIVATE)
|
||||
sharedPref.registerOnSharedPreferenceChangeListener(preferenceChangeListener)
|
||||
val sharedI18nUtilInstance = I18nUtil.getInstance()
|
||||
sharedI18nUtilInstance.allowRTL(applicationContext, true)
|
||||
SoLoader.init(this, /* native exopackage */ false)
|
||||
|
||||
SoLoader.init(this, OpenSourceMergedSoMapping)
|
||||
if (BuildConfig.IS_NEW_ARCHITECTURE_ENABLED) {
|
||||
// If you opted-in for the New Architecture, we load the native entry point for this app.
|
||||
load()
|
||||
}
|
||||
|
||||
val sharedPref = applicationContext.getSharedPreferences("group.io.bluewallet.bluewallet", Context.MODE_PRIVATE)
|
||||
initializeBugsnag()
|
||||
}
|
||||
|
||||
// Retrieve the "donottrack" value. Default to "0" if not found.
|
||||
override fun onTerminate() {
|
||||
super.onTerminate()
|
||||
sharedPref.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener)
|
||||
}
|
||||
|
||||
private fun initializeBugsnag() {
|
||||
val isDoNotTrackEnabled = sharedPref.getString("donottrack", "0")
|
||||
|
||||
// Check if do not track is not enabled and initialize Bugsnag if so
|
||||
if (isDoNotTrackEnabled != "1") {
|
||||
// Initialize Bugsnag or your error tracking here
|
||||
Bugsnag.start(this)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,20 +2,21 @@ package io.bluewallet.bluewallet
|
|||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.json.JSONObject
|
||||
import java.io.InputStreamReader
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
|
||||
object MarketAPI {
|
||||
|
||||
private const val TAG = "MarketAPI"
|
||||
private val client = OkHttpClient()
|
||||
|
||||
var baseUrl: String? = null
|
||||
|
||||
fun fetchPrice(context: Context, currency: String): String? {
|
||||
suspend fun fetchPrice(context: Context, currency: String): String? {
|
||||
return try {
|
||||
// Load the JSON data from the assets
|
||||
val fiatUnitsJson = context.assets.open("fiatUnits.json").bufferedReader().use { it.readText() }
|
||||
val json = JSONObject(fiatUnitsJson)
|
||||
val currencyInfo = json.getJSONObject(currency)
|
||||
|
@ -25,26 +26,16 @@ object MarketAPI {
|
|||
val urlString = buildURLString(source, endPointKey)
|
||||
Log.d(TAG, "Fetching price from URL: $urlString")
|
||||
|
||||
val url = URL(urlString)
|
||||
val urlConnection = url.openConnection() as HttpURLConnection
|
||||
urlConnection.requestMethod = "GET"
|
||||
urlConnection.connect()
|
||||
val request = Request.Builder().url(urlString).build()
|
||||
val response = withContext(Dispatchers.IO) { client.newCall(request).execute() }
|
||||
|
||||
val responseCode = urlConnection.responseCode
|
||||
if (responseCode != 200) {
|
||||
Log.e(TAG, "Failed to fetch price. Response code: $responseCode")
|
||||
if (!response.isSuccessful) {
|
||||
Log.e(TAG, "Failed to fetch price. Response code: ${response.code}")
|
||||
return null
|
||||
}
|
||||
|
||||
val reader = InputStreamReader(urlConnection.inputStream)
|
||||
val jsonResponse = StringBuilder()
|
||||
val buffer = CharArray(1024)
|
||||
var read: Int
|
||||
while (reader.read(buffer).also { read = it } != -1) {
|
||||
jsonResponse.append(buffer, 0, read)
|
||||
}
|
||||
|
||||
parseJSONBasedOnSource(jsonResponse.toString(), source, endPointKey)
|
||||
val jsonResponse = response.body?.string() ?: return null
|
||||
parseJSONBasedOnSource(jsonResponse, source, endPointKey)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error fetching price", e)
|
||||
null
|
||||
|
@ -65,7 +56,8 @@ object MarketAPI {
|
|||
"CoinGecko" -> "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=${endPointKey.lowercase()}"
|
||||
"BNR" -> "https://www.bnr.ro/nbrfxrates.xml"
|
||||
"Kraken" -> "https://api.kraken.com/0/public/Ticker?pair=XXBTZ${endPointKey.uppercase()}"
|
||||
else -> "https://api.coindesk.com/v1/bpi/currentprice/$endPointKey.json"
|
||||
"CoinDesk" -> "https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=${endPointKey.uppercase()}"
|
||||
else -> "https://min-api.cryptocompare.com/data/price?fsym=BTC&tsyms=${endPointKey.uppercase()}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -82,6 +74,10 @@ object MarketAPI {
|
|||
"coinpaprika" -> json.getJSONObject("quotes").getJSONObject("INR").getString("price")
|
||||
"Coinbase" -> json.getJSONObject("data").getString("amount")
|
||||
"Kraken" -> json.getJSONObject("result").getJSONObject("XXBTZ${endPointKey.uppercase()}").getJSONArray("c").getString(0)
|
||||
"CoinDesk" -> {
|
||||
val rate = json.optDouble(endPointKey.uppercase(), -1.0)
|
||||
if (rate < 0) null else rate.toString()
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
package io.bluewallet.bluewallet
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
|
@ -13,8 +15,10 @@ import java.text.NumberFormat
|
|||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class WidgetUpdateWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) {
|
||||
class WidgetUpdateWorker(context: Context, workerParams: WorkerParameters) : CoroutineWorker(context, workerParams) {
|
||||
|
||||
companion object {
|
||||
const val TAG = "WidgetUpdateWorker"
|
||||
|
@ -35,66 +39,57 @@ class WidgetUpdateWorker(context: Context, workerParams: WorkerParameters) : Wor
|
|||
}
|
||||
|
||||
private lateinit var sharedPref: SharedPreferences
|
||||
private lateinit var preferenceChangeListener: SharedPreferences.OnSharedPreferenceChangeListener
|
||||
|
||||
override fun doWork(): Result {
|
||||
override suspend fun doWork(): Result {
|
||||
Log.d(TAG, "Widget update worker running")
|
||||
|
||||
sharedPref = applicationContext.getSharedPreferences("group.io.bluewallet.bluewallet", Context.MODE_PRIVATE)
|
||||
registerPreferenceChangeListener()
|
||||
|
||||
val appWidgetManager = AppWidgetManager.getInstance(applicationContext)
|
||||
val thisWidget = ComponentName(applicationContext, BitcoinPriceWidget::class.java)
|
||||
val appWidgetIds = appWidgetManager.getAppWidgetIds(thisWidget)
|
||||
val views = RemoteViews(applicationContext.packageName, R.layout.widget_layout)
|
||||
|
||||
val intent = Intent(applicationContext, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
}
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
applicationContext,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
views.setOnClickPendingIntent(R.id.widget_layout, pendingIntent)
|
||||
|
||||
// Show loading indicator
|
||||
views.setViewVisibility(R.id.loading_indicator, View.VISIBLE)
|
||||
views.setViewVisibility(R.id.price_value, View.GONE)
|
||||
views.setViewVisibility(R.id.last_updated_label, View.GONE)
|
||||
views.setViewVisibility(R.id.last_updated_time, View.GONE)
|
||||
views.setViewVisibility(R.id.price_arrow_container, View.GONE)
|
||||
|
||||
appWidgetManager.updateAppWidget(appWidgetIds, views)
|
||||
|
||||
val preferredCurrency = sharedPref.getString("preferredCurrency", null) ?: "USD"
|
||||
val preferredCurrencyLocale = sharedPref.getString("preferredCurrencyLocale", null) ?: "en-US"
|
||||
val previousPrice = sharedPref.getString("previous_price", null)
|
||||
|
||||
val currentTime = SimpleDateFormat("hh:mm a", Locale.getDefault()).format(Date())
|
||||
|
||||
fetchPrice(preferredCurrency) { fetchedPrice, error ->
|
||||
handlePriceResult(
|
||||
appWidgetManager, appWidgetIds, views, sharedPref,
|
||||
fetchedPrice, previousPrice, currentTime, preferredCurrency, preferredCurrencyLocale, error
|
||||
)
|
||||
}
|
||||
val fetchedPrice = fetchPrice(preferredCurrency)
|
||||
|
||||
handlePriceResult(
|
||||
appWidgetManager, appWidgetIds, views, sharedPref,
|
||||
fetchedPrice, previousPrice, currentTime, preferredCurrency, preferredCurrencyLocale
|
||||
)
|
||||
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
private fun registerPreferenceChangeListener() {
|
||||
preferenceChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { sharedPreferences, key ->
|
||||
if (key == "preferredCurrency" || key == "preferredCurrencyLocale" || key == "previous_price") {
|
||||
Log.d(TAG, "Preference changed: $key")
|
||||
updateWidgetOnPreferenceChange()
|
||||
}
|
||||
}
|
||||
sharedPref.registerOnSharedPreferenceChangeListener(preferenceChangeListener)
|
||||
}
|
||||
|
||||
override fun onStopped() {
|
||||
super.onStopped()
|
||||
sharedPref.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener)
|
||||
}
|
||||
|
||||
private fun updateWidgetOnPreferenceChange() {
|
||||
val appWidgetManager = AppWidgetManager.getInstance(applicationContext)
|
||||
val thisWidget = ComponentName(applicationContext, BitcoinPriceWidget::class.java)
|
||||
val appWidgetIds = appWidgetManager.getAppWidgetIds(thisWidget)
|
||||
val views = RemoteViews(applicationContext.packageName, R.layout.widget_layout)
|
||||
|
||||
val preferredCurrency = sharedPref.getString("preferredCurrency", null) ?: "USD"
|
||||
val preferredCurrencyLocale = sharedPref.getString("preferredCurrencyLocale", null) ?: "en-US"
|
||||
val previousPrice = sharedPref.getString("previous_price", null)
|
||||
val currentTime = SimpleDateFormat("hh:mm a", Locale.getDefault()).format(Date())
|
||||
|
||||
fetchPrice(preferredCurrency) { fetchedPrice, error ->
|
||||
handlePriceResult(
|
||||
appWidgetManager, appWidgetIds, views, sharedPref,
|
||||
fetchedPrice, previousPrice, currentTime, preferredCurrency, preferredCurrencyLocale, error
|
||||
)
|
||||
private suspend fun fetchPrice(currency: String?): String? {
|
||||
return withContext(Dispatchers.IO) {
|
||||
MarketAPI.fetchPrice(applicationContext, currency ?: "USD")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -107,24 +102,27 @@ class WidgetUpdateWorker(context: Context, workerParams: WorkerParameters) : Wor
|
|||
previousPrice: String?,
|
||||
currentTime: String,
|
||||
preferredCurrency: String?,
|
||||
preferredCurrencyLocale: String?,
|
||||
error: String?
|
||||
preferredCurrencyLocale: String?
|
||||
) {
|
||||
val isPriceFetched = fetchedPrice != null
|
||||
val isPriceCached = previousPrice != null
|
||||
|
||||
if (error != null || !isPriceFetched) {
|
||||
Log.e(TAG, "Error fetching price: $error")
|
||||
if (!isPriceFetched) {
|
||||
Log.e(TAG, "Error fetching price.")
|
||||
if (!isPriceCached) {
|
||||
showLoadingError(views)
|
||||
} else {
|
||||
displayCachedPrice(views, previousPrice, currentTime, preferredCurrency, preferredCurrencyLocale)
|
||||
}
|
||||
} else {
|
||||
displayFetchedPrice(
|
||||
views, fetchedPrice!!, previousPrice, currentTime, preferredCurrency, preferredCurrencyLocale
|
||||
)
|
||||
savePrice(sharedPref, fetchedPrice)
|
||||
if (fetchedPrice != null) {
|
||||
displayFetchedPrice(
|
||||
views, fetchedPrice, previousPrice, currentTime, preferredCurrency, preferredCurrencyLocale
|
||||
)
|
||||
}
|
||||
if (fetchedPrice != null) {
|
||||
savePrice(sharedPref, fetchedPrice)
|
||||
}
|
||||
}
|
||||
|
||||
appWidgetManager.updateAppWidget(appWidgetIds, views)
|
||||
|
@ -132,7 +130,7 @@ class WidgetUpdateWorker(context: Context, workerParams: WorkerParameters) : Wor
|
|||
|
||||
private fun showLoadingError(views: RemoteViews) {
|
||||
views.apply {
|
||||
setViewVisibility(R.id.loading_indicator, View.VISIBLE)
|
||||
setViewVisibility(R.id.loading_indicator, View.GONE)
|
||||
setViewVisibility(R.id.price_value, View.GONE)
|
||||
setViewVisibility(R.id.last_updated_label, View.GONE)
|
||||
setViewVisibility(R.id.last_updated_time, View.GONE)
|
||||
|
@ -216,15 +214,6 @@ class WidgetUpdateWorker(context: Context, workerParams: WorkerParameters) : Wor
|
|||
return currencyFormat
|
||||
}
|
||||
|
||||
private fun fetchPrice(currency: String?, callback: (String?, String?) -> Unit) {
|
||||
val price = MarketAPI.fetchPrice(applicationContext, currency ?: "USD")
|
||||
if (price == null) {
|
||||
callback(null, "Failed to fetch price")
|
||||
} else {
|
||||
callback(price, null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun savePrice(sharedPref: SharedPreferences, price: String) {
|
||||
sharedPref.edit().putString("previous_price", price).apply()
|
||||
}
|
||||
|
|
|
@ -13,13 +13,14 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone"/>
|
||||
android:visibility="visible"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="end">
|
||||
android:gravity="end"
|
||||
android:layout_gravity="end">
|
||||
<TextView
|
||||
android:id="@+id/last_updated_label"
|
||||
style="@style/WidgetTextSecondary"
|
||||
|
@ -29,69 +30,86 @@
|
|||
android:textSize="12sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:visibility="gone"/>
|
||||
android:visibility="gone"
|
||||
android:layout_gravity="end"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/last_updated_time"
|
||||
style="@style/WidgetTextPrimary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text=""
|
||||
android:text="--:--"
|
||||
android:textSize="12sp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:visibility="gone"/>
|
||||
android:visibility="gone"
|
||||
android:layout_gravity="end"/>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="end">
|
||||
android:gravity="end"
|
||||
android:layout_gravity="end">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/price_value"
|
||||
style="@style/WidgetTextPrimary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text=""
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:visibility="gone"/>
|
||||
<LinearLayout
|
||||
android:id="@+id/price_arrow_container"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="end"
|
||||
android:visibility="gone">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/price_arrow"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:layout_marginBottom="8dp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/previous_price_label"
|
||||
style="@style/WidgetTextPrimary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="From:"
|
||||
android:textSize="12sp"
|
||||
android:layout_gravity="end"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="8dp"/>
|
||||
<TextView
|
||||
android:id="@+id/previous_price"
|
||||
style="@style/WidgetTextPrimary"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:autoSizeMaxTextSize="24sp"
|
||||
android:autoSizeMinTextSize="12sp"
|
||||
android:autoSizeStepGranularity="2sp"
|
||||
android:autoSizeTextType="uniform"
|
||||
android:duplicateParentState="false"
|
||||
android:editable="false"
|
||||
android:gravity="end"
|
||||
android:lines="1"
|
||||
android:text="Loading..."
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
android:visibility="gone" />
|
||||
<LinearLayout
|
||||
android:id="@+id/price_arrow_container"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text=""
|
||||
android:textSize="12sp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="8dp"/>
|
||||
</LinearLayout></LinearLayout>
|
||||
android:orientation="horizontal"
|
||||
android:gravity="end"
|
||||
android:visibility="gone"
|
||||
android:layout_gravity="end">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/price_arrow"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:layout_gravity="end"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/previous_price_label"
|
||||
style="@style/WidgetTextPrimary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="From:"
|
||||
android:textSize="12sp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:layout_gravity="end"/>
|
||||
<TextView
|
||||
android:id="@+id/previous_price"
|
||||
style="@style/WidgetTextPrimary"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="--"
|
||||
android:textSize="12sp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:layout_gravity="end"/>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:initialLayout="@layout/widget_layout"
|
||||
android:minWidth="160dp"
|
||||
android:minWidth="170dp"
|
||||
android:minHeight="100dp"
|
||||
android:updatePeriodMillis="0"
|
||||
android:widgetCategory="home_screen"
|
||||
|
|
4
android/app/src/main/res/xml/file_paths.xml
Normal file
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<external-path name="downloads" path="Download/" />
|
||||
</paths>
|
|
@ -3,8 +3,8 @@
|
|||
buildscript {
|
||||
ext {
|
||||
minSdkVersion = 24
|
||||
buildToolsVersion = "34.0.0"
|
||||
compileSdkVersion = 34
|
||||
buildToolsVersion = "35.0.0"
|
||||
compileSdkVersion = 35
|
||||
targetSdkVersion = 34
|
||||
googlePlayServicesVersion = "16.+"
|
||||
googlePlayServicesIidVersion = "16.0.1"
|
||||
|
@ -58,8 +58,8 @@ subprojects {
|
|||
afterEvaluate {project ->
|
||||
if (project.hasProperty("android")) {
|
||||
android {
|
||||
buildToolsVersion "34.0.0"
|
||||
compileSdkVersion 34
|
||||
buildToolsVersion "35.0.0"
|
||||
compileSdkVersion 35
|
||||
defaultConfig {
|
||||
minSdkVersion 24
|
||||
}
|
||||
|
|
2
android/gradlew
vendored
|
@ -249,4 +249,4 @@ eval "set -- $(
|
|||
tr '\n' ' '
|
||||
)" '"$@"'
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
exec "$JAVACMD" "$@"
|
188
android/gradlew.bat
vendored
|
@ -1,94 +1,94 @@
|
|||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
set EXIT_CODE=%ERRORLEVEL%
|
||||
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||
exit /b %EXIT_CODE%
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import BigNumber from 'bignumber.js';
|
||||
import * as bitcoin from 'bitcoinjs-lib';
|
||||
import DefaultPreference from 'react-native-default-preference';
|
||||
|
@ -9,6 +8,9 @@ import { LegacyWallet, SegwitBech32Wallet, SegwitP2SHWallet, TaprootWallet } fro
|
|||
import presentAlert from '../components/Alert';
|
||||
import loc from '../loc';
|
||||
import { GROUP_IO_BLUEWALLET } from './currency';
|
||||
import { ElectrumServerItem } from '../screen/settings/ElectrumSettings';
|
||||
import { triggerWarningHapticFeedback } from './hapticFeedback';
|
||||
import { AlertButton } from 'react-native';
|
||||
|
||||
const ElectrumClient = require('electrum-client');
|
||||
const net = require('net');
|
||||
|
@ -67,17 +69,11 @@ type MempoolTransaction = {
|
|||
fee: number;
|
||||
};
|
||||
|
||||
type Peer =
|
||||
| {
|
||||
host: string;
|
||||
ssl: string;
|
||||
tcp?: undefined;
|
||||
}
|
||||
| {
|
||||
host: string;
|
||||
tcp: string;
|
||||
ssl?: undefined;
|
||||
};
|
||||
type Peer = {
|
||||
host: string;
|
||||
ssl?: number;
|
||||
tcp?: number;
|
||||
};
|
||||
|
||||
export const ELECTRUM_HOST = 'electrum_host';
|
||||
export const ELECTRUM_TCP_PORT = 'electrum_tcp_port';
|
||||
|
@ -85,16 +81,20 @@ export const ELECTRUM_SSL_PORT = 'electrum_ssl_port';
|
|||
export const ELECTRUM_SERVER_HISTORY = 'electrum_server_history';
|
||||
const ELECTRUM_CONNECTION_DISABLED = 'electrum_disabled';
|
||||
const storageKey = 'ELECTRUM_PEERS';
|
||||
const defaultPeer = { host: 'electrum1.bluewallet.io', ssl: '443' };
|
||||
const defaultPeer = { host: 'electrum1.bluewallet.io', ssl: 443 };
|
||||
export const hardcodedPeers: Peer[] = [
|
||||
{ host: 'mainnet.foundationdevices.com', ssl: '50002' },
|
||||
// { host: 'bitcoin.lukechilds.co', ssl: '50002' },
|
||||
{ host: 'mainnet.foundationdevices.com', ssl: 50002 },
|
||||
// { host: 'bitcoin.lukechilds.co', ssl: 50002 },
|
||||
// { host: 'electrum.jochen-hoenicke.de', ssl: '50006' },
|
||||
{ host: 'electrum1.bluewallet.io', ssl: '443' },
|
||||
{ host: 'electrum.acinq.co', ssl: '50002' },
|
||||
{ host: 'electrum.bitaroo.net', ssl: '50002' },
|
||||
{ host: 'electrum1.bluewallet.io', ssl: 443 },
|
||||
{ host: 'electrum.acinq.co', ssl: 50002 },
|
||||
{ host: 'electrum.bitaroo.net', ssl: 50002 },
|
||||
];
|
||||
|
||||
export const suggestedServers: Peer[] = hardcodedPeers.map(peer => ({
|
||||
...peer,
|
||||
}));
|
||||
|
||||
let mainClient: typeof ElectrumClient | undefined;
|
||||
let mainConnected: boolean = false;
|
||||
let wasConnectedAtLeastOnce: boolean = false;
|
||||
|
@ -131,28 +131,71 @@ async function _getRealm() {
|
|||
schema,
|
||||
path,
|
||||
encryptionKey,
|
||||
excludeFromIcloudBackup: true,
|
||||
});
|
||||
|
||||
return _realm;
|
||||
}
|
||||
|
||||
export const getPreferredServer = async (): Promise<ElectrumServerItem | undefined> => {
|
||||
try {
|
||||
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
|
||||
const host = (await DefaultPreference.get(ELECTRUM_HOST)) as string;
|
||||
const tcpPort = await DefaultPreference.get(ELECTRUM_TCP_PORT);
|
||||
const sslPort = await DefaultPreference.get(ELECTRUM_SSL_PORT);
|
||||
|
||||
console.log('Getting preferred server:', { host, tcpPort, sslPort });
|
||||
|
||||
if (!host) {
|
||||
console.warn('Preferred server host is undefined');
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
host,
|
||||
tcp: tcpPort ? Number(tcpPort) : undefined,
|
||||
ssl: sslPort ? Number(sslPort) : undefined,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error in getPreferredServer:', error);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
export const removePreferredServer = async () => {
|
||||
try {
|
||||
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
|
||||
console.log('Removing preferred server');
|
||||
await DefaultPreference.clear(ELECTRUM_HOST);
|
||||
await DefaultPreference.clear(ELECTRUM_TCP_PORT);
|
||||
await DefaultPreference.clear(ELECTRUM_SSL_PORT);
|
||||
} catch (error) {
|
||||
console.error('Error in removePreferredServer:', error);
|
||||
}
|
||||
};
|
||||
|
||||
export async function isDisabled(): Promise<boolean> {
|
||||
let result;
|
||||
try {
|
||||
const savedValue = await AsyncStorage.getItem(ELECTRUM_CONNECTION_DISABLED);
|
||||
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
|
||||
const savedValue = await DefaultPreference.get(ELECTRUM_CONNECTION_DISABLED);
|
||||
console.log('Getting Electrum connection disabled state:', savedValue);
|
||||
if (savedValue === null) {
|
||||
result = false;
|
||||
} else {
|
||||
result = savedValue;
|
||||
}
|
||||
} catch {
|
||||
} catch (error) {
|
||||
console.error('Error getting Electrum connection disabled state:', error);
|
||||
result = false;
|
||||
}
|
||||
return !!result;
|
||||
}
|
||||
|
||||
export async function setDisabled(disabled = true) {
|
||||
return AsyncStorage.setItem(ELECTRUM_CONNECTION_DISABLED, disabled ? '1' : '');
|
||||
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
|
||||
console.log('Setting Electrum connection disabled state to:', disabled);
|
||||
return DefaultPreference.set(ELECTRUM_CONNECTION_DISABLED, disabled ? '1' : '');
|
||||
}
|
||||
|
||||
function getCurrentPeer() {
|
||||
|
@ -170,23 +213,31 @@ function getNextPeer() {
|
|||
}
|
||||
|
||||
async function getSavedPeer(): Promise<Peer | null> {
|
||||
const host = await AsyncStorage.getItem(ELECTRUM_HOST);
|
||||
const tcpPort = await AsyncStorage.getItem(ELECTRUM_TCP_PORT);
|
||||
const sslPort = await AsyncStorage.getItem(ELECTRUM_SSL_PORT);
|
||||
try {
|
||||
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
|
||||
const host = (await DefaultPreference.get(ELECTRUM_HOST)) as string;
|
||||
const tcpPort = await DefaultPreference.get(ELECTRUM_TCP_PORT);
|
||||
const sslPort = await DefaultPreference.get(ELECTRUM_SSL_PORT);
|
||||
|
||||
if (!host) {
|
||||
console.log('Getting saved peer:', { host, tcpPort, sslPort });
|
||||
|
||||
if (!host) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (sslPort) {
|
||||
return { host, ssl: Number(sslPort) };
|
||||
}
|
||||
|
||||
if (tcpPort) {
|
||||
return { host, tcp: Number(tcpPort) };
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
console.error('Error in getSavedPeer:', error);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (sslPort) {
|
||||
return { host, ssl: sslPort };
|
||||
}
|
||||
|
||||
if (tcpPort) {
|
||||
return { host, tcp: tcpPort };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function connectMain(): Promise<void> {
|
||||
|
@ -200,22 +251,7 @@ export async function connectMain(): Promise<void> {
|
|||
usingPeer = savedPeer;
|
||||
}
|
||||
|
||||
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
|
||||
try {
|
||||
if (usingPeer.host.endsWith('onion')) {
|
||||
const randomPeer = getCurrentPeer();
|
||||
await DefaultPreference.set(ELECTRUM_HOST, randomPeer.host);
|
||||
await DefaultPreference.set(ELECTRUM_TCP_PORT, randomPeer.tcp ?? '');
|
||||
await DefaultPreference.set(ELECTRUM_SSL_PORT, randomPeer.ssl ?? '');
|
||||
} else {
|
||||
await DefaultPreference.set(ELECTRUM_HOST, usingPeer.host);
|
||||
await DefaultPreference.set(ELECTRUM_TCP_PORT, usingPeer.tcp ?? '');
|
||||
await DefaultPreference.set(ELECTRUM_SSL_PORT, usingPeer.ssl ?? '');
|
||||
}
|
||||
} catch (e) {
|
||||
// Must be running on Android
|
||||
console.log(e);
|
||||
}
|
||||
console.log('Using peer:', JSON.stringify(usingPeer));
|
||||
|
||||
try {
|
||||
console.log('begin connection:', JSON.stringify(usingPeer));
|
||||
|
@ -227,7 +263,8 @@ export async function connectMain(): Promise<void> {
|
|||
// most likely got a timeout from electrum ping. lets reconnect
|
||||
// but only if we were previously connected (mainConnected), otherwise theres other
|
||||
// code which does connection retries
|
||||
mainClient.close();
|
||||
mainClient?.close();
|
||||
mainClient = undefined;
|
||||
mainConnected = false;
|
||||
// dropping `mainConnected` flag ensures there wont be reconnection race condition if several
|
||||
// errors triggered
|
||||
|
@ -275,12 +312,15 @@ export async function connectMain(): Promise<void> {
|
|||
} catch (e) {
|
||||
mainConnected = false;
|
||||
console.log('bad connection:', JSON.stringify(usingPeer), e);
|
||||
mainClient?.close();
|
||||
mainClient = undefined;
|
||||
}
|
||||
|
||||
if (!mainConnected) {
|
||||
console.log('retry');
|
||||
connectionAttempt = connectionAttempt + 1;
|
||||
mainClient.close && mainClient.close();
|
||||
mainClient?.close();
|
||||
mainClient = undefined;
|
||||
if (connectionAttempt >= 5) {
|
||||
presentNetworkErrorAlert(usingPeer);
|
||||
} else {
|
||||
|
@ -291,6 +331,67 @@ export async function connectMain(): Promise<void> {
|
|||
}
|
||||
}
|
||||
|
||||
export async function presentResetToDefaultsAlert(): Promise<boolean> {
|
||||
const hasPreferredServer = await getPreferredServer();
|
||||
const serverHistoryStr = await DefaultPreference.get(ELECTRUM_SERVER_HISTORY);
|
||||
const serverHistory = typeof serverHistoryStr === 'string' ? JSON.parse(serverHistoryStr) : [];
|
||||
return new Promise(resolve => {
|
||||
triggerWarningHapticFeedback();
|
||||
|
||||
const buttons: AlertButton[] = [];
|
||||
|
||||
if (hasPreferredServer?.host && (hasPreferredServer.tcp || hasPreferredServer.ssl)) {
|
||||
buttons.push({
|
||||
text: loc.settings.electrum_reset,
|
||||
onPress: async () => {
|
||||
try {
|
||||
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
|
||||
await DefaultPreference.clear(ELECTRUM_HOST);
|
||||
await DefaultPreference.clear(ELECTRUM_SSL_PORT);
|
||||
await DefaultPreference.clear(ELECTRUM_TCP_PORT);
|
||||
} catch (e) {
|
||||
console.log(e); // Must be running on Android
|
||||
}
|
||||
resolve(true);
|
||||
},
|
||||
style: 'default',
|
||||
});
|
||||
}
|
||||
|
||||
if (serverHistory.length > 0) {
|
||||
buttons.push({
|
||||
text: loc.settings.electrum_reset_to_default_and_clear_history,
|
||||
onPress: async () => {
|
||||
try {
|
||||
await DefaultPreference.setName(GROUP_IO_BLUEWALLET);
|
||||
await DefaultPreference.clear(ELECTRUM_SERVER_HISTORY);
|
||||
await DefaultPreference.clear(ELECTRUM_HOST);
|
||||
await DefaultPreference.clear(ELECTRUM_SSL_PORT);
|
||||
await DefaultPreference.clear(ELECTRUM_TCP_PORT);
|
||||
} catch (e) {
|
||||
console.log(e); // Must be running on Android
|
||||
}
|
||||
resolve(true);
|
||||
},
|
||||
style: 'destructive',
|
||||
});
|
||||
}
|
||||
|
||||
buttons.push({
|
||||
text: loc._.cancel,
|
||||
onPress: () => resolve(false),
|
||||
style: 'cancel',
|
||||
});
|
||||
|
||||
presentAlert({
|
||||
title: loc.settings.electrum_reset,
|
||||
message: loc.settings.electrum_reset_to_default,
|
||||
buttons,
|
||||
options: { cancelable: true },
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const presentNetworkErrorAlert = async (usingPeer?: Peer) => {
|
||||
if (await isDisabled()) {
|
||||
console.log(
|
||||
|
@ -298,6 +399,7 @@ const presentNetworkErrorAlert = async (usingPeer?: Peer) => {
|
|||
);
|
||||
return;
|
||||
}
|
||||
|
||||
presentAlert({
|
||||
allowRepeat: false,
|
||||
title: loc.errors.network,
|
||||
|
@ -310,7 +412,8 @@ const presentNetworkErrorAlert = async (usingPeer?: Peer) => {
|
|||
text: loc.wallets.list_tryagain,
|
||||
onPress: () => {
|
||||
connectionAttempt = 0;
|
||||
mainClient.close() && mainClient.close();
|
||||
mainClient?.close();
|
||||
mainClient = undefined;
|
||||
setTimeout(connectMain, 500);
|
||||
},
|
||||
style: 'default',
|
||||
|
@ -318,39 +421,14 @@ const presentNetworkErrorAlert = async (usingPeer?: Peer) => {
|
|||
{
|
||||
text: loc.settings.electrum_reset,
|
||||
onPress: () => {
|
||||
presentAlert({
|
||||
title: loc.settings.electrum_reset,
|
||||
message: loc.settings.electrum_reset_to_default,
|
||||
buttons: [
|
||||
{
|
||||
text: loc._.cancel,
|
||||
style: 'cancel',
|
||||
onPress: () => {},
|
||||
},
|
||||
{
|
||||
text: loc._.ok,
|
||||
style: 'destructive',
|
||||
onPress: async () => {
|
||||
await AsyncStorage.setItem(ELECTRUM_HOST, '');
|
||||
await AsyncStorage.setItem(ELECTRUM_TCP_PORT, '');
|
||||
await AsyncStorage.setItem(ELECTRUM_SSL_PORT, '');
|
||||
try {
|
||||
await DefaultPreference.setName('group.io.bluewallet.bluewallet');
|
||||
await DefaultPreference.clear(ELECTRUM_HOST);
|
||||
await DefaultPreference.clear(ELECTRUM_SSL_PORT);
|
||||
await DefaultPreference.clear(ELECTRUM_TCP_PORT);
|
||||
} catch (e) {
|
||||
console.log(e); // Must be running on Android
|
||||
}
|
||||
presentAlert({ message: loc.settings.electrum_saved });
|
||||
setTimeout(connectMain, 500);
|
||||
},
|
||||
},
|
||||
],
|
||||
options: { cancelable: true },
|
||||
presentResetToDefaultsAlert().then(result => {
|
||||
if (result) {
|
||||
connectionAttempt = 0;
|
||||
mainClient?.close();
|
||||
mainClient = undefined;
|
||||
setTimeout(connectMain, 500);
|
||||
}
|
||||
});
|
||||
connectionAttempt = 0;
|
||||
mainClient.close() && mainClient.close();
|
||||
},
|
||||
style: 'destructive',
|
||||
},
|
||||
|
@ -358,7 +436,8 @@ const presentNetworkErrorAlert = async (usingPeer?: Peer) => {
|
|||
text: loc._.cancel,
|
||||
onPress: () => {
|
||||
connectionAttempt = 0;
|
||||
mainClient.close() && mainClient.close();
|
||||
mainClient?.close();
|
||||
mainClient = undefined;
|
||||
},
|
||||
style: 'cancel',
|
||||
},
|
||||
|
@ -376,13 +455,18 @@ const presentNetworkErrorAlert = async (usingPeer?: Peer) => {
|
|||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async function getRandomDynamicPeer(): Promise<Peer> {
|
||||
try {
|
||||
let peers = JSON.parse((await AsyncStorage.getItem(storageKey)) as string);
|
||||
let peers = JSON.parse((await DefaultPreference.get(storageKey)) as string);
|
||||
peers = peers.sort(() => Math.random() - 0.5); // shuffle
|
||||
for (const peer of peers) {
|
||||
const ret = {
|
||||
host: peer[1] as string,
|
||||
tcp: '',
|
||||
};
|
||||
const ret: Peer = { host: peer[0], ssl: peer[1] };
|
||||
ret.host = peer[1];
|
||||
|
||||
if (peer[1] === 's') {
|
||||
ret.ssl = peer[2];
|
||||
} else {
|
||||
ret.tcp = peer[2];
|
||||
}
|
||||
|
||||
for (const item of peer[2]) {
|
||||
if (item.startsWith('t')) {
|
||||
ret.tcp = item.replace('t', '');
|
||||
|
@ -398,13 +482,18 @@ async function getRandomDynamicPeer(): Promise<Peer> {
|
|||
}
|
||||
|
||||
export const getBalanceByAddress = async function (address: string): Promise<{ confirmed: number; unconfirmed: number }> {
|
||||
if (!mainClient) throw new Error('Electrum client is not connected');
|
||||
const script = bitcoin.address.toOutputScript(address);
|
||||
const hash = bitcoin.crypto.sha256(script);
|
||||
const reversedHash = Buffer.from(hash).reverse();
|
||||
const balance = await mainClient.blockchainScripthash_getBalance(reversedHash.toString('hex'));
|
||||
balance.addr = address;
|
||||
return balance;
|
||||
try {
|
||||
if (!mainClient) throw new Error('Electrum client is not connected');
|
||||
const script = bitcoin.address.toOutputScript(address);
|
||||
const hash = bitcoin.crypto.sha256(script);
|
||||
const reversedHash = Buffer.from(hash).reverse();
|
||||
const balance = await mainClient.blockchainScripthash_getBalance(reversedHash.toString('hex'));
|
||||
balance.addr = address;
|
||||
return balance;
|
||||
} catch (error) {
|
||||
console.error('Error in getBalanceByAddress:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const getConfig = async function () {
|
||||
|
@ -882,25 +971,29 @@ export async function multiGetTransactionByTxid<T extends boolean>(
|
|||
}
|
||||
|
||||
// saving cache:
|
||||
realm.write(() => {
|
||||
for (const txid of Object.keys(ret)) {
|
||||
const tx = ret[txid];
|
||||
// dont cache immature txs, but only for 'verbose', since its fully decoded tx jsons. non-verbose are just plain
|
||||
// strings txhex
|
||||
if (verbose && typeof tx !== 'string' && (!tx?.confirmations || tx.confirmations < 7)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
realm.write(() => {
|
||||
for (const txid of Object.keys(ret)) {
|
||||
const tx = ret[txid];
|
||||
// dont cache immature txs, but only for 'verbose', since its fully decoded tx jsons. non-verbose are just plain
|
||||
// strings txhex
|
||||
if (verbose && typeof tx !== 'string' && (!tx?.confirmations || tx.confirmations < 7)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
realm.create(
|
||||
'Cache',
|
||||
{
|
||||
cache_key: txid + cacheKeySuffix,
|
||||
cache_value: JSON.stringify(ret[txid]),
|
||||
},
|
||||
Realm.UpdateMode.Modified,
|
||||
);
|
||||
}
|
||||
});
|
||||
realm.create(
|
||||
'Cache',
|
||||
{
|
||||
cache_key: txid + cacheKeySuffix,
|
||||
cache_value: JSON.stringify(ret[txid]),
|
||||
},
|
||||
Realm.UpdateMode.Modified,
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (writeError) {
|
||||
console.error('Failed to write transaction cache:', writeError);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import { Alert, Linking, Platform } from 'react-native';
|
||||
import { Platform } from 'react-native';
|
||||
import DocumentPicker from 'react-native-document-picker';
|
||||
import RNFS from 'react-native-fs';
|
||||
import { launchImageLibrary } from 'react-native-image-picker';
|
||||
import { launchImageLibrary, ImagePickerResponse } from 'react-native-image-picker';
|
||||
import Share from 'react-native-share';
|
||||
import { request, PERMISSIONS } from 'react-native-permissions';
|
||||
import presentAlert from '../components/Alert';
|
||||
import loc from '../loc';
|
||||
import { isDesktop } from './environment';
|
||||
|
@ -16,65 +15,54 @@ const _sanitizeFileName = (fileName: string) => {
|
|||
};
|
||||
|
||||
const _shareOpen = async (filePath: string, showShareDialog: boolean = false) => {
|
||||
return await Share.open({
|
||||
url: 'file://' + filePath,
|
||||
saveToFiles: isDesktop || !showShareDialog,
|
||||
// @ts-ignore: Website claims this propertie exists, but TS cant find it. Send anyways.
|
||||
useInternalStorage: Platform.OS === 'android',
|
||||
failOnCancel: false,
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
// If user cancels sharing, we dont want to show an error. for some reason we get 'CANCELLED' string as error
|
||||
if (error.message !== 'CANCELLED') {
|
||||
presentAlert({ message: error.message });
|
||||
}
|
||||
})
|
||||
.finally(() => {
|
||||
RNFS.unlink(filePath);
|
||||
try {
|
||||
await Share.open({
|
||||
url: 'file://' + filePath,
|
||||
saveToFiles: isDesktop || !showShareDialog,
|
||||
// @ts-ignore: Website claims this propertie exists, but TS cant find it. Send anyways.
|
||||
useInternalStorage: Platform.OS === 'android',
|
||||
failOnCancel: false,
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.log(error);
|
||||
// If user cancels sharing, we dont want to show an error. for some reason we get 'CANCELLED' string as error
|
||||
if (error.message !== 'CANCELLED') {
|
||||
presentAlert({ message: error.message });
|
||||
}
|
||||
} finally {
|
||||
await RNFS.unlink(filePath);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Writes a file to fs, and triggers an OS sharing dialog, so user can decide where to put this file (share to cloud
|
||||
* or perhabs messaging app). Provided filename should be just a file name, NOT a path
|
||||
* or perhaps messaging app). Provided filename should be just a file name, NOT a path
|
||||
*/
|
||||
|
||||
export const writeFileAndExport = async function (fileName: string, contents: string, showShareDialog: boolean = true) {
|
||||
const sanitizedFileName = _sanitizeFileName(fileName);
|
||||
if (Platform.OS === 'ios') {
|
||||
const filePath = RNFS.TemporaryDirectoryPath + `/${sanitizedFileName}`;
|
||||
await RNFS.writeFile(filePath, contents);
|
||||
await _shareOpen(filePath, showShareDialog);
|
||||
} else if (Platform.OS === 'android') {
|
||||
const isAndroidVersion33OrAbove = Platform.Version >= 33;
|
||||
const permissionType = isAndroidVersion33OrAbove ? PERMISSIONS.ANDROID.READ_MEDIA_IMAGES : PERMISSIONS.ANDROID.READ_EXTERNAL_STORAGE;
|
||||
request(permissionType).then(async result => {
|
||||
if (result === 'granted') {
|
||||
const filePath = RNFS.ExternalDirectoryPath + `/${sanitizedFileName}`;
|
||||
try {
|
||||
await RNFS.writeFile(filePath, contents);
|
||||
if (showShareDialog) {
|
||||
await _shareOpen(filePath);
|
||||
} else {
|
||||
presentAlert({ message: loc.formatString(loc.send.file_saved_at_path, { filePath }) });
|
||||
}
|
||||
} catch (e: any) {
|
||||
presentAlert({ message: e.message });
|
||||
try {
|
||||
if (Platform.OS === 'ios') {
|
||||
const filePath = `${RNFS.TemporaryDirectoryPath}/${sanitizedFileName}`;
|
||||
await RNFS.writeFile(filePath, contents);
|
||||
await _shareOpen(filePath, showShareDialog);
|
||||
} else if (Platform.OS === 'android') {
|
||||
const filePath = `${RNFS.DownloadDirectoryPath}/${sanitizedFileName}`;
|
||||
try {
|
||||
await RNFS.writeFile(filePath, contents);
|
||||
if (showShareDialog) {
|
||||
await _shareOpen(filePath);
|
||||
} else {
|
||||
presentAlert({ message: loc.formatString(loc.send.file_saved_at_path, { filePath }) });
|
||||
}
|
||||
} else {
|
||||
Alert.alert(loc.send.permission_storage_title, loc.send.permission_storage_denied_message, [
|
||||
{
|
||||
text: loc.send.open_settings,
|
||||
onPress: () => {
|
||||
Linking.openSettings();
|
||||
},
|
||||
style: 'default',
|
||||
},
|
||||
{ text: loc._.cancel, onPress: () => {}, style: 'cancel' },
|
||||
]);
|
||||
} catch (e: any) {
|
||||
console.error(e);
|
||||
presentAlert({ message: e.message });
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
presentAlert({ message: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -110,41 +98,45 @@ const _readPsbtFileIntoBase64 = async function (uri: string): Promise<string> {
|
|||
} else {
|
||||
// file was a text file, having base64 psbt in there. so we basically have double base64encoded string
|
||||
// thats why we are returning string that was decoded once;
|
||||
// most likely produced by Coldcard
|
||||
// most likely produced by ColdCard
|
||||
return stringData;
|
||||
}
|
||||
};
|
||||
|
||||
export const showImagePickerAndReadImage = (): Promise<string | undefined> => {
|
||||
return new Promise((resolve, reject) =>
|
||||
launchImageLibrary(
|
||||
{
|
||||
mediaType: 'photo',
|
||||
maxHeight: 800,
|
||||
maxWidth: 600,
|
||||
selectionLimit: 1,
|
||||
},
|
||||
response => {
|
||||
if (!response.didCancel) {
|
||||
const asset = response.assets?.[0] ?? {};
|
||||
if (asset.uri) {
|
||||
RNQRGenerator.detect({
|
||||
uri: decodeURI(asset.uri.toString()),
|
||||
})
|
||||
.then(result => {
|
||||
if (result) {
|
||||
resolve(result.values[0]);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
reject(new Error(loc.send.qr_error_no_qrcode));
|
||||
});
|
||||
export const showImagePickerAndReadImage = async (): Promise<string | undefined> => {
|
||||
try {
|
||||
const response: ImagePickerResponse = await launchImageLibrary({
|
||||
mediaType: 'photo',
|
||||
maxHeight: 800,
|
||||
maxWidth: 600,
|
||||
selectionLimit: 1,
|
||||
});
|
||||
|
||||
if (response.didCancel) {
|
||||
return undefined;
|
||||
} else if (response.errorCode) {
|
||||
throw new Error(response.errorMessage);
|
||||
} else if (response.assets) {
|
||||
try {
|
||||
const uri = response.assets[0].uri;
|
||||
if (uri) {
|
||||
const result = await RNQRGenerator.detect({ uri: decodeURI(uri.toString()) });
|
||||
if (result?.values.length > 0) {
|
||||
return result?.values[0];
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
throw new Error(loc.send.qr_error_no_qrcode);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
presentAlert({ message: loc.send.qr_error_no_qrcode });
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const showFilePickerAndReadFile = async function (): Promise<{ data: string | false; uri: string | false }> {
|
||||
|
@ -165,45 +157,24 @@ export const showFilePickerAndReadFile = async function (): Promise<{ data: stri
|
|||
});
|
||||
|
||||
if (!res.fileCopyUri) {
|
||||
// to make ts happy, should not need this check here
|
||||
presentAlert({ message: 'Picking and caching a file failed' });
|
||||
return { data: false, uri: false };
|
||||
}
|
||||
|
||||
const fileCopyUri = decodeURI(res.fileCopyUri);
|
||||
|
||||
let file;
|
||||
if (res.fileCopyUri.toLowerCase().endsWith('.psbt')) {
|
||||
// this is either binary file from ElectrumDesktop OR string file with base64 string in there
|
||||
file = await _readPsbtFileIntoBase64(fileCopyUri);
|
||||
return { data: file, uri: decodeURI(res.fileCopyUri) };
|
||||
const file = await _readPsbtFileIntoBase64(fileCopyUri);
|
||||
return { data: file, uri: fileCopyUri };
|
||||
}
|
||||
|
||||
if (res.type === DocumentPicker.types.images || res.type?.startsWith('image/')) {
|
||||
return new Promise(resolve => {
|
||||
if (!res.fileCopyUri) {
|
||||
// to make ts happy, should not need this check here
|
||||
presentAlert({ message: 'Picking and caching a file failed' });
|
||||
resolve({ data: false, uri: false });
|
||||
return;
|
||||
}
|
||||
const uri2 = res.fileCopyUri.replace('file://', '');
|
||||
|
||||
RNQRGenerator.detect({
|
||||
uri: decodeURI(uri2),
|
||||
})
|
||||
.then(result => {
|
||||
if (result) {
|
||||
resolve({ data: result.values[0], uri: fileCopyUri });
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
resolve({ data: false, uri: false });
|
||||
});
|
||||
});
|
||||
return await handleImageFile(fileCopyUri);
|
||||
}
|
||||
|
||||
file = await RNFS.readFile(fileCopyUri);
|
||||
const file = await RNFS.readFile(fileCopyUri);
|
||||
return { data: file, uri: fileCopyUri };
|
||||
} catch (err: any) {
|
||||
if (!DocumentPicker.isCancel(err)) {
|
||||
|
@ -213,6 +184,33 @@ export const showFilePickerAndReadFile = async function (): Promise<{ data: stri
|
|||
}
|
||||
};
|
||||
|
||||
const handleImageFile = async (fileCopyUri: string): Promise<{ data: string | false; uri: string | false }> => {
|
||||
try {
|
||||
const exists = await RNFS.exists(fileCopyUri);
|
||||
if (!exists) {
|
||||
presentAlert({ message: 'File does not exist' });
|
||||
return { data: false, uri: false };
|
||||
}
|
||||
// First attempt: use original URI
|
||||
let result = await RNQRGenerator.detect({ uri: decodeURI(fileCopyUri) });
|
||||
if (result?.values && result.values.length > 0) {
|
||||
return { data: result.values[0], uri: fileCopyUri };
|
||||
}
|
||||
// Second attempt: remove file:// prefix and try again
|
||||
const altUri = fileCopyUri.replace(/^file:\/\//, '');
|
||||
result = await RNQRGenerator.detect({ uri: decodeURI(altUri) });
|
||||
if (result?.values && result.values.length > 0) {
|
||||
return { data: result.values[0], uri: fileCopyUri };
|
||||
}
|
||||
presentAlert({ message: loc.send.qr_error_no_qrcode });
|
||||
return { data: false, uri: false };
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
presentAlert({ message: loc.send.qr_error_no_qrcode });
|
||||
return { data: false, uri: false };
|
||||
}
|
||||
};
|
||||
|
||||
export const readFileOutsideSandbox = (filePath: string) => {
|
||||
if (Platform.OS === 'ios') {
|
||||
return readFile(filePath);
|
||||
|
|
|
@ -2,10 +2,11 @@ import AsyncStorage from '@react-native-async-storage/async-storage';
|
|||
import PushNotificationIOS from '@react-native-community/push-notification-ios';
|
||||
import { AppState, Platform } from 'react-native';
|
||||
import { getApplicationName, getSystemName, getSystemVersion, getVersion, hasGmsSync, hasHmsSync } from 'react-native-device-info';
|
||||
import { checkNotifications, requestNotifications } from 'react-native-permissions';
|
||||
import { checkNotifications, requestNotifications, RESULTS } from 'react-native-permissions';
|
||||
import PushNotification from 'react-native-push-notification';
|
||||
import loc from '../loc';
|
||||
import { groundControlUri } from './constants';
|
||||
import { fetch } from '../util/fetch';
|
||||
|
||||
const PUSH_TOKEN = 'PUSH_TOKEN';
|
||||
const GROUNDCONTROL_BASE_URI = 'GROUNDCONTROL_BASE_URI';
|
||||
|
@ -14,6 +15,19 @@ export const NOTIFICATIONS_NO_AND_DONT_ASK_FLAG = 'NOTIFICATIONS_NO_AND_DONT_ASK
|
|||
let alreadyConfigured = false;
|
||||
let baseURI = groundControlUri;
|
||||
|
||||
const deepClone = obj => JSON.parse(JSON.stringify(obj));
|
||||
|
||||
const checkAndroidNotificationPermission = async () => {
|
||||
try {
|
||||
const { status } = await checkNotifications();
|
||||
console.debug('Notification permission check:', status);
|
||||
return status === RESULTS.GRANTED;
|
||||
} catch (err) {
|
||||
console.error('Failed to check notification permission:', err);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const checkNotificationPermissionStatus = async () => {
|
||||
try {
|
||||
const { status } = await checkNotifications();
|
||||
|
@ -28,15 +42,14 @@ export const checkNotificationPermissionStatus = async () => {
|
|||
let currentPermissionStatus = 'unavailable';
|
||||
const handleAppStateChange = async nextAppState => {
|
||||
if (nextAppState === 'active') {
|
||||
const newPermissionStatus = await checkNotificationPermissionStatus();
|
||||
if (newPermissionStatus !== currentPermissionStatus) {
|
||||
currentPermissionStatus = newPermissionStatus;
|
||||
if (newPermissionStatus === 'granted') {
|
||||
// Re-initialize notifications if permissions are granted
|
||||
await initializeNotifications();
|
||||
} else {
|
||||
// Optionally, handle the case where permissions are disabled (e.g., disable in-app notifications)
|
||||
console.warn('Notifications have been disabled at the system level.');
|
||||
const isDisabledByUser = (await AsyncStorage.getItem(NOTIFICATIONS_NO_AND_DONT_ASK_FLAG)) === 'true';
|
||||
if (!isDisabledByUser) {
|
||||
const newPermissionStatus = await checkNotificationPermissionStatus();
|
||||
if (newPermissionStatus !== currentPermissionStatus) {
|
||||
currentPermissionStatus = newPermissionStatus;
|
||||
if (newPermissionStatus === 'granted') {
|
||||
await initializeNotifications();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -64,25 +77,34 @@ export const cleanUserOptOutFlag = async () => {
|
|||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
export const tryToObtainPermissions = async () => {
|
||||
if (!isNotificationsCapable) return false;
|
||||
console.debug('tryToObtainPermissions: Starting user-triggered permission request');
|
||||
|
||||
try {
|
||||
const token = await getPushToken();
|
||||
if (token) {
|
||||
if (!alreadyConfigured) {
|
||||
await configureNotifications();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to obtain permissions:', error.message);
|
||||
if (error.code) {
|
||||
console.debug('Error code:', error.code);
|
||||
}
|
||||
if (!isNotificationsCapable) {
|
||||
console.debug('tryToObtainPermissions: Device not capable');
|
||||
return false;
|
||||
}
|
||||
|
||||
return configureNotifications();
|
||||
try {
|
||||
const rationale = {
|
||||
title: loc.settings.notifications,
|
||||
message: loc.notifications.would_you_like_to_receive_notifications,
|
||||
buttonPositive: loc._.ok,
|
||||
buttonNegative: loc.notifications.no_and_dont_ask,
|
||||
};
|
||||
|
||||
const { status } = await requestNotifications(
|
||||
['alert', 'sound', 'badge'],
|
||||
Platform.OS === 'android' && Platform.Version < 33 ? rationale : undefined,
|
||||
);
|
||||
if (status !== RESULTS.GRANTED) {
|
||||
console.debug('tryToObtainPermissions: Permission denied');
|
||||
return false;
|
||||
}
|
||||
return configureNotifications();
|
||||
} catch (error) {
|
||||
console.error('Error requesting notification permissions:', error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
/**
|
||||
* Submits onchain bitcoin addresses and ln invoice preimage hashes to GroundControl server, so later we could
|
||||
|
@ -94,6 +116,12 @@ export const tryToObtainPermissions = async () => {
|
|||
* @returns {Promise<object>} Response object from API rest call
|
||||
*/
|
||||
export const majorTomToGroundControl = async (addresses, hashes, txids) => {
|
||||
console.debug('majorTomToGroundControl: Starting notification registration', {
|
||||
addressCount: addresses?.length,
|
||||
hashCount: hashes?.length,
|
||||
txidCount: txids?.length,
|
||||
});
|
||||
|
||||
try {
|
||||
const noAndDontAskFlag = await AsyncStorage.getItem(NOTIFICATIONS_NO_AND_DONT_ASK_FLAG);
|
||||
if (noAndDontAskFlag === 'true') {
|
||||
|
@ -106,6 +134,7 @@ export const majorTomToGroundControl = async (addresses, hashes, txids) => {
|
|||
}
|
||||
|
||||
const pushToken = await getPushToken();
|
||||
console.debug('majorTomToGroundControl: Retrieved push token:', !!pushToken);
|
||||
if (!pushToken || !pushToken.token || !pushToken.os) {
|
||||
return;
|
||||
}
|
||||
|
@ -120,6 +149,7 @@ export const majorTomToGroundControl = async (addresses, hashes, txids) => {
|
|||
|
||||
let response;
|
||||
try {
|
||||
console.debug('majorTomToGroundControl: Sending request to:', `${baseURI}/majorTomToGroundControl`);
|
||||
response = await fetch(`${baseURI}/majorTomToGroundControl`, {
|
||||
method: 'POST',
|
||||
headers: _getHeaders(),
|
||||
|
@ -160,11 +190,16 @@ export const majorTomToGroundControl = async (addresses, hashes, txids) => {
|
|||
* @returns {Promise<Object>}
|
||||
*/
|
||||
export const checkPermissions = async () => {
|
||||
return new Promise(function (resolve) {
|
||||
PushNotification.checkPermissions(result => {
|
||||
resolve(result);
|
||||
try {
|
||||
return new Promise(function (resolve) {
|
||||
PushNotification.checkPermissions(result => {
|
||||
resolve(result);
|
||||
});
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error checking permissions:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -199,7 +234,7 @@ export const setLevels = async levelAll => {
|
|||
await Promise.all([
|
||||
new Promise(resolve => PushNotification.removeAllDeliveredNotifications(resolve)),
|
||||
new Promise(resolve => PushNotification.setApplicationIconBadgeNumber(0, resolve)),
|
||||
new Promise(resolve => PushNotification.removePendingNotificationRequests(resolve)),
|
||||
new Promise(resolve => PushNotification.cancelAllLocalNotifications(resolve)),
|
||||
AsyncStorage.setItem(NOTIFICATIONS_NO_AND_DONT_ASK_FLAG, 'true'),
|
||||
]);
|
||||
console.debug('Notifications disabled successfully');
|
||||
|
@ -228,12 +263,19 @@ export const addNotification = async notification => {
|
|||
};
|
||||
|
||||
const postTokenConfig = async () => {
|
||||
console.debug('postTokenConfig: Starting token configuration');
|
||||
const pushToken = await getPushToken();
|
||||
if (!pushToken || !pushToken.token || !pushToken.os) return;
|
||||
console.debug('postTokenConfig: Retrieved push token:', !!pushToken);
|
||||
|
||||
if (!pushToken || !pushToken.token || !pushToken.os) {
|
||||
console.debug('postTokenConfig: Invalid token or missing OS info');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const lang = (await AsyncStorage.getItem('lang')) || 'en';
|
||||
const appVersion = getSystemName() + ' ' + getSystemVersion() + ';' + getApplicationName() + ' ' + getVersion();
|
||||
console.debug('postTokenConfig: Posting configuration', { lang, appVersion });
|
||||
|
||||
await fetch(`${baseURI}/setTokenConfiguration`, {
|
||||
method: 'POST',
|
||||
|
@ -253,8 +295,13 @@ const postTokenConfig = async () => {
|
|||
};
|
||||
|
||||
const _setPushToken = async token => {
|
||||
token = JSON.stringify(token);
|
||||
return AsyncStorage.setItem(PUSH_TOKEN, token);
|
||||
try {
|
||||
token = JSON.stringify(token);
|
||||
return await AsyncStorage.setItem(PUSH_TOKEN, token);
|
||||
} catch (error) {
|
||||
console.error('Error setting push token:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -263,103 +310,82 @@ const _setPushToken = async token => {
|
|||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
export const configureNotifications = async onProcessNotifications => {
|
||||
if (alreadyConfigured) {
|
||||
console.debug('configureNotifications: Already configured, skipping');
|
||||
return true;
|
||||
}
|
||||
|
||||
return new Promise(resolve => {
|
||||
const configure = async () => {
|
||||
const existingToken = await getPushToken();
|
||||
if (existingToken) {
|
||||
alreadyConfigured = true;
|
||||
console.debug('Notifications already configured with existing token.');
|
||||
if (__DEV__) {
|
||||
console.debug('Existing Token:', existingToken);
|
||||
}
|
||||
resolve(true);
|
||||
const handleRegistration = async token => {
|
||||
if (__DEV__) {
|
||||
console.debug('configureNotifications: Token received:', token);
|
||||
}
|
||||
alreadyConfigured = true;
|
||||
await _setPushToken(token);
|
||||
resolve(true);
|
||||
};
|
||||
|
||||
const handleNotification = async notification => {
|
||||
// Deep clone to avoid modifying the original object
|
||||
const payload = deepClone({
|
||||
...notification,
|
||||
...notification.data,
|
||||
});
|
||||
|
||||
if (notification.data?.data) {
|
||||
const validData = Object.fromEntries(Object.entries(notification.data.data).filter(([_, value]) => value != null));
|
||||
Object.assign(payload, validData);
|
||||
}
|
||||
payload.data = undefined;
|
||||
|
||||
if (!payload.title && !payload.message) {
|
||||
console.warn('Notification missing required fields:', payload);
|
||||
return;
|
||||
}
|
||||
|
||||
const rationale = {
|
||||
title: loc.settings.notifications,
|
||||
message: loc.notifications.would_you_like_to_receive_notifications,
|
||||
buttonPositive: loc._.ok,
|
||||
buttonNegative: loc.notifications.no_and_dont_ask,
|
||||
};
|
||||
await addNotification(payload);
|
||||
notification.finish(PushNotificationIOS.FetchResult.NoData);
|
||||
|
||||
const requestPermissions = Platform.OS === 'ios';
|
||||
if (payload.foreground && onProcessNotifications) {
|
||||
await onProcessNotifications();
|
||||
}
|
||||
};
|
||||
|
||||
requestNotifications(['alert', 'sound', 'badge'], Platform.OS === 'android' ? rationale : undefined)
|
||||
.then(({ status }) => {
|
||||
if (status === 'granted') {
|
||||
console.debug('Notification permissions granted.');
|
||||
PushNotification.configure({
|
||||
onRegister: async token => {
|
||||
console.debug('TOKEN:', token);
|
||||
if (__DEV__) {
|
||||
console.debug('New Token:', token);
|
||||
}
|
||||
alreadyConfigured = true;
|
||||
await _setPushToken(token);
|
||||
resolve(true);
|
||||
},
|
||||
onNotification: async notification => {
|
||||
// Deep clone to avoid modifying the original notification
|
||||
const payload = structuredClone({
|
||||
...notification,
|
||||
...notification.data,
|
||||
});
|
||||
const configure = async () => {
|
||||
try {
|
||||
const { status } = await checkNotifications();
|
||||
if (status !== RESULTS.GRANTED) {
|
||||
console.debug('configureNotifications: Permissions not granted');
|
||||
return resolve(false);
|
||||
}
|
||||
|
||||
if (notification.data?.data) {
|
||||
// Validate data before merging
|
||||
const validData = {};
|
||||
for (const [key, value] of Object.entries(notification.data.data)) {
|
||||
if (value != null) {
|
||||
validData[key] = value;
|
||||
}
|
||||
}
|
||||
Object.assign(payload, validData);
|
||||
}
|
||||
payload.data = undefined;
|
||||
const existingToken = await getPushToken();
|
||||
if (existingToken) {
|
||||
alreadyConfigured = true;
|
||||
console.debug('Notifications already configured with existing token');
|
||||
return resolve(true);
|
||||
}
|
||||
|
||||
// Ensure required fields exist
|
||||
if (!payload.title && !payload.message) {
|
||||
console.warn('Notification missing required fields:', payload);
|
||||
return;
|
||||
}
|
||||
|
||||
console.debug('Received Push Notification Payload:', payload);
|
||||
|
||||
await addNotification(payload);
|
||||
notification.finish(PushNotificationIOS.FetchResult.NoData);
|
||||
|
||||
if (payload.foreground && onProcessNotifications) {
|
||||
await onProcessNotifications();
|
||||
}
|
||||
},
|
||||
onRegistrationError: err => {
|
||||
console.error(err.message, err);
|
||||
resolve(false);
|
||||
},
|
||||
permissions: { alert: true, badge: true, sound: true },
|
||||
popInitialNotification: true,
|
||||
requestPermissions,
|
||||
});
|
||||
} else {
|
||||
console.warn('Notification permissions not granted.');
|
||||
PushNotification.configure({
|
||||
onRegister: handleRegistration,
|
||||
onNotification: handleNotification,
|
||||
onRegistrationError: error => {
|
||||
console.error('Registration error:', error);
|
||||
resolve(false);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Failed to request notifications permission:', error);
|
||||
resolve(false);
|
||||
},
|
||||
permissions: { alert: true, badge: true, sound: true },
|
||||
popInitialNotification: true,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error in configure:', error);
|
||||
resolve(false);
|
||||
}
|
||||
};
|
||||
|
||||
configure();
|
||||
});
|
||||
};
|
||||
|
||||
const _sleep = async ms => {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates whether the provided GroundControl URI is valid by pinging it.
|
||||
*
|
||||
|
@ -367,15 +393,13 @@ const _sleep = async ms => {
|
|||
* @returns {Promise<boolean>} TRUE if valid, FALSE otherwise
|
||||
*/
|
||||
export const isGroundControlUriValid = async uri => {
|
||||
let response;
|
||||
try {
|
||||
response = await Promise.race([fetch(`${uri}/ping`, { headers: _getHeaders() }), _sleep(2000)]);
|
||||
} catch (_) {}
|
||||
|
||||
if (!response) return false;
|
||||
|
||||
const json = await response.json();
|
||||
return !!json.description;
|
||||
const response = await fetch(`${uri}/ping`, { headers: _getHeaders() });
|
||||
const json = await response.json();
|
||||
return !!json.description;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const isNotificationsCapable = hasGmsSync() || hasHmsSync() || Platform.OS !== 'android';
|
||||
|
@ -401,24 +425,21 @@ const getLevels = async () => {
|
|||
const pushToken = await getPushToken();
|
||||
if (!pushToken || !pushToken.token || !pushToken.os) return;
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await Promise.race([
|
||||
fetch(`${baseURI}/getTokenConfiguration`, {
|
||||
method: 'POST',
|
||||
headers: _getHeaders(),
|
||||
body: JSON.stringify({
|
||||
token: pushToken.token,
|
||||
os: pushToken.os,
|
||||
}),
|
||||
const response = await fetch(`${baseURI}/getTokenConfiguration`, {
|
||||
method: 'POST',
|
||||
headers: _getHeaders(),
|
||||
body: JSON.stringify({
|
||||
token: pushToken.token,
|
||||
os: pushToken.os,
|
||||
}),
|
||||
_sleep(3000),
|
||||
]);
|
||||
} catch (_) {}
|
||||
});
|
||||
|
||||
if (!response) return {};
|
||||
|
||||
return await response.json();
|
||||
if (!response) return {};
|
||||
return await response.json();
|
||||
} catch (_) {
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -481,16 +502,21 @@ export const clearStoredNotifications = async () => {
|
|||
};
|
||||
|
||||
export const getDeliveredNotifications = () => {
|
||||
return new Promise(resolve => {
|
||||
PushNotification.getDeliveredNotifications(notifications => resolve(notifications));
|
||||
});
|
||||
try {
|
||||
return new Promise(resolve => {
|
||||
PushNotification.getDeliveredNotifications(notifications => resolve(notifications));
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error getting delivered notifications:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const removeDeliveredNotifications = (identifiers = []) => {
|
||||
PushNotification.removeDeliveredNotifications(identifiers);
|
||||
};
|
||||
|
||||
export const setApplicationIconBadgeNumber = function (badges) {
|
||||
export const setApplicationIconBadgeNumber = badges => {
|
||||
PushNotification.setApplicationIconBadgeNumber(badges);
|
||||
};
|
||||
|
||||
|
@ -503,12 +529,12 @@ export const getDefaultUri = () => {
|
|||
};
|
||||
|
||||
export const saveUri = async uri => {
|
||||
baseURI = uri || groundControlUri;
|
||||
try {
|
||||
baseURI = uri || groundControlUri;
|
||||
await AsyncStorage.setItem(GROUNDCONTROL_BASE_URI, baseURI);
|
||||
} catch (storageError) {
|
||||
console.error('Failed to reset URI:', storageError);
|
||||
throw storageError;
|
||||
} catch (error) {
|
||||
console.error('Error saving URI:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -531,8 +557,20 @@ export const getSavedUri = async () => {
|
|||
};
|
||||
|
||||
export const isNotificationsEnabled = async () => {
|
||||
const levels = await getLevels();
|
||||
return !!(await getPushToken()) && !!levels.level_all;
|
||||
try {
|
||||
const levels = await getLevels();
|
||||
const token = await getPushToken();
|
||||
const isDisabledByUser = (await AsyncStorage.getItem(NOTIFICATIONS_NO_AND_DONT_ASK_FLAG)) === 'true';
|
||||
|
||||
// Return true only if we have all requirements and user hasn't opted out
|
||||
return !isDisabledByUser && !!token && !!levels.level_all;
|
||||
} catch (error) {
|
||||
console.log('Error checking notification levels:', error);
|
||||
if (error instanceof SyntaxError) {
|
||||
throw error;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const getStoredNotifications = async () => {
|
||||
|
@ -557,8 +595,11 @@ export const getStoredNotifications = async () => {
|
|||
|
||||
// on app launch (load module):
|
||||
export const initializeNotifications = async onProcessNotifications => {
|
||||
console.debug('initializeNotifications: Starting initialization');
|
||||
try {
|
||||
const noAndDontAskFlag = await AsyncStorage.getItem(NOTIFICATIONS_NO_AND_DONT_ASK_FLAG);
|
||||
console.debug('initializeNotifications: No ask flag status:', noAndDontAskFlag);
|
||||
|
||||
if (noAndDontAskFlag === 'true') {
|
||||
console.warn('User has opted out of notifications.');
|
||||
return;
|
||||
|
@ -567,25 +608,37 @@ export const initializeNotifications = async onProcessNotifications => {
|
|||
const baseUriStored = await AsyncStorage.getItem(GROUNDCONTROL_BASE_URI);
|
||||
baseURI = baseUriStored || groundControlUri;
|
||||
console.debug('Base URI set to:', baseURI);
|
||||
} catch (e) {
|
||||
console.error('Failed to load custom URI, falling back to default', e);
|
||||
baseURI = groundControlUri;
|
||||
await AsyncStorage.setItem(GROUNDCONTROL_BASE_URI, groundControlUri).catch(err => console.error('Failed to reset URI:', err));
|
||||
}
|
||||
|
||||
setApplicationIconBadgeNumber(0);
|
||||
setApplicationIconBadgeNumber(0);
|
||||
|
||||
try {
|
||||
// Only check permissions, never request
|
||||
currentPermissionStatus = await checkNotificationPermissionStatus();
|
||||
console.warn('currentPermissionStatus', currentPermissionStatus);
|
||||
if (currentPermissionStatus === 'granted' && (await getPushToken())) {
|
||||
console.debug('Permissions granted and push token exists. Configuring notifications...');
|
||||
await configureNotifications(onProcessNotifications);
|
||||
await postTokenConfig();
|
||||
console.debug('initializeNotifications: Permission status:', currentPermissionStatus);
|
||||
|
||||
// Handle Android 13+ permissions differently
|
||||
const canProceed =
|
||||
Platform.OS === 'android'
|
||||
? isNotificationsCapable && (await checkAndroidNotificationPermission())
|
||||
: currentPermissionStatus === 'granted';
|
||||
|
||||
if (canProceed) {
|
||||
console.debug('initializeNotifications: Can proceed with notification setup');
|
||||
const token = await getPushToken();
|
||||
|
||||
if (token) {
|
||||
console.debug('initializeNotifications: Existing token found, configuring');
|
||||
await configureNotifications(onProcessNotifications);
|
||||
await postTokenConfig();
|
||||
} else {
|
||||
console.debug('initializeNotifications: No token found, will request permissions');
|
||||
await tryToObtainPermissions();
|
||||
}
|
||||
} else {
|
||||
console.warn('Notifications are disabled at the system level.');
|
||||
console.debug('Notifications require user action to enable');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize notifications:', error);
|
||||
baseURI = groundControlUri;
|
||||
await AsyncStorage.setItem(GROUNDCONTROL_BASE_URI, groundControlUri).catch(err => console.error('Failed to reset URI:', err));
|
||||
}
|
||||
};
|
||||
|
|
|
@ -157,7 +157,7 @@
|
|||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||
LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)";
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"",
|
||||
|
@ -209,7 +209,7 @@
|
|||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
|
||||
LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)";
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"\"$(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME)\"",
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import URL from 'url';
|
||||
import { fetch } from '../util/fetch';
|
||||
|
||||
export default class Azteco {
|
||||
/**
|
||||
|
|
|
@ -285,6 +285,7 @@ export class BlueApp {
|
|||
schema,
|
||||
path,
|
||||
encryptionKey,
|
||||
excludeFromIcloudBackup: true,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -327,6 +328,7 @@ export class BlueApp {
|
|||
schema,
|
||||
path,
|
||||
encryptionKey,
|
||||
excludeFromIcloudBackup: true,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import bip21, { TOptions } from 'bip21';
|
||||
import * as bitcoin from 'bitcoinjs-lib';
|
||||
import URL from 'url';
|
||||
|
||||
import { readFileOutsideSandbox } from '../blue_modules/fs';
|
||||
import { Chain } from '../models/bitcoinUnits';
|
||||
import { WatchOnlyWallet } from './';
|
||||
|
@ -87,9 +86,9 @@ class DeeplinkSchemaMatch {
|
|||
} else if (wallet.chain === Chain.OFFCHAIN) {
|
||||
if (action === 'openSend') {
|
||||
completionHandler([
|
||||
'ScanLndInvoiceRoot',
|
||||
'ScanLNDInvoiceRoot',
|
||||
{
|
||||
screen: 'ScanLndInvoice',
|
||||
screen: 'ScanLNDInvoice',
|
||||
params: {
|
||||
walletID: wallet.getID(),
|
||||
},
|
||||
|
@ -157,9 +156,9 @@ class DeeplinkSchemaMatch {
|
|||
]);
|
||||
} else if (DeeplinkSchemaMatch.isLightningInvoice(event.url)) {
|
||||
completionHandler([
|
||||
'ScanLndInvoiceRoot',
|
||||
'ScanLNDInvoiceRoot',
|
||||
{
|
||||
screen: 'ScanLndInvoice',
|
||||
screen: 'ScanLNDInvoice',
|
||||
params: {
|
||||
uri: event.url.replace('://', ':'),
|
||||
},
|
||||
|
@ -182,9 +181,9 @@ class DeeplinkSchemaMatch {
|
|||
// this might be not just an email but a lightning address
|
||||
// @see https://lightningaddress.com
|
||||
completionHandler([
|
||||
'ScanLndInvoiceRoot',
|
||||
'ScanLNDInvoiceRoot',
|
||||
{
|
||||
screen: 'ScanLndInvoice',
|
||||
screen: 'ScanLNDInvoice',
|
||||
params: {
|
||||
uri: event.url,
|
||||
},
|
||||
|
@ -306,9 +305,9 @@ class DeeplinkSchemaMatch {
|
|||
];
|
||||
} else {
|
||||
return [
|
||||
'ScanLndInvoiceRoot',
|
||||
'ScanLNDInvoiceRoot',
|
||||
{
|
||||
screen: 'ScanLndInvoice',
|
||||
screen: 'ScanLNDInvoice',
|
||||
params: {
|
||||
uri: uri.lndInvoice,
|
||||
walletID: wallet.getID(),
|
||||
|
@ -413,6 +412,12 @@ class DeeplinkSchemaMatch {
|
|||
}
|
||||
|
||||
static bip21encode(address: string, options: TOptions): string {
|
||||
// uppercase address if bech32 to satisfy BIP_0173
|
||||
const isBech32 = address.startsWith('bc1');
|
||||
if (isBech32) {
|
||||
address = address.toUpperCase();
|
||||
}
|
||||
|
||||
for (const key in options) {
|
||||
if (key === 'label' && String(options[key]).replace(' ', '').length === 0) {
|
||||
delete options[key];
|
||||
|
|
|
@ -6,6 +6,7 @@ import CryptoJS from 'crypto-js';
|
|||
// @ts-ignore theres no types for secp256k1
|
||||
import secp256k1 from 'secp256k1';
|
||||
import { parse } from 'url'; // eslint-disable-line n/no-deprecated-api
|
||||
import { fetch } from '../util/fetch';
|
||||
|
||||
const ONION_REGEX = /^(http:\/\/[^/:@]+\.onion(?::\d{1,5})?)(\/.*)?$/; // regex for onion URL
|
||||
|
||||
|
|
|
@ -348,11 +348,31 @@ const startImport = (
|
|||
|
||||
// maybe its a watch-only address?
|
||||
yield { progress: 'watch only' };
|
||||
const watchOnly = new WatchOnlyWallet();
|
||||
watchOnly.setSecret(text);
|
||||
if (watchOnly.valid()) {
|
||||
await fetch(watchOnly, true);
|
||||
yield { wallet: watchOnly };
|
||||
const wo1 = new WatchOnlyWallet();
|
||||
wo1.setSecret(text);
|
||||
if (wo1.valid()) {
|
||||
wo1.init();
|
||||
if (text.startsWith('xpub')) {
|
||||
// for xpub we also check ypub and zpub. If any of them was used, we import it.
|
||||
let found = false;
|
||||
const pubs = [text, wo1._xpubToYpub(text), wo1._xpubToZpub(text)];
|
||||
for (const pub of pubs) {
|
||||
const wo2 = new WatchOnlyWallet();
|
||||
wo2.setSecret(pub);
|
||||
wo2.init();
|
||||
if (await wasUsed(wo2)) {
|
||||
yield { wallet: wo2 };
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
await fetch(wo1, true);
|
||||
yield { wallet: wo1 };
|
||||
}
|
||||
} else {
|
||||
await fetch(wo1, true);
|
||||
yield { wallet: wo1 };
|
||||
}
|
||||
}
|
||||
|
||||
// electrum p2wpkh-p2sh
|
||||
|
|
|
@ -310,8 +310,11 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
|
|||
// then we combine it all together
|
||||
|
||||
const addresses2fetch = [];
|
||||
// Store these values to avoid a race condition if fetchBalance func changes them
|
||||
const next_free_address_index = this.next_free_address_index;
|
||||
const next_free_change_address_index = this.next_free_change_address_index;
|
||||
|
||||
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
|
||||
for (let c = 0; c < next_free_address_index + this.gap_limit; c++) {
|
||||
// external addresses first
|
||||
let hasUnconfirmed = false;
|
||||
this._txs_by_external_index[c] = this._txs_by_external_index[c] || [];
|
||||
|
@ -322,7 +325,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
|
|||
}
|
||||
}
|
||||
|
||||
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
|
||||
for (let c = 0; c < next_free_change_address_index + this.gap_limit; c++) {
|
||||
// next, internal addresses
|
||||
let hasUnconfirmed = false;
|
||||
this._txs_by_internal_index[c] = this._txs_by_internal_index[c] || [];
|
||||
|
@ -389,10 +392,10 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
|
|||
|
||||
// now purge all unconfirmed txs from internal hashmaps, since some may be evicted from mempool because they became invalid
|
||||
// or replaced. hashmaps are going to be re-populated anyways, since we fetched TXs for addresses with unconfirmed TXs
|
||||
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
|
||||
for (let c = 0; c < next_free_address_index + this.gap_limit; c++) {
|
||||
this._txs_by_external_index[c] = this._txs_by_external_index[c].filter(tx => !!tx.confirmations);
|
||||
}
|
||||
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
|
||||
for (let c = 0; c < next_free_change_address_index + this.gap_limit; c++) {
|
||||
this._txs_by_internal_index[c] = this._txs_by_internal_index[c].filter(tx => !!tx.confirmations);
|
||||
}
|
||||
for (const pc of this._receive_payment_codes) {
|
||||
|
@ -404,7 +407,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
|
|||
// now, we need to put transactions in all relevant `cells` of internal hashmaps:
|
||||
// this._txs_by_internal_index, this._txs_by_external_index & this._txs_by_payment_code_index
|
||||
|
||||
for (let c = 0; c < this.next_free_address_index + this.gap_limit; c++) {
|
||||
for (let c = 0; c < next_free_address_index + this.gap_limit; c++) {
|
||||
for (const tx of Object.values(txdatas)) {
|
||||
for (const vin of tx.vin) {
|
||||
if (vin.addresses && vin.addresses.indexOf(this._getExternalAddressByIndex(c)) !== -1) {
|
||||
|
@ -445,7 +448,7 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
|
|||
}
|
||||
}
|
||||
|
||||
for (let c = 0; c < this.next_free_change_address_index + this.gap_limit; c++) {
|
||||
for (let c = 0; c < next_free_change_address_index + this.gap_limit; c++) {
|
||||
for (const tx of Object.values(txdatas)) {
|
||||
for (const vin of tx.vin) {
|
||||
if (vin.addresses && vin.addresses.indexOf(this._getInternalAddressByIndex(c)) !== -1) {
|
||||
|
@ -1417,6 +1420,12 @@ export class AbstractHDElectrumWallet extends AbstractHDWallet {
|
|||
if (!this.allowBIP47()) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
// watch-only wallet will throw an error here
|
||||
this.getDerivationPath();
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
// only check BIP47 if derivation path is regular, otherwise too many wallets will be found
|
||||
if (!["m/84'/0'/0'", "m/44'/0'/0'", "m/49'/0'/0'"].includes(this.getDerivationPath() as string)) {
|
||||
return false;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import b58 from 'bs58check';
|
||||
import createHash from 'create-hash';
|
||||
import wif from 'wif';
|
||||
|
||||
import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
|
||||
import { CreateTransactionResult, CreateTransactionUtxo, Transaction, Utxo } from './types';
|
||||
|
@ -211,6 +212,17 @@ export class AbstractWallet {
|
|||
|
||||
setSecret(newSecret: string): this {
|
||||
const origSecret = newSecret;
|
||||
|
||||
// is it minikey https://en.bitcoin.it/wiki/Mini_private_key_format
|
||||
// Starts with S, is 22 length or larger, is base58
|
||||
if (newSecret.startsWith('S') && newSecret.length >= 22 && /^[1-9A-HJ-NP-Za-km-z]+$/.test(newSecret)) {
|
||||
// minikey + ? hashed with SHA256 starts with 0x00 byte
|
||||
if (createHash('sha256').update(`${newSecret}?`).digest('hex').startsWith('00')) {
|
||||
// it is a valid minikey
|
||||
newSecret = wif.encode(0x80, createHash('sha256').update(newSecret).digest(), false);
|
||||
}
|
||||
}
|
||||
|
||||
this.secret = newSecret.trim().replace('bitcoin:', '').replace('BITCOIN:', '');
|
||||
|
||||
if (this.secret.startsWith('BC1')) this.secret = this.secret.toLowerCase();
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import bolt11 from 'bolt11';
|
||||
import { BitcoinUnit, Chain } from '../../models/bitcoinUnits';
|
||||
import { LegacyWallet } from './legacy-wallet';
|
||||
import { fetch } from '../../util/fetch';
|
||||
|
||||
export class LightningCustodianWallet extends LegacyWallet {
|
||||
static readonly type = 'lightningCustodianWallet';
|
||||
|
|
|
@ -629,7 +629,11 @@ export class MultisigHDWallet extends AbstractHDElectrumWallet {
|
|||
hexFingerprint = Buffer.from(hexFingerprint, 'hex').toString('hex');
|
||||
}
|
||||
|
||||
const path = 'm/' + m[1].split('/').slice(1).join('/').replace(/[h]/g, "'");
|
||||
let path = 'm/' + m[1].split('/').slice(1).join('/').replace(/[h]/g, "'");
|
||||
if (path === 'm/') {
|
||||
// not considered valid by Bip32 lib
|
||||
path = 'm/0';
|
||||
}
|
||||
let xpub = m[2];
|
||||
if (xpub.indexOf('/') !== -1) {
|
||||
xpub = xpub.substr(0, xpub.indexOf('/'));
|
||||
|
|
|
@ -79,6 +79,20 @@ export type TransactionOutput = {
|
|||
};
|
||||
};
|
||||
|
||||
export interface DecodedInvoice {
|
||||
destination: string;
|
||||
payment_hash: string;
|
||||
num_satoshis: number;
|
||||
timestamp: number;
|
||||
expiry: number;
|
||||
description: string;
|
||||
description_hash: string;
|
||||
fallback_addr: string;
|
||||
cltv_expiry: string;
|
||||
route_hints: any[];
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export type LightningTransaction = {
|
||||
memo?: string;
|
||||
type?: 'user_invoice' | 'payment_request' | 'bitcoind_tx' | 'paid_invoice';
|
||||
|
@ -88,6 +102,12 @@ export type LightningTransaction = {
|
|||
expire_time?: number;
|
||||
ispaid?: boolean;
|
||||
walletID?: string;
|
||||
value?: number;
|
||||
amt?: number;
|
||||
fee?: number;
|
||||
payment_preimage?: string;
|
||||
payment_request?: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
export type Transaction = {
|
||||
|
|
|
@ -312,4 +312,9 @@ export class WatchOnlyWallet extends LegacyWallet {
|
|||
if (this._hdWalletInstance) return this._hdWalletInstance.isSegwit();
|
||||
return super.isSegwit();
|
||||
}
|
||||
|
||||
wasEverUsed(): Promise<boolean> {
|
||||
if (this._hdWalletInstance) return this._hdWalletInstance.wasEverUsed();
|
||||
return super.wasEverUsed();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import { useTheme } from './themes';
|
|||
import ToolTipMenu from './TooltipMenu';
|
||||
import { CommonToolTipActions } from '../typings/CommonToolTipActions';
|
||||
import loc from '../loc';
|
||||
import { navigationRef } from '../NavigationService';
|
||||
import { useExtendedNavigation } from '../hooks/useExtendedNavigation';
|
||||
|
||||
type AddWalletButtonProps = {
|
||||
onPress?: (event: GestureResponderEvent) => void;
|
||||
|
@ -23,21 +23,25 @@ const styles = StyleSheet.create({
|
|||
|
||||
const AddWalletButton: React.FC<AddWalletButtonProps> = ({ onPress }) => {
|
||||
const { colors } = useTheme();
|
||||
const navigation = useExtendedNavigation();
|
||||
const stylesHook = StyleSheet.create({
|
||||
ball: {
|
||||
backgroundColor: colors.buttonBackgroundColor,
|
||||
},
|
||||
});
|
||||
|
||||
const onPressMenuItem = useCallback((action: string) => {
|
||||
switch (action) {
|
||||
case CommonToolTipActions.ImportWallet.id:
|
||||
navigationRef.current?.navigate('AddWalletRoot', { screen: 'ImportWallet' });
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}, []);
|
||||
const onPressMenuItem = useCallback(
|
||||
(action: string) => {
|
||||
switch (action) {
|
||||
case CommonToolTipActions.ImportWallet.id:
|
||||
navigation.navigate('AddWalletRoot', { screen: 'ImportWallet' });
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
[navigation],
|
||||
);
|
||||
|
||||
const actions = useMemo(() => [CommonToolTipActions.ImportWallet], []);
|
||||
|
||||
|
|
|
@ -1,24 +1,20 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import { Keyboard, StyleSheet, TextInput, View } from 'react-native';
|
||||
import React from 'react';
|
||||
import { StyleProp, StyleSheet, TextInput, View, ViewStyle } from 'react-native';
|
||||
import loc from '../loc';
|
||||
import { AddressInputScanButton } from './AddressInputScanButton';
|
||||
import { useTheme } from './themes';
|
||||
import DeeplinkSchemaMatch from '../class/deeplink-schema-match';
|
||||
import triggerHapticFeedback, { HapticFeedbackTypes } from '../blue_modules/hapticFeedback';
|
||||
|
||||
interface AddressInputProps {
|
||||
isLoading?: boolean;
|
||||
address?: string;
|
||||
placeholder?: string;
|
||||
onChangeText: (text: string) => void;
|
||||
onBarScanned: (ret: { data?: any }) => void;
|
||||
scanButtonTapped?: () => void;
|
||||
launchedBy?: string;
|
||||
editable?: boolean;
|
||||
inputAccessoryViewID?: string;
|
||||
onBlur?: () => void;
|
||||
onFocus?: () => void;
|
||||
onBlur?: () => void;
|
||||
testID?: string;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
keyboardType?:
|
||||
| 'default'
|
||||
| 'numeric'
|
||||
|
@ -41,14 +37,12 @@ const AddressInput = ({
|
|||
testID = 'AddressInput',
|
||||
placeholder = loc.send.details_address,
|
||||
onChangeText,
|
||||
onBarScanned,
|
||||
scanButtonTapped = () => {},
|
||||
launchedBy,
|
||||
editable = true,
|
||||
inputAccessoryViewID,
|
||||
onBlur = () => {},
|
||||
onFocus = () => {},
|
||||
onBlur = () => {},
|
||||
keyboardType = 'default',
|
||||
style,
|
||||
}: AddressInputProps) => {
|
||||
const { colors } = useTheme();
|
||||
const stylesHook = StyleSheet.create({
|
||||
|
@ -62,26 +56,8 @@ const AddressInput = ({
|
|||
},
|
||||
});
|
||||
|
||||
const validateAddressWithFeedback = useCallback((value: string) => {
|
||||
const isBitcoinAddress = DeeplinkSchemaMatch.isBitcoinAddress(value);
|
||||
const isLightningInvoice = DeeplinkSchemaMatch.isLightningInvoice(value);
|
||||
const isValid = isBitcoinAddress || isLightningInvoice;
|
||||
|
||||
triggerHapticFeedback(isValid ? HapticFeedbackTypes.NotificationSuccess : HapticFeedbackTypes.NotificationError);
|
||||
return {
|
||||
isValid,
|
||||
type: isBitcoinAddress ? 'bitcoin' : isLightningInvoice ? 'lightning' : 'invalid',
|
||||
};
|
||||
}, []);
|
||||
|
||||
const onBlurEditing = () => {
|
||||
validateAddressWithFeedback(address);
|
||||
onBlur();
|
||||
Keyboard.dismiss();
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={[styles.root, stylesHook.root]}>
|
||||
<View style={[styles.root, stylesHook.root, style]}>
|
||||
<TextInput
|
||||
testID={testID}
|
||||
onChangeText={onChangeText}
|
||||
|
@ -93,21 +69,13 @@ const AddressInput = ({
|
|||
multiline={!editable}
|
||||
inputAccessoryViewID={inputAccessoryViewID}
|
||||
clearButtonMode="while-editing"
|
||||
onBlur={onBlurEditing}
|
||||
onFocus={onFocus}
|
||||
autoCapitalize="none"
|
||||
autoCorrect={false}
|
||||
keyboardType={keyboardType}
|
||||
onBlur={onBlur}
|
||||
/>
|
||||
{editable ? (
|
||||
<AddressInputScanButton
|
||||
isLoading={isLoading}
|
||||
launchedBy={launchedBy}
|
||||
scanButtonTapped={scanButtonTapped}
|
||||
onBarScanned={onBarScanned}
|
||||
onChangeText={onChangeText}
|
||||
/>
|
||||
) : null}
|
||||
{editable ? <AddressInputScanButton isLoading={isLoading} onChangeText={onChangeText} /> : null}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
@ -120,8 +88,6 @@ const styles = StyleSheet.create({
|
|||
minHeight: 44,
|
||||
height: 44,
|
||||
alignItems: 'center',
|
||||
marginVertical: 8,
|
||||
marginHorizontal: 18,
|
||||
borderRadius: 4,
|
||||
},
|
||||
input: {
|
||||
|
|
|
@ -3,31 +3,33 @@ import { Image, Keyboard, Platform, StyleSheet, Text } from 'react-native';
|
|||
import Clipboard from '@react-native-clipboard/clipboard';
|
||||
import ToolTipMenu from './TooltipMenu';
|
||||
import loc from '../loc';
|
||||
import { scanQrHelper } from '../helpers/scan-qr';
|
||||
import { showFilePickerAndReadFile, showImagePickerAndReadImage } from '../blue_modules/fs';
|
||||
import presentAlert from './Alert';
|
||||
import { useTheme } from './themes';
|
||||
import RNQRGenerator from 'rn-qr-generator';
|
||||
import { CommonToolTipActions } from '../typings/CommonToolTipActions';
|
||||
import { useSettings } from '../hooks/context/useSettings';
|
||||
import { useExtendedNavigation } from '../hooks/useExtendedNavigation';
|
||||
|
||||
interface AddressInputScanButtonProps {
|
||||
isLoading: boolean;
|
||||
launchedBy?: string;
|
||||
scanButtonTapped: () => void;
|
||||
onBarScanned: (ret: { data?: any }) => void;
|
||||
isLoading?: boolean;
|
||||
onChangeText: (text: string) => void;
|
||||
type?: 'default' | 'link';
|
||||
testID?: string;
|
||||
beforePress?: () => Promise<void> | void;
|
||||
}
|
||||
|
||||
export const AddressInputScanButton = ({
|
||||
isLoading,
|
||||
launchedBy,
|
||||
scanButtonTapped,
|
||||
onBarScanned,
|
||||
onChangeText,
|
||||
type = 'default',
|
||||
testID = 'BlueAddressInputScanQrButton',
|
||||
beforePress,
|
||||
}: AddressInputScanButtonProps) => {
|
||||
const { colors } = useTheme();
|
||||
const { isClipboardGetContentEnabled } = useSettings();
|
||||
|
||||
const navigation = useExtendedNavigation();
|
||||
const stylesHook = StyleSheet.create({
|
||||
scan: {
|
||||
backgroundColor: colors.scanLabel,
|
||||
|
@ -38,14 +40,17 @@ export const AddressInputScanButton = ({
|
|||
});
|
||||
|
||||
const toolTipOnPress = useCallback(async () => {
|
||||
await scanButtonTapped();
|
||||
if (beforePress) {
|
||||
await beforePress();
|
||||
}
|
||||
Keyboard.dismiss();
|
||||
if (launchedBy) scanQrHelper(launchedBy, true).then(value => onBarScanned({ data: value }));
|
||||
}, [launchedBy, onBarScanned, scanButtonTapped]);
|
||||
navigation.navigate('ScanQRCode', {
|
||||
showFileImportButton: true,
|
||||
});
|
||||
}, [navigation, beforePress]);
|
||||
|
||||
const actions = useMemo(() => {
|
||||
const availableActions = [
|
||||
CommonToolTipActions.ScanQR,
|
||||
CommonToolTipActions.ChoosePhoto,
|
||||
CommonToolTipActions.ImportFile,
|
||||
{
|
||||
|
@ -59,18 +64,11 @@ export const AddressInputScanButton = ({
|
|||
|
||||
const onMenuItemPressed = useCallback(
|
||||
async (action: string) => {
|
||||
if (onBarScanned === undefined) throw new Error('onBarScanned is required');
|
||||
switch (action) {
|
||||
case CommonToolTipActions.ScanQR.id:
|
||||
scanButtonTapped();
|
||||
if (launchedBy) {
|
||||
scanQrHelper(launchedBy)
|
||||
.then(value => onBarScanned({ data: value }))
|
||||
.catch(error => {
|
||||
presentAlert({ message: error.message });
|
||||
});
|
||||
}
|
||||
|
||||
navigation.navigate('ScanQRCode', {
|
||||
showFileImportButton: true,
|
||||
});
|
||||
break;
|
||||
case CommonToolTipActions.PasteFromClipboard.id:
|
||||
try {
|
||||
|
@ -88,8 +86,7 @@ export const AddressInputScanButton = ({
|
|||
|
||||
if (getImage) {
|
||||
try {
|
||||
const base64Data = getImage.replace(/^data:image\/jpeg;base64,/, '');
|
||||
|
||||
const base64Data = getImage.replace(/^data:image\/(png|jpeg|jpg);base64,/, '');
|
||||
const values = await RNQRGenerator.detect({
|
||||
base64: base64Data,
|
||||
});
|
||||
|
@ -135,7 +132,7 @@ export const AddressInputScanButton = ({
|
|||
}
|
||||
Keyboard.dismiss();
|
||||
},
|
||||
[launchedBy, onBarScanned, onChangeText, scanButtonTapped],
|
||||
[navigation, onChangeText],
|
||||
);
|
||||
|
||||
const buttonStyle = useMemo(() => [styles.scan, stylesHook.scan], [stylesHook.scan]);
|
||||
|
@ -145,21 +142,29 @@ export const AddressInputScanButton = ({
|
|||
actions={actions}
|
||||
isButton
|
||||
onPressMenuItem={onMenuItemPressed}
|
||||
testID="BlueAddressInputScanQrButton"
|
||||
testID={testID}
|
||||
disabled={isLoading}
|
||||
onPress={toolTipOnPress}
|
||||
buttonStyle={buttonStyle}
|
||||
buttonStyle={type === 'default' ? buttonStyle : undefined}
|
||||
accessibilityLabel={loc.send.details_scan}
|
||||
accessibilityHint={loc.send.details_scan_hint}
|
||||
>
|
||||
<Image source={require('../img/scan-white.png')} accessible={false} />
|
||||
<Text style={[styles.scanText, stylesHook.scanText]} accessible={false}>
|
||||
{loc.send.details_scan}
|
||||
</Text>
|
||||
{type === 'default' ? (
|
||||
<>
|
||||
<Image source={require('../img/scan-white.png')} accessible={false} />
|
||||
<Text style={[styles.scanText, stylesHook.scanText]} accessible={false}>
|
||||
{loc.send.details_scan}
|
||||
</Text>
|
||||
</>
|
||||
) : (
|
||||
<Text style={[styles.linkText, { color: colors.foregroundColor }]}>{loc.wallets.import_scan_qr}</Text>
|
||||
)}
|
||||
</ToolTipMenu>
|
||||
);
|
||||
};
|
||||
|
||||
AddressInputScanButton.displayName = 'AddressInputScanButton';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
scan: {
|
||||
height: 36,
|
||||
|
@ -174,4 +179,8 @@ const styles = StyleSheet.create({
|
|||
scanText: {
|
||||
marginLeft: 4,
|
||||
},
|
||||
linkText: {
|
||||
textAlign: 'center',
|
||||
fontSize: 16,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { Alert as RNAlert, Platform, ToastAndroid, AlertButton, AlertOptions } from 'react-native';
|
||||
import triggerHapticFeedback, { HapticFeedbackTypes } from '../blue_modules/hapticFeedback';
|
||||
import loc from '../loc';
|
||||
import { navigationRef } from '../NavigationService';
|
||||
|
||||
export enum AlertType {
|
||||
Alert,
|
||||
|
@ -22,7 +23,7 @@ const presentAlert = (() => {
|
|||
};
|
||||
|
||||
const showAlert = (title: string | undefined, message: string, buttons: AlertButton[], options: AlertOptions) => {
|
||||
if (Platform.OS === 'ios') {
|
||||
if (Platform.OS === 'ios' && navigationRef.isReady()) {
|
||||
RNAlert.alert(title ?? message, title && message ? message : undefined, buttons, options);
|
||||
} else {
|
||||
RNAlert.alert(title ?? '', message, buttons, options);
|
||||
|
|
|
@ -3,7 +3,7 @@ import dayjs from 'dayjs';
|
|||
import localizedFormat from 'dayjs/plugin/localizedFormat';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { Image, LayoutAnimation, Pressable, StyleSheet, TextInput, TouchableOpacity, TouchableWithoutFeedback, View } from 'react-native';
|
||||
import { Image, LayoutAnimation, Pressable, StyleSheet, TextInput, TouchableOpacity, View } from 'react-native';
|
||||
import { Badge, Icon, Text } from '@rneui/themed';
|
||||
|
||||
import {
|
||||
|
@ -146,7 +146,9 @@ class AmountInput extends Component {
|
|||
textInput = React.createRef();
|
||||
|
||||
handleTextInputOnPress = () => {
|
||||
this.textInput.current.focus();
|
||||
if (this.textInput && this.textInput.current && typeof this.textInput.current.focus === 'function') {
|
||||
this.textInput.current.focus();
|
||||
}
|
||||
};
|
||||
|
||||
handleChangeText = text => {
|
||||
|
@ -254,11 +256,15 @@ class AmountInput extends Component {
|
|||
});
|
||||
|
||||
return (
|
||||
<TouchableWithoutFeedback
|
||||
<Pressable
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={loc._.enter_amount}
|
||||
disabled={this.props.pointerEvents === 'none'}
|
||||
onPress={() => this.textInput.focus()}
|
||||
onPress={() => {
|
||||
if (this.textInput && this.textInput.current && typeof this.textInput.current.focus === 'function') {
|
||||
this.textInput.current.focus();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<>
|
||||
<View style={styles.root}>
|
||||
|
@ -340,7 +346,7 @@ class AmountInput extends Component {
|
|||
</View>
|
||||
)}
|
||||
</>
|
||||
</TouchableWithoutFeedback>
|
||||
</Pressable>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
import React, { forwardRef, useImperativeHandle, useRef, ReactElement, ComponentType, ReactNode } from 'react';
|
||||
import { SheetSize, SizeInfo, TrueSheet, TrueSheetProps } from '@lodev09/react-native-true-sheet';
|
||||
import { Keyboard, StyleSheet, View, TouchableOpacity, Platform, GestureResponderEvent, Text } from 'react-native';
|
||||
import Ionicons from 'react-native-vector-icons/Ionicons';
|
||||
import React, { forwardRef, useImperativeHandle, useRef, ReactElement, ComponentType } from 'react';
|
||||
import { SheetSize, SizeChangeEvent, TrueSheet, TrueSheetProps } from '@lodev09/react-native-true-sheet';
|
||||
import { Keyboard, Image, StyleSheet, View, TouchableOpacity, Platform, GestureResponderEvent, Text } from 'react-native';
|
||||
import SaveFileButton from './SaveFileButton';
|
||||
import { useTheme } from './themes';
|
||||
import { Image } from '@rneui/base';
|
||||
import { Icon } from '@rneui/base';
|
||||
|
||||
interface BottomModalProps extends TrueSheetProps {
|
||||
children?: React.ReactNode;
|
||||
|
@ -15,7 +14,7 @@ interface BottomModalProps extends TrueSheetProps {
|
|||
footer?: ReactElement | ComponentType<any> | null;
|
||||
footerDefaultMargins?: boolean | number;
|
||||
onPresent?: () => void;
|
||||
onSizeChange?: (size: SizeInfo) => void;
|
||||
onSizeChange?: (event: SizeChangeEvent) => void;
|
||||
showCloseButton?: boolean;
|
||||
shareContent?: BottomModalShareContent;
|
||||
shareButtonOnPress?: (event: GestureResponderEvent) => void;
|
||||
|
@ -57,7 +56,7 @@ const BottomModal = forwardRef<BottomModalHandle, BottomModalProps>(
|
|||
ref,
|
||||
) => {
|
||||
const trueSheetRef = useRef<TrueSheet>(null);
|
||||
const { colors } = useTheme();
|
||||
const { colors, closeImage } = useTheme();
|
||||
const stylesHook = StyleSheet.create({
|
||||
barButton: {
|
||||
backgroundColor: colors.lightButton,
|
||||
|
@ -107,7 +106,12 @@ const BottomModal = forwardRef<BottomModalHandle, BottomModalProps>(
|
|||
testID="ModalShareButton"
|
||||
key="ModalShareButton"
|
||||
>
|
||||
<Ionicons name="share" size={20} color={colors.buttonTextColor} />
|
||||
<Icon
|
||||
name={Platform.OS === 'android' ? 'share' : 'file-upload'}
|
||||
type="font-awesome6"
|
||||
size={20}
|
||||
color={colors.buttonTextColor}
|
||||
/>
|
||||
</SaveFileButton>,
|
||||
);
|
||||
} else if (shareButtonOnPress) {
|
||||
|
@ -118,7 +122,12 @@ const BottomModal = forwardRef<BottomModalHandle, BottomModalProps>(
|
|||
style={[styles.topRightButton, stylesHook.barButton]}
|
||||
onPress={shareButtonOnPress}
|
||||
>
|
||||
<Ionicons name="share" size={20} color={colors.buttonTextColor} />
|
||||
<Icon
|
||||
name={Platform.OS === 'android' ? 'share' : 'file-upload'}
|
||||
type="font-awesome6"
|
||||
size={20}
|
||||
color={colors.buttonTextColor}
|
||||
/>
|
||||
</TouchableOpacity>,
|
||||
);
|
||||
}
|
||||
|
@ -131,11 +140,7 @@ const BottomModal = forwardRef<BottomModalHandle, BottomModalProps>(
|
|||
key="ModalDoneButton"
|
||||
testID="ModalDoneButton"
|
||||
>
|
||||
{Platform.OS === 'ios' ? (
|
||||
<Ionicons name="close" size={20} color={colors.buttonTextColor} />
|
||||
) : (
|
||||
<Image source={require('../img/close.png')} style={styles.closeButton} />
|
||||
)}
|
||||
<Image source={closeImage} />
|
||||
</TouchableOpacity>,
|
||||
);
|
||||
}
|
||||
|
@ -155,18 +160,26 @@ const BottomModal = forwardRef<BottomModalHandle, BottomModalProps>(
|
|||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.headerContainer}>
|
||||
<View style={styles.headerContent}>{typeof header === 'function' ? <header /> : header}</View>
|
||||
{renderTopRightButton()}
|
||||
</View>
|
||||
);
|
||||
if (showCloseButton || shareContent)
|
||||
return (
|
||||
<View style={styles.headerContainer}>
|
||||
<View style={styles.headerContent}>{typeof header === 'function' ? <header /> : header}</View>
|
||||
{renderTopRightButton()}
|
||||
</View>
|
||||
);
|
||||
|
||||
if (React.isValidElement(header)) {
|
||||
return (
|
||||
<View style={styles.headerContainerWithCloseButton}>
|
||||
{header}
|
||||
{renderTopRightButton()}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const renderFooter = (): ReactElement | undefined => {
|
||||
// Footer is not working correctly on Android yet.
|
||||
if (!footer) return undefined;
|
||||
|
||||
if (React.isValidElement(footer)) {
|
||||
return footerDefaultMargins ? <View style={styles.footerContainer}>{footer}</View> : footer;
|
||||
} else if (typeof footer === 'function') {
|
||||
|
@ -177,7 +190,7 @@ const BottomModal = forwardRef<BottomModalHandle, BottomModalProps>(
|
|||
return undefined;
|
||||
};
|
||||
|
||||
const FooterComponent = Platform.OS !== 'android' && renderFooter();
|
||||
const FooterComponent = renderFooter();
|
||||
|
||||
return (
|
||||
<TrueSheet
|
||||
|
@ -191,7 +204,6 @@ const BottomModal = forwardRef<BottomModalHandle, BottomModalProps>(
|
|||
{...props}
|
||||
>
|
||||
<View style={styles.childrenContainer}>{children}</View>
|
||||
{Platform.OS === 'android' && (renderFooter() as ReactNode)}
|
||||
{renderHeader()}
|
||||
</TrueSheet>
|
||||
);
|
||||
|
@ -214,6 +226,17 @@ const styles = StyleSheet.create({
|
|||
right: 16,
|
||||
top: 16,
|
||||
},
|
||||
headerContainerWithCloseButton: {
|
||||
position: 'absolute',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 8,
|
||||
minHeight: 22,
|
||||
width: '100%',
|
||||
top: 16,
|
||||
left: 0,
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
headerContent: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
|
@ -223,10 +246,6 @@ const styles = StyleSheet.create({
|
|||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
closeButton: {
|
||||
width: 10,
|
||||
height: 10,
|
||||
},
|
||||
headerSubtitle: {
|
||||
fontSize: 14,
|
||||
},
|
||||
|
|
268
components/CameraScreen.tsx
Normal file
|
@ -0,0 +1,268 @@
|
|||
import React, { useState, useRef } from 'react';
|
||||
import { Animated, Platform, StyleSheet, TouchableOpacity, View } from 'react-native';
|
||||
// @ts-ignore: no declaration file yet
|
||||
import { Camera, CameraApi, CameraType, Orientation } from 'react-native-camera-kit';
|
||||
import loc from '../loc';
|
||||
import { Icon } from '@rneui/base';
|
||||
import { triggerSelectionHapticFeedback } from '../blue_modules/hapticFeedback';
|
||||
import { isDesktop } from '../blue_modules/environment';
|
||||
// @ts-ignore: no declaration file yet
|
||||
import { OnOrientationChangeData, OnReadCodeData } from 'react-native-camera-kit/dist/CameraProps';
|
||||
|
||||
interface CameraScreenProps {
|
||||
onCancelButtonPress: () => void;
|
||||
showImagePickerButton?: boolean;
|
||||
showFilePickerButton?: boolean;
|
||||
onImagePickerButtonPress?: () => void;
|
||||
onFilePickerButtonPress?: () => void;
|
||||
onReadCode?: (event: OnReadCodeData) => void;
|
||||
}
|
||||
|
||||
const CameraScreen: React.FC<CameraScreenProps> = ({
|
||||
onCancelButtonPress,
|
||||
showImagePickerButton,
|
||||
showFilePickerButton,
|
||||
onImagePickerButtonPress,
|
||||
onFilePickerButtonPress,
|
||||
onReadCode,
|
||||
}) => {
|
||||
const cameraRef = useRef<CameraApi>(null);
|
||||
const [torchMode, setTorchMode] = useState(false);
|
||||
const [cameraType, setCameraType] = useState(CameraType.Back);
|
||||
const [zoom, setZoom] = useState<number | undefined>();
|
||||
const [orientationAnim] = useState(new Animated.Value(3));
|
||||
|
||||
const onSwitchCameraPressed = () => {
|
||||
const direction = cameraType === CameraType.Back ? CameraType.Front : CameraType.Back;
|
||||
setCameraType(direction);
|
||||
setZoom(1); // When changing camera type, reset to default zoom for that camera
|
||||
triggerSelectionHapticFeedback();
|
||||
};
|
||||
|
||||
const onSetTorch = () => {
|
||||
setTorchMode(!torchMode);
|
||||
triggerSelectionHapticFeedback();
|
||||
};
|
||||
|
||||
// Counter-rotate the icons to indicate the actual orientation of the captured photo.
|
||||
// For this example, it'll behave incorrectly since UI orientation is allowed (and already-counter rotates the entire screen)
|
||||
// For real phone apps, lock your UI orientation using a library like 'react-native-orientation-locker'
|
||||
const rotateUi = true;
|
||||
const uiRotation = orientationAnim.interpolate({
|
||||
inputRange: [1, 2, 3, 4],
|
||||
outputRange: ['180deg', '90deg', '0deg', '-90deg'],
|
||||
});
|
||||
const uiRotationStyle = rotateUi ? { transform: [{ rotate: uiRotation }] } : {};
|
||||
|
||||
function rotateUiTo(rotationValue: number) {
|
||||
Animated.timing(orientationAnim, {
|
||||
toValue: rotationValue,
|
||||
useNativeDriver: true,
|
||||
duration: 200,
|
||||
isInteraction: false,
|
||||
}).start();
|
||||
}
|
||||
|
||||
const handleZoom = (e: { nativeEvent: { zoom: number } }) => {
|
||||
console.debug('zoom', e.nativeEvent.zoom);
|
||||
setZoom(e.nativeEvent.zoom);
|
||||
};
|
||||
|
||||
const handleOrientationChange = (e: OnOrientationChangeData) => {
|
||||
switch (e.nativeEvent.orientation) {
|
||||
case Orientation.PORTRAIT_UPSIDE_DOWN:
|
||||
console.debug('orientationChange', 'PORTRAIT_UPSIDE_DOWN');
|
||||
rotateUiTo(1);
|
||||
break;
|
||||
case Orientation.LANDSCAPE_LEFT:
|
||||
console.debug('orientationChange', 'LANDSCAPE_LEFT');
|
||||
rotateUiTo(2);
|
||||
break;
|
||||
case Orientation.PORTRAIT:
|
||||
console.debug('orientationChange', 'PORTRAIT');
|
||||
rotateUiTo(3);
|
||||
break;
|
||||
case Orientation.LANDSCAPE_RIGHT:
|
||||
console.debug('orientationChange', 'LANDSCAPE_RIGHT');
|
||||
rotateUiTo(4);
|
||||
break;
|
||||
default:
|
||||
console.debug('orientationChange', e.nativeEvent);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const handleReadCode = (event: OnReadCodeData) => {
|
||||
onReadCode?.(event);
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={styles.screen}>
|
||||
{/* Render top buttons only if not desktop as they would not be relevant */}
|
||||
{!isDesktop && (
|
||||
<View style={styles.topButtons}>
|
||||
<TouchableOpacity style={[styles.topButton, uiRotationStyle, torchMode ? styles.activeTorch : {}]} onPress={onSetTorch}>
|
||||
<Animated.View style={styles.topButtonImg}>
|
||||
{Platform.OS === 'ios' ? (
|
||||
<Icon name={torchMode ? 'flashlight-on' : 'flashlight-off'} type="font-awesome-6" color={torchMode ? '#000' : '#fff'} />
|
||||
) : (
|
||||
<Icon name={torchMode ? 'flash-on' : 'flash-off'} type="ionicons" color={torchMode ? '#000' : '#fff'} />
|
||||
)}
|
||||
</Animated.View>
|
||||
</TouchableOpacity>
|
||||
<View style={styles.rightButtonsContainer}>
|
||||
{showImagePickerButton && (
|
||||
<TouchableOpacity
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={loc._.pick_image}
|
||||
style={[styles.topButton, styles.spacing, uiRotationStyle]}
|
||||
onPress={onImagePickerButtonPress}
|
||||
>
|
||||
<Animated.View style={styles.topButtonImg}>
|
||||
<Icon name="image" type="font-awesome" color="#ffffff" />
|
||||
</Animated.View>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{showFilePickerButton && (
|
||||
<TouchableOpacity
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={loc._.pick_file}
|
||||
style={[styles.topButton, styles.spacing, uiRotationStyle]}
|
||||
onPress={onFilePickerButtonPress}
|
||||
>
|
||||
<Animated.View style={styles.topButtonImg}>
|
||||
<Icon name="file-import" type="font-awesome-5" color="#ffffff" />
|
||||
</Animated.View>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
<View style={styles.cameraContainer}>
|
||||
<Camera
|
||||
ref={cameraRef}
|
||||
style={styles.cameraPreview}
|
||||
cameraType={cameraType}
|
||||
scanBarcode
|
||||
resizeMode="cover"
|
||||
onReadCode={handleReadCode}
|
||||
torchMode={torchMode ? 'on' : 'off'}
|
||||
resetFocusWhenMotionDetected
|
||||
zoom={zoom}
|
||||
onZoom={handleZoom}
|
||||
maxZoom={10}
|
||||
onOrientationChange={handleOrientationChange}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.bottomButtons}>
|
||||
<TouchableOpacity onPress={onCancelButtonPress}>
|
||||
<Animated.Text style={[styles.backTextStyle, uiRotationStyle]}>{loc._.cancel}</Animated.Text>
|
||||
</TouchableOpacity>
|
||||
{isDesktop ? (
|
||||
<View style={styles.rightButtonsContainer}>
|
||||
{showImagePickerButton && (
|
||||
<TouchableOpacity
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={loc._.pick_image}
|
||||
style={[styles.bottomButton, styles.spacing, uiRotationStyle]}
|
||||
onPress={onImagePickerButtonPress}
|
||||
>
|
||||
<Animated.View style={styles.topButtonImg}>
|
||||
<Icon name="image" type="font-awesome" color="#ffffff" />
|
||||
</Animated.View>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
{showFilePickerButton && (
|
||||
<TouchableOpacity
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={loc._.pick_file}
|
||||
style={[styles.bottomButton, styles.spacing, uiRotationStyle]}
|
||||
onPress={onFilePickerButtonPress}
|
||||
>
|
||||
<Animated.View style={styles.topButtonImg}>
|
||||
<Icon name="file-import" type="font-awesome-5" color="#ffffff" />
|
||||
</Animated.View>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
) : (
|
||||
<TouchableOpacity style={[styles.bottomButton, uiRotationStyle]} onPress={onSwitchCameraPressed}>
|
||||
<Animated.View style={[styles.topButtonImg, uiRotationStyle]}>
|
||||
{Platform.OS === 'ios' ? (
|
||||
<Icon name="cameraswitch" type="font-awesome-6" color="#ffffff" />
|
||||
) : (
|
||||
<Icon name={cameraType === CameraType.Back ? 'camera-rear' : 'camera-front'} type="ionicons" color="#ffffff" />
|
||||
)}
|
||||
</Animated.View>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default CameraScreen;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
activeTorch: {
|
||||
backgroundColor: '#fff',
|
||||
},
|
||||
screen: {
|
||||
height: '100%',
|
||||
backgroundColor: '#000000',
|
||||
},
|
||||
topButtons: {
|
||||
padding: 10,
|
||||
zIndex: 10,
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
topButton: {
|
||||
backgroundColor: '#222',
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: 22,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
topButtonImg: {
|
||||
margin: 10,
|
||||
width: 24,
|
||||
height: 24,
|
||||
},
|
||||
cameraContainer: {
|
||||
justifyContent: 'center',
|
||||
flex: 1,
|
||||
},
|
||||
cameraPreview: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
bottomButtons: {
|
||||
padding: 10,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
backTextStyle: {
|
||||
padding: 10,
|
||||
color: 'white',
|
||||
fontSize: 20,
|
||||
},
|
||||
rightButtonsContainer: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
},
|
||||
bottomButton: {
|
||||
backgroundColor: '#222',
|
||||
width: 44,
|
||||
height: 44,
|
||||
borderRadius: 22,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
marginLeft: 10,
|
||||
},
|
||||
spacing: {
|
||||
marginLeft: 20,
|
||||
},
|
||||
});
|
|
@ -1,14 +1,15 @@
|
|||
import React, { createContext, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { InteractionManager } from 'react-native';
|
||||
import { InteractionManager, LayoutAnimation } from 'react-native';
|
||||
import A from '../../blue_modules/analytics';
|
||||
import { BlueApp as BlueAppClass, LegacyWallet, TCounterpartyMetadata, TTXMetadata, WatchOnlyWallet } from '../../class';
|
||||
import type { TWallet } from '../../class/wallets/types';
|
||||
import presentAlert from '../../components/Alert';
|
||||
import loc from '../../loc';
|
||||
import loc, { formatBalanceWithoutSuffix } from '../../loc';
|
||||
import * as BlueElectrum from '../../blue_modules/BlueElectrum';
|
||||
import triggerHapticFeedback, { HapticFeedbackTypes } from '../../blue_modules/hapticFeedback';
|
||||
import { startAndDecrypt } from '../../blue_modules/start-and-decrypt';
|
||||
import { majorTomToGroundControl } from '../../blue_modules/notifications';
|
||||
import { isNotificationsEnabled, majorTomToGroundControl, unsubscribe } from '../../blue_modules/notifications';
|
||||
import { BitcoinUnit } from '../../models/bitcoinUnits';
|
||||
|
||||
const BlueApp = BlueAppClass.getInstance();
|
||||
|
||||
|
@ -49,6 +50,8 @@ interface StorageContextType {
|
|||
cachedPassword: typeof BlueApp.cachedPassword;
|
||||
getItem: typeof BlueApp.getItem;
|
||||
setItem: typeof BlueApp.setItem;
|
||||
handleWalletDeletion: (walletID: string, forceDelete?: boolean) => Promise<boolean>;
|
||||
confirmWalletDeletion: (wallet: any, onConfirmed: () => void) => void;
|
||||
}
|
||||
|
||||
export enum WalletTransactionsStatus {
|
||||
|
@ -99,6 +102,120 @@ export const StorageProvider = ({ children }: { children: React.ReactNode }) =>
|
|||
setWallets([...BlueApp.getWallets()]);
|
||||
}, []);
|
||||
|
||||
const handleWalletDeletion = useCallback(
|
||||
async (walletID: string, forceDelete = false): Promise<boolean> => {
|
||||
console.debug(`handleWalletDeletion: invoked for walletID ${walletID}`);
|
||||
const wallet = wallets.find(w => w.getID() === walletID);
|
||||
if (!wallet) {
|
||||
console.warn(`handleWalletDeletion: wallet not found for ${walletID}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (forceDelete) {
|
||||
deleteWallet(wallet);
|
||||
await saveToDisk(true);
|
||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
|
||||
return true;
|
||||
}
|
||||
|
||||
let isNotificationsSettingsEnabled = false;
|
||||
try {
|
||||
isNotificationsSettingsEnabled = await isNotificationsEnabled();
|
||||
} catch (error) {
|
||||
console.error(`handleWalletDeletion: error checking notifications for wallet ${walletID}`, error);
|
||||
return await new Promise<boolean>(resolve => {
|
||||
presentAlert({
|
||||
title: loc.errors.error,
|
||||
message: loc.wallets.details_delete_wallet_error_message,
|
||||
buttons: [
|
||||
{
|
||||
text: loc.wallets.details_delete_anyway,
|
||||
onPress: async () => {
|
||||
const result = await handleWalletDeletion(walletID, true);
|
||||
resolve(result);
|
||||
},
|
||||
style: 'destructive',
|
||||
},
|
||||
{
|
||||
text: loc.wallets.list_tryagain,
|
||||
onPress: async () => {
|
||||
const result = await handleWalletDeletion(walletID);
|
||||
resolve(result);
|
||||
},
|
||||
},
|
||||
{
|
||||
text: loc._.cancel,
|
||||
onPress: () => resolve(false),
|
||||
style: 'cancel',
|
||||
},
|
||||
],
|
||||
options: { cancelable: false },
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
if (isNotificationsSettingsEnabled) {
|
||||
const externalAddresses = wallet.getAllExternalAddresses();
|
||||
if (externalAddresses.length > 0) {
|
||||
console.debug(`handleWalletDeletion: unsubscribing addresses for wallet ${walletID}`);
|
||||
try {
|
||||
await unsubscribe(externalAddresses, [], []);
|
||||
console.debug(`handleWalletDeletion: unsubscribe succeeded for wallet ${walletID}`);
|
||||
} catch (unsubscribeError) {
|
||||
console.error(`handleWalletDeletion: unsubscribe failed for wallet ${walletID}`, unsubscribeError);
|
||||
presentAlert({
|
||||
title: loc.errors.error,
|
||||
message: loc.wallets.details_delete_wallet_error_message,
|
||||
buttons: [{ text: loc._.ok, onPress: () => {} }],
|
||||
options: { cancelable: false },
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
deleteWallet(wallet);
|
||||
console.debug(`handleWalletDeletion: wallet ${walletID} deleted successfully`);
|
||||
await saveToDisk(true);
|
||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
|
||||
return true;
|
||||
} catch (e: unknown) {
|
||||
console.error(`handleWalletDeletion: encountered error for wallet ${walletID}`, e);
|
||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
|
||||
return await new Promise<boolean>(resolve => {
|
||||
presentAlert({
|
||||
title: loc.errors.error,
|
||||
message: loc.wallets.details_delete_wallet_error_message,
|
||||
buttons: [
|
||||
{
|
||||
text: loc.wallets.details_delete_anyway,
|
||||
onPress: async () => {
|
||||
const result = await handleWalletDeletion(walletID, true);
|
||||
resolve(result);
|
||||
},
|
||||
style: 'destructive',
|
||||
},
|
||||
{
|
||||
text: loc.wallets.list_tryagain,
|
||||
onPress: async () => {
|
||||
const result = await handleWalletDeletion(walletID);
|
||||
resolve(result);
|
||||
},
|
||||
},
|
||||
{
|
||||
text: loc._.cancel,
|
||||
onPress: () => resolve(false),
|
||||
style: 'cancel',
|
||||
},
|
||||
],
|
||||
options: { cancelable: false },
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
[deleteWallet, saveToDisk, wallets],
|
||||
);
|
||||
|
||||
const resetWallets = useCallback(() => {
|
||||
setWallets(BlueApp.getWallets());
|
||||
}, []);
|
||||
|
@ -120,56 +237,72 @@ export const StorageProvider = ({ children }: { children: React.ReactNode }) =>
|
|||
}
|
||||
}, [walletsInitialized]);
|
||||
|
||||
// Add a refresh lock to prevent concurrent refreshes
|
||||
const refreshingRef = useRef<boolean>(false);
|
||||
|
||||
const refreshAllWalletTransactions = useCallback(
|
||||
async (lastSnappedTo?: number, showUpdateStatusIndicator: boolean = true) => {
|
||||
const TIMEOUT_DURATION = 30000;
|
||||
if (refreshingRef.current) {
|
||||
console.debug('[refreshAllWalletTransactions] Refresh already in progress');
|
||||
return;
|
||||
}
|
||||
refreshingRef.current = true;
|
||||
|
||||
await new Promise<void>(resolve => InteractionManager.runAfterInteractions(() => resolve()));
|
||||
|
||||
const TIMEOUT_DURATION = 30000;
|
||||
const timeoutPromise = new Promise<never>((_resolve, reject) =>
|
||||
setTimeout(() => {
|
||||
reject(new Error('refreshAllWalletTransactions: Timeout reached'));
|
||||
console.debug('[refreshAllWalletTransactions] Timeout reached');
|
||||
reject(new Error('Timeout reached'));
|
||||
}, TIMEOUT_DURATION),
|
||||
);
|
||||
|
||||
const mainLogicPromise = new Promise<void>((resolve, reject) => {
|
||||
InteractionManager.runAfterInteractions(async () => {
|
||||
let noErr = true;
|
||||
try {
|
||||
await BlueElectrum.waitTillConnected();
|
||||
if (showUpdateStatusIndicator) {
|
||||
setWalletTransactionUpdateStatus(WalletTransactionsStatus.ALL);
|
||||
}
|
||||
const paymentCodesStart = Date.now();
|
||||
await BlueApp.fetchSenderPaymentCodes(lastSnappedTo);
|
||||
const paymentCodesEnd = Date.now();
|
||||
console.debug('fetch payment codes took', (paymentCodesEnd - paymentCodesStart) / 1000, 'sec');
|
||||
try {
|
||||
if (showUpdateStatusIndicator) {
|
||||
console.debug('[refreshAllWalletTransactions] Setting wallet transaction status to ALL');
|
||||
setWalletTransactionUpdateStatus(WalletTransactionsStatus.ALL);
|
||||
}
|
||||
console.debug('[refreshAllWalletTransactions] Waiting for connectivity...');
|
||||
await BlueElectrum.waitTillConnected();
|
||||
console.debug('[refreshAllWalletTransactions] Connected to Electrum');
|
||||
|
||||
// Restore fetch payment codes timing measurement
|
||||
if (typeof BlueApp.fetchSenderPaymentCodes === 'function') {
|
||||
const codesStart = Date.now();
|
||||
console.debug('[refreshAllWalletTransactions] Fetching sender payment codes');
|
||||
await BlueApp.fetchSenderPaymentCodes(lastSnappedTo);
|
||||
const codesEnd = Date.now();
|
||||
console.debug('[refreshAllWalletTransactions] fetch payment codes took', (codesEnd - codesStart) / 1000, 'sec');
|
||||
} else {
|
||||
console.warn('[refreshAllWalletTransactions] fetchSenderPaymentCodes is not available');
|
||||
}
|
||||
|
||||
console.debug('[refreshAllWalletTransactions] Fetching wallet balances and transactions');
|
||||
await Promise.race([
|
||||
(async () => {
|
||||
const balanceStart = Date.now();
|
||||
await BlueApp.fetchWalletBalances(lastSnappedTo);
|
||||
const balanceEnd = Date.now();
|
||||
console.debug('fetch balance took', (balanceEnd - balanceStart) / 1000, 'sec');
|
||||
console.debug('[refreshAllWalletTransactions] fetch balance took', (balanceEnd - balanceStart) / 1000, 'sec');
|
||||
|
||||
const start = Date.now();
|
||||
const txStart = Date.now();
|
||||
await BlueApp.fetchWalletTransactions(lastSnappedTo);
|
||||
const end = Date.now();
|
||||
console.debug('fetch tx took', (end - start) / 1000, 'sec');
|
||||
} catch (err) {
|
||||
noErr = false;
|
||||
console.error(err);
|
||||
reject(err);
|
||||
} finally {
|
||||
setWalletTransactionUpdateStatus(WalletTransactionsStatus.NONE);
|
||||
}
|
||||
if (noErr) await saveToDisk();
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
const txEnd = Date.now();
|
||||
console.debug('[refreshAllWalletTransactions] fetch tx took', (txEnd - txStart) / 1000, 'sec');
|
||||
|
||||
try {
|
||||
await Promise.race([mainLogicPromise, timeoutPromise]);
|
||||
} catch (err) {
|
||||
console.error('Error in refreshAllWalletTransactions:', err);
|
||||
console.debug('[refreshAllWalletTransactions] Saving data to disk');
|
||||
await saveToDisk();
|
||||
})(),
|
||||
timeoutPromise,
|
||||
]);
|
||||
console.debug('[refreshAllWalletTransactions] Refresh completed successfully');
|
||||
} catch (error) {
|
||||
console.error('[refreshAllWalletTransactions] Error:', error);
|
||||
} finally {
|
||||
console.debug('[refreshAllWalletTransactions] Resetting wallet transaction status and refresh lock');
|
||||
setWalletTransactionUpdateStatus(WalletTransactionsStatus.NONE);
|
||||
refreshingRef.current = false;
|
||||
}
|
||||
},
|
||||
[saveToDisk],
|
||||
|
@ -182,24 +315,26 @@ export const StorageProvider = ({ children }: { children: React.ReactNode }) =>
|
|||
let noErr = true;
|
||||
try {
|
||||
if (Date.now() - (_lastTimeTriedToRefetchWallet[walletID] || 0) < 5000) {
|
||||
console.debug('Re-fetch wallet happens too fast; NOP');
|
||||
console.debug('[fetchAndSaveWalletTransactions] Re-fetch wallet happens too fast; NOP');
|
||||
return;
|
||||
}
|
||||
_lastTimeTriedToRefetchWallet[walletID] = Date.now();
|
||||
|
||||
await BlueElectrum.waitTillConnected();
|
||||
setWalletTransactionUpdateStatus(walletID);
|
||||
|
||||
const balanceStart = Date.now();
|
||||
await BlueApp.fetchWalletBalances(index);
|
||||
const balanceEnd = Date.now();
|
||||
console.debug('fetch balance took', (balanceEnd - balanceStart) / 1000, 'sec');
|
||||
const start = Date.now();
|
||||
console.debug('[fetchAndSaveWalletTransactions] fetch balance took', (balanceEnd - balanceStart) / 1000, 'sec');
|
||||
|
||||
const txStart = Date.now();
|
||||
await BlueApp.fetchWalletTransactions(index);
|
||||
const end = Date.now();
|
||||
console.debug('fetch tx took', (end - start) / 1000, 'sec');
|
||||
const txEnd = Date.now();
|
||||
console.debug('[fetchAndSaveWalletTransactions] fetch tx took', (txEnd - txStart) / 1000, 'sec');
|
||||
} catch (err) {
|
||||
noErr = false;
|
||||
console.error(err);
|
||||
console.error('[fetchAndSaveWalletTransactions] Error:', err);
|
||||
} finally {
|
||||
setWalletTransactionUpdateStatus(WalletTransactionsStatus.NONE);
|
||||
}
|
||||
|
@ -217,10 +352,10 @@ export const StorageProvider = ({ children }: { children: React.ReactNode }) =>
|
|||
return;
|
||||
}
|
||||
const emptyWalletLabel = new LegacyWallet().getLabel();
|
||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
|
||||
if (w.getLabel() === emptyWalletLabel) w.setLabel(loc.wallets.import_imported + ' ' + w.typeReadable);
|
||||
w.setUserHasSavedExport(true);
|
||||
addWallet(w);
|
||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
|
||||
await saveToDisk();
|
||||
A(A.ENUM.CREATED_WALLET);
|
||||
presentAlert({
|
||||
|
@ -239,6 +374,36 @@ export const StorageProvider = ({ children }: { children: React.ReactNode }) =>
|
|||
[wallets, addWallet, saveToDisk],
|
||||
);
|
||||
|
||||
function confirmWalletDeletion(wallet: any, onConfirmed: () => void) {
|
||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationWarning);
|
||||
try {
|
||||
const balance = formatBalanceWithoutSuffix(wallet.getBalance(), BitcoinUnit.SATS, true);
|
||||
presentAlert({
|
||||
title: loc.wallets.details_delete_wallet,
|
||||
message: loc.formatString(loc.wallets.details_del_wb_q, { balance }),
|
||||
buttons: [
|
||||
{
|
||||
text: loc.wallets.details_delete,
|
||||
onPress: () => {
|
||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
|
||||
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
|
||||
onConfirmed();
|
||||
},
|
||||
style: 'destructive',
|
||||
},
|
||||
{
|
||||
text: loc._.cancel,
|
||||
onPress: () => {},
|
||||
style: 'cancel',
|
||||
},
|
||||
],
|
||||
options: { cancelable: false },
|
||||
});
|
||||
} catch (error) {
|
||||
// Handle error silently if needed
|
||||
}
|
||||
}
|
||||
|
||||
const value: StorageContextType = useMemo(
|
||||
() => ({
|
||||
wallets,
|
||||
|
@ -274,6 +439,8 @@ export const StorageProvider = ({ children }: { children: React.ReactNode }) =>
|
|||
isPasswordInUse: BlueApp.isPasswordInUse,
|
||||
walletTransactionUpdateStatus,
|
||||
setWalletTransactionUpdateStatus,
|
||||
handleWalletDeletion,
|
||||
confirmWalletDeletion,
|
||||
}),
|
||||
[
|
||||
wallets,
|
||||
|
@ -291,7 +458,7 @@ export const StorageProvider = ({ children }: { children: React.ReactNode }) =>
|
|||
refreshAllWalletTransactions,
|
||||
resetWallets,
|
||||
walletTransactionUpdateStatus,
|
||||
setWalletTransactionUpdateStatus,
|
||||
handleWalletDeletion,
|
||||
],
|
||||
);
|
||||
|
||||
|
|
|
@ -1,14 +1,18 @@
|
|||
import React, { forwardRef, ReactNode, useEffect, useRef, useState } from 'react';
|
||||
import React, { forwardRef, ReactNode, useEffect, useRef, useState, useCallback } from 'react';
|
||||
import { Animated, Dimensions, PixelRatio, StyleSheet, Text, TouchableOpacity, useWindowDimensions, View } from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
import { useTheme } from './themes';
|
||||
|
||||
const BORDER_RADIUS = 8;
|
||||
const PADDINGS = 24;
|
||||
const ICON_MARGIN = 7;
|
||||
|
||||
const cStyles = StyleSheet.create({
|
||||
const buttonFontSize = (() => {
|
||||
const baseSize = PixelRatio.roundToNearestPixel(Dimensions.get('window').width / 26);
|
||||
return Math.min(22, baseSize);
|
||||
})();
|
||||
|
||||
const containerStyles = StyleSheet.create({
|
||||
root: {
|
||||
alignSelf: 'center',
|
||||
height: '6.9%',
|
||||
|
@ -26,6 +30,27 @@ const cStyles = StyleSheet.create({
|
|||
flexDirection: 'row',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
rootRound: {
|
||||
borderRadius: 9999,
|
||||
},
|
||||
});
|
||||
|
||||
const buttonStyles = StyleSheet.create({
|
||||
root: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
icon: {
|
||||
alignItems: 'center',
|
||||
},
|
||||
text: {
|
||||
fontSize: buttonFontSize,
|
||||
fontWeight: '600',
|
||||
marginLeft: ICON_MARGIN,
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
});
|
||||
|
||||
interface FContainerProps {
|
||||
|
@ -51,93 +76,79 @@ export const FContainer = forwardRef<View, FContainerProps>((props, ref) => {
|
|||
}).start();
|
||||
}, [height, slideAnimation]);
|
||||
|
||||
const computeNewWidth = useCallback(
|
||||
(layoutWidth: number, totalChildren: number) => {
|
||||
const maxWidth = width - BORDER_RADIUS - 140;
|
||||
const paddedWidth = Math.ceil(layoutWidth + PADDINGS * 2);
|
||||
let calculatedWidth = paddedWidth * totalChildren > maxWidth ? Math.floor(maxWidth / totalChildren) : paddedWidth;
|
||||
if (totalChildren === 1 && calculatedWidth < 90) calculatedWidth = 90;
|
||||
return calculatedWidth;
|
||||
},
|
||||
[width],
|
||||
);
|
||||
|
||||
const onLayout = (event: { nativeEvent: { layout: { width: number } } }) => {
|
||||
if (layoutCalculated.current) return;
|
||||
const maxWidth = width - BORDER_RADIUS - 140;
|
||||
const layoutWidth = event.nativeEvent.layout.width;
|
||||
const withPaddings = Math.ceil(layoutWidth + PADDINGS * 2);
|
||||
const len = React.Children.toArray(props.children).filter(Boolean).length;
|
||||
let newW = withPaddings * len > maxWidth ? Math.floor(maxWidth / len) : withPaddings;
|
||||
if (len === 1 && newW < 90) newW = 90;
|
||||
setNewWidth(newW);
|
||||
const { width: layoutWidth } = event.nativeEvent.layout;
|
||||
const totalChildren = React.Children.toArray(props.children).filter(Boolean).length;
|
||||
setNewWidth(computeNewWidth(layoutWidth, totalChildren));
|
||||
layoutCalculated.current = true;
|
||||
};
|
||||
|
||||
const renderChild = (child: ReactNode, index: number, array: ReactNode[]): ReactNode => {
|
||||
if (typeof child === 'string') {
|
||||
return (
|
||||
<View key={index} style={{ width: newWidth }}>
|
||||
<Text adjustsFontSizeToFit numberOfLines={1}>
|
||||
{child}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return React.cloneElement(child as React.ReactElement<any>, {
|
||||
width: newWidth,
|
||||
key: index,
|
||||
first: index === 0,
|
||||
last: index === array.length - 1,
|
||||
singleChild: array.length === 1,
|
||||
});
|
||||
};
|
||||
|
||||
const totalChildren = React.Children.toArray(props.children).filter(Boolean).length;
|
||||
return (
|
||||
<Animated.View
|
||||
ref={ref}
|
||||
onLayout={onLayout}
|
||||
style={[
|
||||
cStyles.root,
|
||||
props.inline ? cStyles.rootInline : cStyles.rootAbsolute,
|
||||
containerStyles.root,
|
||||
props.inline ? containerStyles.rootInline : containerStyles.rootAbsolute,
|
||||
bottomInsets,
|
||||
newWidth ? cStyles.rootPost : cStyles.rootPre,
|
||||
newWidth ? containerStyles.rootPost : containerStyles.rootPre,
|
||||
totalChildren === 1 ? containerStyles.rootRound : null,
|
||||
{ transform: [{ translateY: slideAnimation }] },
|
||||
]}
|
||||
>
|
||||
{newWidth
|
||||
? React.Children.toArray(props.children)
|
||||
.filter(Boolean)
|
||||
.map((child, index, array) => {
|
||||
if (typeof child === 'string') {
|
||||
return (
|
||||
<View key={index} style={{ width: newWidth }}>
|
||||
<Text adjustsFontSizeToFit numberOfLines={1}>
|
||||
{child}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return React.cloneElement(child as React.ReactElement<any>, {
|
||||
width: newWidth,
|
||||
key: index,
|
||||
first: index === 0,
|
||||
last: index === array.length - 1,
|
||||
});
|
||||
})
|
||||
: props.children}
|
||||
{newWidth ? React.Children.toArray(props.children).filter(Boolean).map(renderChild) : props.children}
|
||||
</Animated.View>
|
||||
);
|
||||
});
|
||||
|
||||
const buttonFontSize =
|
||||
PixelRatio.roundToNearestPixel(Dimensions.get('window').width / 26) > 22
|
||||
? 22
|
||||
: PixelRatio.roundToNearestPixel(Dimensions.get('window').width / 26);
|
||||
|
||||
const bStyles = StyleSheet.create({
|
||||
root: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
icon: {
|
||||
alignItems: 'center',
|
||||
},
|
||||
text: {
|
||||
fontSize: buttonFontSize,
|
||||
fontWeight: '600',
|
||||
marginLeft: ICON_MARGIN,
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
});
|
||||
|
||||
interface FButtonProps {
|
||||
text: string;
|
||||
icon: ReactNode;
|
||||
width?: number;
|
||||
first?: boolean;
|
||||
last?: boolean;
|
||||
singleChild?: boolean;
|
||||
disabled?: boolean;
|
||||
testID?: string;
|
||||
onPress: () => void;
|
||||
onLongPress?: () => void;
|
||||
}
|
||||
|
||||
export const FButton = ({ text, icon, width, first, last, testID, ...props }: FButtonProps) => {
|
||||
export const FButton = ({ text, icon, width, first, last, singleChild, testID, ...props }: FButtonProps) => {
|
||||
const { colors } = useTheme();
|
||||
const bStylesHook = StyleSheet.create({
|
||||
const customButtonStyles = StyleSheet.create({
|
||||
root: {
|
||||
backgroundColor: colors.buttonBackgroundColor,
|
||||
borderRadius: BORDER_RADIUS,
|
||||
|
@ -151,9 +162,12 @@ export const FButton = ({ text, icon, width, first, last, testID, ...props }: FB
|
|||
marginRight: {
|
||||
marginRight: 10,
|
||||
},
|
||||
rootRound: {
|
||||
borderRadius: 9999,
|
||||
},
|
||||
});
|
||||
const style: Record<string, any> = {};
|
||||
const additionalStyles = !last ? bStylesHook.marginRight : {};
|
||||
const additionalStyles = !last ? customButtonStyles.marginRight : {};
|
||||
|
||||
if (width) {
|
||||
style.paddingHorizontal = PADDINGS;
|
||||
|
@ -165,11 +179,15 @@ export const FButton = ({ text, icon, width, first, last, testID, ...props }: FB
|
|||
accessibilityLabel={text}
|
||||
accessibilityRole="button"
|
||||
testID={testID}
|
||||
style={[bStyles.root, bStylesHook.root, style, additionalStyles]}
|
||||
style={[buttonStyles.root, customButtonStyles.root, style, additionalStyles, singleChild ? customButtonStyles.rootRound : null]}
|
||||
{...props}
|
||||
>
|
||||
<View style={bStyles.icon}>{icon}</View>
|
||||
<Text numberOfLines={1} adjustsFontSizeToFit style={[bStyles.text, props.disabled ? bStylesHook.textDisabled : bStylesHook.text]}>
|
||||
<View style={buttonStyles.icon}>{icon}</View>
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
adjustsFontSizeToFit
|
||||
style={[buttonStyles.text, props.disabled ? customButtonStyles.textDisabled : customButtonStyles.text]}
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
|
|
@ -9,7 +9,12 @@ import { HandOffComponentProps } from './types';
|
|||
|
||||
const HandOffComponent: React.FC<HandOffComponentProps> = props => {
|
||||
const { isHandOffUseEnabled } = useSettings();
|
||||
console.debug('HandOffComponent is rendering.');
|
||||
if (!props || !props.type || !props.userInfo || Object.keys(props.userInfo).length === 0) {
|
||||
console.debug('HandOffComponent: Missing required type or userInfo data');
|
||||
return null;
|
||||
}
|
||||
const userInfo = JSON.stringify(props.userInfo);
|
||||
console.debug(`HandOffComponent is rendering. Type: ${props.type}, UserInfo: ${userInfo}...`);
|
||||
return isHandOffUseEnabled ? <Handoff {...props} /> : null;
|
||||
};
|
||||
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import { View, StyleSheet, ViewStyle, TouchableOpacity } from 'react-native';
|
||||
import React, { useCallback, useState, useEffect, useRef } from 'react';
|
||||
import { StyleSheet, ViewStyle, TouchableOpacity, ActivityIndicator, Platform, Animated } from 'react-native';
|
||||
import { Icon, ListItem } from '@rneui/base';
|
||||
import { ExtendedTransaction, LightningTransaction, TWallet } from '../class/wallets/types';
|
||||
import { WalletCarouselItem } from './WalletsCarousel';
|
||||
import { TransactionListItem } from './TransactionListItem';
|
||||
import { useTheme } from './themes';
|
||||
import { BitcoinUnit } from '../models/bitcoinUnits';
|
||||
import loc from '../loc';
|
||||
import triggerHapticFeedback, { HapticFeedbackTypes } from '../blue_modules/hapticFeedback';
|
||||
|
||||
enum ItemType {
|
||||
WalletSection = 'wallet',
|
||||
|
@ -29,11 +31,16 @@ interface ManageWalletsListItemProps {
|
|||
isDraggingDisabled: boolean;
|
||||
drag?: () => void;
|
||||
isPlaceHolder?: boolean;
|
||||
onPressIn?: () => void;
|
||||
onPressOut?: () => void;
|
||||
state: { wallets: TWallet[]; searchQuery: string };
|
||||
navigateToWallet: (wallet: TWallet) => void;
|
||||
renderHighlightedText: (text: string, query: string) => JSX.Element;
|
||||
handleDeleteWallet: (wallet: TWallet) => void;
|
||||
handleToggleHideBalance: (wallet: TWallet) => void;
|
||||
isActive?: boolean;
|
||||
style?: ViewStyle;
|
||||
globalDragActive?: boolean;
|
||||
}
|
||||
|
||||
interface SwipeContentProps {
|
||||
|
@ -46,14 +53,21 @@ const LeftSwipeContent: React.FC<SwipeContentProps> = ({ onPress, hideBalance, c
|
|||
<TouchableOpacity
|
||||
onPress={onPress}
|
||||
style={[styles.leftButtonContainer, { backgroundColor: colors.buttonAlternativeTextColor } as ViewStyle]}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel={hideBalance ? loc.transactions.details_balance_show : loc.transactions.details_balance_hide}
|
||||
>
|
||||
<Icon name={hideBalance ? 'eye-slash' : 'eye'} color={colors.brandingColor} type="font-awesome-5" />
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
const RightSwipeContent: React.FC<Partial<SwipeContentProps>> = ({ onPress }) => (
|
||||
<TouchableOpacity onPress={onPress} style={styles.rightButtonContainer as ViewStyle}>
|
||||
<Icon name="delete-outline" color="#FFFFFF" />
|
||||
<TouchableOpacity
|
||||
onPress={onPress}
|
||||
style={styles.rightButtonContainer as ViewStyle}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Delete Wallet"
|
||||
>
|
||||
<Icon name={Platform.OS === 'android' ? 'delete' : 'delete-outline'} color="#FFFFFF" />
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
|
@ -67,68 +81,141 @@ const ManageWalletsListItem: React.FC<ManageWalletsListItemProps> = ({
|
|||
renderHighlightedText,
|
||||
handleDeleteWallet,
|
||||
handleToggleHideBalance,
|
||||
onPressIn,
|
||||
onPressOut,
|
||||
isActive,
|
||||
globalDragActive,
|
||||
style,
|
||||
}) => {
|
||||
const { colors } = useTheme();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isSwipeActive, setIsSwipeActive] = useState(false);
|
||||
const resetFunctionRef = useRef<(() => void) | null>(null);
|
||||
|
||||
const CARD_SORT_ACTIVE = 1.06;
|
||||
const INACTIVE_SCALE_WHEN_ACTIVE = 0.9;
|
||||
const SCALE_DURATION = 200;
|
||||
const scaleValue = useRef(new Animated.Value(1)).current;
|
||||
const prevIsActive = useRef(isActive);
|
||||
|
||||
const DEFAULT_VERTICAL_MARGIN = -10;
|
||||
const REDUCED_VERTICAL_MARGIN = -50;
|
||||
|
||||
useEffect(() => {
|
||||
if (isActive !== prevIsActive.current) {
|
||||
triggerHapticFeedback(HapticFeedbackTypes.ImpactMedium);
|
||||
}
|
||||
prevIsActive.current = isActive;
|
||||
|
||||
Animated.timing(scaleValue, {
|
||||
toValue: isActive ? CARD_SORT_ACTIVE : globalDragActive ? INACTIVE_SCALE_WHEN_ACTIVE : 1,
|
||||
duration: SCALE_DURATION,
|
||||
useNativeDriver: true,
|
||||
}).start();
|
||||
}, [isActive, globalDragActive, scaleValue]);
|
||||
|
||||
const onPress = useCallback(() => {
|
||||
if (item.type === ItemType.WalletSection) {
|
||||
setIsLoading(true);
|
||||
navigateToWallet(item.data);
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [item, navigateToWallet]);
|
||||
|
||||
const leftContent = useCallback(
|
||||
(reset: () => void) => (
|
||||
<LeftSwipeContent
|
||||
onPress={() => {
|
||||
handleToggleHideBalance(item.data as TWallet);
|
||||
reset();
|
||||
}}
|
||||
hideBalance={(item.data as TWallet).hideBalance}
|
||||
colors={colors}
|
||||
/>
|
||||
),
|
||||
[colors, handleToggleHideBalance, item.data],
|
||||
);
|
||||
const handleLeftPress = (reset: () => void) => {
|
||||
handleToggleHideBalance(item.data as TWallet);
|
||||
reset();
|
||||
};
|
||||
|
||||
const rightContent = useCallback(
|
||||
(reset: () => void) => (
|
||||
<RightSwipeContent
|
||||
onPress={() => {
|
||||
handleDeleteWallet(item.data as TWallet);
|
||||
reset();
|
||||
}}
|
||||
/>
|
||||
),
|
||||
[handleDeleteWallet, item.data],
|
||||
);
|
||||
const leftContent = (reset: () => void) => {
|
||||
resetFunctionRef.current = reset;
|
||||
return <LeftSwipeContent onPress={() => handleLeftPress(reset)} hideBalance={(item.data as TWallet).hideBalance} colors={colors} />;
|
||||
};
|
||||
|
||||
const handleRightPress = (reset: () => void) => {
|
||||
reset();
|
||||
|
||||
setTimeout(() => {
|
||||
handleDeleteWallet(item.data as TWallet);
|
||||
}, 100); // short delay to allow swipe reset animation to complete
|
||||
};
|
||||
|
||||
const rightContent = (reset: () => void) => {
|
||||
resetFunctionRef.current = reset;
|
||||
return <RightSwipeContent onPress={() => handleRightPress(reset)} />;
|
||||
};
|
||||
|
||||
const startDrag = useCallback(() => {
|
||||
if (isSwipeActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (resetFunctionRef.current) {
|
||||
resetFunctionRef.current();
|
||||
}
|
||||
|
||||
scaleValue.setValue(CARD_SORT_ACTIVE);
|
||||
triggerHapticFeedback(HapticFeedbackTypes.ImpactMedium);
|
||||
if (drag) {
|
||||
drag();
|
||||
}
|
||||
}, [CARD_SORT_ACTIVE, drag, scaleValue, isSwipeActive]);
|
||||
|
||||
if (isLoading) {
|
||||
return <ActivityIndicator size="large" color={colors.brandingColor} />;
|
||||
}
|
||||
|
||||
if (item.type === ItemType.WalletSection) {
|
||||
const animatedStyle = {
|
||||
transform: [{ scale: scaleValue }],
|
||||
marginVertical: globalDragActive && !isActive ? REDUCED_VERTICAL_MARGIN : DEFAULT_VERTICAL_MARGIN,
|
||||
};
|
||||
|
||||
const backgroundColor = isActive || globalDragActive ? colors.brandingColor : colors.background;
|
||||
|
||||
const swipeDisabled = isActive || globalDragActive;
|
||||
|
||||
return (
|
||||
<ListItem.Swipeable
|
||||
leftWidth={80}
|
||||
rightWidth={90}
|
||||
containerStyle={{ backgroundColor: colors.background }}
|
||||
leftContent={leftContent}
|
||||
rightContent={rightContent}
|
||||
>
|
||||
<ListItem.Content
|
||||
style={{
|
||||
backgroundColor: colors.background,
|
||||
<Animated.View style={animatedStyle}>
|
||||
<ListItem.Swipeable
|
||||
leftWidth={swipeDisabled ? 0 : 80}
|
||||
rightWidth={swipeDisabled ? 0 : 90}
|
||||
containerStyle={[style, { backgroundColor }, swipeDisabled ? styles.transparentBackground : {}]}
|
||||
leftContent={swipeDisabled ? null : leftContent}
|
||||
rightContent={swipeDisabled ? null : rightContent}
|
||||
onPressOut={onPressOut}
|
||||
minSlideWidth={swipeDisabled ? 0 : 80}
|
||||
onPressIn={onPressIn}
|
||||
style={swipeDisabled ? styles.transparentBackground : {}}
|
||||
onSwipeBegin={direction => {
|
||||
if (!swipeDisabled) {
|
||||
console.debug(`Swipe began: ${direction}`);
|
||||
setIsSwipeActive(true);
|
||||
}
|
||||
}}
|
||||
onSwipeEnd={() => {
|
||||
if (!swipeDisabled) {
|
||||
console.debug('Swipe ended');
|
||||
setIsSwipeActive(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<View style={styles.walletCarouselItemContainer}>
|
||||
<ListItem.Content>
|
||||
<WalletCarouselItem
|
||||
item={item.data}
|
||||
handleLongPress={isDraggingDisabled ? undefined : drag}
|
||||
handleLongPress={isDraggingDisabled || isSwipeActive ? undefined : startDrag}
|
||||
onPress={onPress}
|
||||
onPressIn={onPressIn}
|
||||
onPressOut={onPressOut}
|
||||
animationsEnabled={false}
|
||||
searchQuery={state.searchQuery}
|
||||
isPlaceHolder={isPlaceHolder}
|
||||
renderHighlightedText={renderHighlightedText}
|
||||
customStyle={styles.carouselItem}
|
||||
/>
|
||||
</View>
|
||||
</ListItem.Content>
|
||||
</ListItem.Swipeable>
|
||||
</ListItem.Content>
|
||||
</ListItem.Swipeable>
|
||||
</Animated.View>
|
||||
);
|
||||
} else if (item.type === ItemType.TransactionSection && item.data) {
|
||||
const w = state.wallets.find(wallet => wallet.getTransactions().some((tx: ExtendedTransaction) => tx.hash === item.data.hash));
|
||||
|
@ -145,25 +232,28 @@ const ManageWalletsListItem: React.FC<ManageWalletsListItemProps> = ({
|
|||
);
|
||||
}
|
||||
|
||||
console.error('Unrecognized item type:', item);
|
||||
return null;
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
walletCarouselItemContainer: {
|
||||
width: '100%',
|
||||
},
|
||||
leftButtonContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
carouselItem: {
|
||||
width: '100%',
|
||||
},
|
||||
rightButtonContainer: {
|
||||
flex: 1,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'red',
|
||||
},
|
||||
transparentBackground: {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
});
|
||||
|
||||
export { ManageWalletsListItem, LeftSwipeContent, RightSwipeContent };
|
||||
export { LeftSwipeContent, RightSwipeContent };
|
||||
export default ManageWalletsListItem;
|
||||
|
|
|
@ -1,23 +1,66 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import React, { useRef } from 'react';
|
||||
import { ActivityIndicator, findNodeHandle, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
import {
|
||||
ActivityIndicator,
|
||||
findNodeHandle,
|
||||
GestureResponderEvent,
|
||||
StyleProp,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
ViewStyle,
|
||||
} from 'react-native';
|
||||
import { Icon } from '@rneui/themed';
|
||||
|
||||
import ActionSheet from '../screen/ActionSheet';
|
||||
import ActionSheetOptions from '../screen/ActionSheet.common';
|
||||
import { useTheme } from './themes';
|
||||
export const MultipleStepsListItemDashType = Object.freeze({ none: 0, top: 1, bottom: 2, topAndBottom: 3 });
|
||||
export const MultipleStepsListItemButtohType = Object.freeze({ partial: 0, full: 1 });
|
||||
import { ActionSheetOptions } from '../screen/ActionSheet.common';
|
||||
|
||||
const MultipleStepsListItem = props => {
|
||||
export enum MultipleStepsListItemDashType {
|
||||
None = 0,
|
||||
Top = 1,
|
||||
Bottom = 2,
|
||||
TopAndBottom = 3,
|
||||
}
|
||||
|
||||
export enum MultipleStepsListItemButtonType {
|
||||
Partial = 0,
|
||||
Full = 1,
|
||||
}
|
||||
|
||||
interface MultipleStepsListItemProps {
|
||||
circledText?: string;
|
||||
checked?: boolean;
|
||||
leftText?: string;
|
||||
showActivityIndicator?: boolean;
|
||||
isActionSheet?: boolean;
|
||||
actionSheetOptions?: ActionSheetOptions;
|
||||
dashes?: MultipleStepsListItemDashType;
|
||||
button?: {
|
||||
text?: string;
|
||||
onPress?: (e: GestureResponderEvent | number) => void;
|
||||
disabled?: boolean;
|
||||
buttonType?: MultipleStepsListItemButtonType;
|
||||
leftText?: string;
|
||||
showActivityIndicator?: boolean;
|
||||
testID?: string;
|
||||
};
|
||||
rightButton?: {
|
||||
text?: string;
|
||||
onPress?: () => void;
|
||||
disabled?: boolean;
|
||||
showActivityIndicator?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
const MultipleStepsListItem = (props: MultipleStepsListItemProps) => {
|
||||
const { colors } = useTheme();
|
||||
const {
|
||||
showActivityIndicator = false,
|
||||
dashes = MultipleStepsListItemDashType.none,
|
||||
dashes = MultipleStepsListItemDashType.None,
|
||||
circledText = '',
|
||||
leftText = '',
|
||||
checked = false,
|
||||
useActionSheet = false,
|
||||
isActionSheet = false,
|
||||
actionSheetOptions = null, // Default to null or appropriate default
|
||||
} = props;
|
||||
const stylesHook = StyleSheet.create({
|
||||
|
@ -43,7 +86,7 @@ const MultipleStepsListItem = props => {
|
|||
const selfRef = useRef(null); // Create a ref for the component itself
|
||||
|
||||
const handleOnPressForActionSheet = () => {
|
||||
if (useActionSheet && actionSheetOptions) {
|
||||
if (isActionSheet && actionSheetOptions) {
|
||||
// Clone options to modify them
|
||||
let modifiedOptions = { ...actionSheetOptions };
|
||||
|
||||
|
@ -57,16 +100,16 @@ const MultipleStepsListItem = props => {
|
|||
|
||||
ActionSheet.showActionSheetWithOptions(modifiedOptions, buttonIndex => {
|
||||
// Call the original onPress function, if provided, and not cancelled
|
||||
if (buttonIndex !== -1 && props.button.onPress) {
|
||||
if (buttonIndex !== -1 && props.button?.onPress) {
|
||||
props.button.onPress(buttonIndex);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const renderDashes = () => {
|
||||
const renderDashes = (): StyleProp<ViewStyle> => {
|
||||
switch (dashes) {
|
||||
case MultipleStepsListItemDashType.topAndBottom:
|
||||
case MultipleStepsListItemDashType.TopAndBottom:
|
||||
return {
|
||||
width: 1,
|
||||
borderStyle: 'dashed',
|
||||
|
@ -77,7 +120,7 @@ const MultipleStepsListItem = props => {
|
|||
marginLeft: 20,
|
||||
position: 'absolute',
|
||||
};
|
||||
case MultipleStepsListItemDashType.bottom:
|
||||
case MultipleStepsListItemDashType.Bottom:
|
||||
return {
|
||||
width: 1,
|
||||
borderStyle: 'dashed',
|
||||
|
@ -88,7 +131,7 @@ const MultipleStepsListItem = props => {
|
|||
marginLeft: 20,
|
||||
position: 'absolute',
|
||||
};
|
||||
case MultipleStepsListItemDashType.top:
|
||||
case MultipleStepsListItemDashType.Top:
|
||||
return {
|
||||
width: 1,
|
||||
borderStyle: 'dashed',
|
||||
|
@ -105,6 +148,7 @@ const MultipleStepsListItem = props => {
|
|||
};
|
||||
const buttonOpacity = { opacity: props.button?.disabled ? 0.5 : 1.0 };
|
||||
const rightButtonOpacity = { opacity: props.rightButton?.disabled ? 0.5 : 1.0 };
|
||||
const onPress = isActionSheet ? handleOnPressForActionSheet : props.button?.onPress;
|
||||
return (
|
||||
<View>
|
||||
<View style={renderDashes()} />
|
||||
|
@ -131,19 +175,19 @@ const MultipleStepsListItem = props => {
|
|||
{!showActivityIndicator && props.button && (
|
||||
<>
|
||||
{props.button.buttonType === undefined ||
|
||||
(props.button.buttonType === MultipleStepsListItemButtohType.full && (
|
||||
(props.button.buttonType === MultipleStepsListItemButtonType.Full && (
|
||||
<TouchableOpacity
|
||||
ref={useActionSheet ? selfRef : null}
|
||||
ref={isActionSheet ? selfRef : null}
|
||||
testID={props.button.testID}
|
||||
accessibilityRole="button"
|
||||
disabled={props.button.disabled}
|
||||
style={[styles.provideKeyButton, stylesHook.provideKeyButton, buttonOpacity]}
|
||||
onPress={useActionSheet ? handleOnPressForActionSheet : props.button.onPress}
|
||||
onPress={onPress}
|
||||
>
|
||||
<Text style={[styles.provideKeyButtonText, stylesHook.provideKeyButtonText]}>{props.button.text}</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
{props.button.buttonType === MultipleStepsListItemButtohType.partial && (
|
||||
{props.button.buttonType === MultipleStepsListItemButtonType.Partial && (
|
||||
<View style={styles.buttonPartialContainer}>
|
||||
<Text numberOfLines={1} style={[styles.rowPartialLeftText, stylesHook.rowPartialLeftText]} lineBreakMode="middle">
|
||||
{props.button.leftText}
|
||||
|
@ -153,11 +197,15 @@ const MultipleStepsListItem = props => {
|
|||
accessibilityRole="button"
|
||||
disabled={props.button.disabled}
|
||||
style={[styles.rowPartialRightButton, stylesHook.provideKeyButton, rightButtonOpacity]}
|
||||
onPress={props.button.onPress}
|
||||
onPress={onPress}
|
||||
>
|
||||
<Text style={[styles.provideKeyButtonText, stylesHook.provideKeyButtonText, styles.rightButton]}>
|
||||
{props.button.text}
|
||||
</Text>
|
||||
{props.button.showActivityIndicator ? (
|
||||
<ActivityIndicator />
|
||||
) : (
|
||||
<Text style={[styles.provideKeyButtonText, stylesHook.provideKeyButtonText, styles.rightButton]}>
|
||||
{props.button.text}
|
||||
</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
|
@ -171,7 +219,11 @@ const MultipleStepsListItem = props => {
|
|||
style={styles.rightButton}
|
||||
onPress={props.rightButton.onPress}
|
||||
>
|
||||
<Text style={[styles.provideKeyButtonText, stylesHook.provideKeyButtonText]}>{props.rightButton.text}</Text>
|
||||
{props.rightButton.showActivityIndicator ? (
|
||||
<ActivityIndicator />
|
||||
) : (
|
||||
<Text style={[styles.provideKeyButtonText, stylesHook.provideKeyButtonText]}>{props.rightButton.text}</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
)}
|
||||
|
@ -180,28 +232,6 @@ const MultipleStepsListItem = props => {
|
|||
);
|
||||
};
|
||||
|
||||
MultipleStepsListItem.propTypes = {
|
||||
circledText: PropTypes.string,
|
||||
checked: PropTypes.bool,
|
||||
leftText: PropTypes.string,
|
||||
showActivityIndicator: PropTypes.bool,
|
||||
useActionSheet: PropTypes.bool,
|
||||
actionSheetOptions: PropTypes.shape(ActionSheetOptions),
|
||||
dashes: PropTypes.number,
|
||||
button: PropTypes.shape({
|
||||
text: PropTypes.string,
|
||||
onPress: PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
buttonType: PropTypes.number,
|
||||
leftText: PropTypes.string,
|
||||
}),
|
||||
rightButton: PropTypes.shape({
|
||||
text: PropTypes.string,
|
||||
onPress: PropTypes.func,
|
||||
disabled: PropTypes.bool,
|
||||
}),
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flexDirection: 'row',
|
|
@ -1,10 +1,11 @@
|
|||
import React, { useState, useRef, forwardRef, useImperativeHandle, useEffect } from 'react';
|
||||
import { View, Text, TextInput, StyleSheet, Animated, Easing, ViewStyle, Keyboard, Platform, UIManager, ScrollView } from 'react-native';
|
||||
import { View, Text, TextInput, StyleSheet, Animated, Easing, ViewStyle, Keyboard, Platform, UIManager } from 'react-native';
|
||||
import BottomModal, { BottomModalHandle } from './BottomModal';
|
||||
import { useTheme } from '../components/themes';
|
||||
import loc from '../loc';
|
||||
import { SecondButton } from './SecondButton';
|
||||
import triggerHapticFeedback, { HapticFeedbackTypes } from '../blue_modules/hapticFeedback';
|
||||
import { useKeyboard } from '../hooks/useKeyboard';
|
||||
|
||||
if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) {
|
||||
UIManager.setLayoutAnimationEnabledExperimental(true);
|
||||
|
@ -42,11 +43,11 @@ const PromptPasswordConfirmationModal = forwardRef<PromptPasswordConfirmationMod
|
|||
const fadeInAnimation = useRef(new Animated.Value(0)).current;
|
||||
const scaleAnimation = useRef(new Animated.Value(1)).current;
|
||||
const shakeAnimation = useRef(new Animated.Value(0)).current;
|
||||
const explanationOpacity = useRef(new Animated.Value(1)).current; // New animated value for opacity
|
||||
const explanationOpacity = useRef(new Animated.Value(1)).current;
|
||||
const { colors } = useTheme();
|
||||
const passwordInputRef = useRef<TextInput>(null);
|
||||
const confirmPasswordInputRef = useRef<TextInput>(null);
|
||||
const scrollView = useRef<ScrollView>(null);
|
||||
const { isVisible } = useKeyboard();
|
||||
|
||||
const stylesHook = StyleSheet.create({
|
||||
modalContent: {
|
||||
|
@ -101,42 +102,43 @@ const PromptPasswordConfirmationModal = forwardRef<PromptPasswordConfirmationMod
|
|||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [modalType]);
|
||||
|
||||
const handleShakeAnimation = () => {
|
||||
const performShake = (shakeAnimRef: Animated.Value) => {
|
||||
Animated.sequence([
|
||||
Animated.timing(shakeAnimation, {
|
||||
Animated.timing(shakeAnimRef, {
|
||||
toValue: 10,
|
||||
duration: 100,
|
||||
easing: Easing.inOut(Easing.ease),
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(shakeAnimation, {
|
||||
Animated.timing(shakeAnimRef, {
|
||||
toValue: -10,
|
||||
duration: 100,
|
||||
easing: Easing.inOut(Easing.ease),
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(shakeAnimation, {
|
||||
Animated.timing(shakeAnimRef, {
|
||||
toValue: 5,
|
||||
duration: 100,
|
||||
easing: Easing.inOut(Easing.ease),
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(shakeAnimation, {
|
||||
Animated.timing(shakeAnimRef, {
|
||||
toValue: -5,
|
||||
duration: 100,
|
||||
easing: Easing.inOut(Easing.ease),
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
Animated.timing(shakeAnimation, {
|
||||
Animated.timing(shakeAnimRef, {
|
||||
toValue: 0,
|
||||
duration: 100,
|
||||
easing: Easing.inOut(Easing.ease),
|
||||
useNativeDriver: true,
|
||||
}),
|
||||
]).start(() => {
|
||||
confirmPasswordInputRef.current?.focus();
|
||||
confirmPasswordInputRef.current?.setNativeProps({ selection: { start: 0, end: confirmPassword.length } });
|
||||
});
|
||||
]).start();
|
||||
};
|
||||
|
||||
const handleShakeAnimation = () => {
|
||||
performShake(shakeAnimation);
|
||||
};
|
||||
|
||||
const handleSuccessAnimation = () => {
|
||||
|
@ -178,6 +180,17 @@ const PromptPasswordConfirmationModal = forwardRef<PromptPasswordConfirmationMod
|
|||
});
|
||||
};
|
||||
|
||||
const handleConfirmationFailure = () => {
|
||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
|
||||
if (!isSuccess) handleShakeAnimation();
|
||||
onConfirmationFailure();
|
||||
};
|
||||
|
||||
const handleConfirmSuccess = () => {
|
||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
|
||||
handleSuccessAnimation();
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
Keyboard.dismiss();
|
||||
setIsLoading(true);
|
||||
|
@ -187,37 +200,13 @@ const PromptPasswordConfirmationModal = forwardRef<PromptPasswordConfirmationMod
|
|||
if (modalType === MODAL_TYPES.CREATE_PASSWORD || modalType === MODAL_TYPES.CREATE_FAKE_STORAGE) {
|
||||
if (password === confirmPassword && password) {
|
||||
success = await onConfirmationSuccess(password);
|
||||
if (success) {
|
||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
|
||||
handleSuccessAnimation();
|
||||
} else {
|
||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
|
||||
onConfirmationFailure();
|
||||
if (!isSuccess) {
|
||||
// Prevent shake animation if success is detected
|
||||
handleShakeAnimation();
|
||||
}
|
||||
}
|
||||
success ? handleConfirmSuccess() : handleConfirmationFailure();
|
||||
} else {
|
||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
|
||||
if (!isSuccess) {
|
||||
// Prevent shake animation if success is detected
|
||||
handleShakeAnimation();
|
||||
}
|
||||
handleConfirmationFailure();
|
||||
}
|
||||
} else if (modalType === MODAL_TYPES.ENTER_PASSWORD) {
|
||||
success = await onConfirmationSuccess(password);
|
||||
if (success) {
|
||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
|
||||
handleSuccessAnimation();
|
||||
} else {
|
||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
|
||||
if (!isSuccess) {
|
||||
// Prevent shake animation if success is detected
|
||||
handleShakeAnimation();
|
||||
}
|
||||
onConfirmationFailure();
|
||||
}
|
||||
success ? handleConfirmSuccess() : handleConfirmationFailure();
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false); // Ensure loading state is reset
|
||||
|
@ -256,16 +245,18 @@ const PromptPasswordConfirmationModal = forwardRef<PromptPasswordConfirmationMod
|
|||
onConfirmationFailure();
|
||||
};
|
||||
|
||||
const opacity = isVisible ? 0 : 1;
|
||||
return (
|
||||
<BottomModal
|
||||
ref={modalRef}
|
||||
onDismiss={onModalDismiss}
|
||||
onClose={onModalDismiss}
|
||||
grabber={false}
|
||||
showCloseButton={!isSuccess}
|
||||
onCloseModalPressed={handleCancel}
|
||||
backgroundColor={colors.modal}
|
||||
scrollRef={scrollView}
|
||||
isGrabberVisible={!isSuccess}
|
||||
dismissible={false}
|
||||
sizes={Platform.OS === 'ios' ? ['auto'] : [420, 'auto']}
|
||||
footer={
|
||||
!isSuccess ? (
|
||||
showExplanation && modalType === MODAL_TYPES.CREATE_PASSWORD ? (
|
||||
|
@ -278,16 +269,19 @@ const PromptPasswordConfirmationModal = forwardRef<PromptPasswordConfirmationMod
|
|||
/>
|
||||
</Animated.View>
|
||||
) : (
|
||||
<Animated.View style={{ opacity: fadeOutAnimation, transform: [{ scale: scaleAnimation }] }}>
|
||||
<View style={styles.feeModalFooter}>
|
||||
<SecondButton
|
||||
title={isLoading ? '' : loc._.ok}
|
||||
onPress={handleSubmit}
|
||||
testID="OKButton"
|
||||
loading={isLoading}
|
||||
disabled={isLoading || !password || (modalType === MODAL_TYPES.CREATE_PASSWORD && !confirmPassword)}
|
||||
/>
|
||||
</View>
|
||||
<Animated.View
|
||||
style={[
|
||||
{ opacity: isVisible ? opacity : fadeOutAnimation, transform: [{ scale: scaleAnimation }] },
|
||||
styles.feeModalFooterSpacing,
|
||||
]}
|
||||
>
|
||||
<SecondButton
|
||||
title={isLoading ? '' : loc._.ok}
|
||||
onPress={handleSubmit}
|
||||
testID="OKButton"
|
||||
loading={isLoading}
|
||||
disabled={isLoading || !password || (modalType === MODAL_TYPES.CREATE_PASSWORD && !confirmPassword)}
|
||||
/>
|
||||
</Animated.View>
|
||||
)
|
||||
) : null
|
||||
|
@ -298,14 +292,14 @@ const PromptPasswordConfirmationModal = forwardRef<PromptPasswordConfirmationMod
|
|||
{modalType === MODAL_TYPES.CREATE_PASSWORD && showExplanation && (
|
||||
<Animated.View style={{ opacity: explanationOpacity }}>
|
||||
<Text style={[styles.textLabel, stylesHook.feeModalLabel]}>{loc.settings.encrypt_storage_explanation_headline}</Text>
|
||||
<Animated.ScrollView style={styles.explanationScrollView} ref={scrollView}>
|
||||
<Text style={[styles.description, stylesHook.feeModalCustomText]}>
|
||||
<Animated.View>
|
||||
<Text style={[styles.description, stylesHook.feeModalCustomText]} maxFontSizeMultiplier={1.2}>
|
||||
{loc.settings.encrypt_storage_explanation_description_line1}
|
||||
</Text>
|
||||
<Text style={[styles.description, stylesHook.feeModalCustomText]}>
|
||||
<Text style={[styles.description, stylesHook.feeModalCustomText]} maxFontSizeMultiplier={1.2}>
|
||||
{loc.settings.encrypt_storage_explanation_description_line2}
|
||||
</Text>
|
||||
</Animated.ScrollView>
|
||||
</Animated.View>
|
||||
<View style={styles.feeModalFooter} />
|
||||
</Animated.View>
|
||||
)}
|
||||
|
@ -394,32 +388,33 @@ export default PromptPasswordConfirmationModal;
|
|||
const styles = StyleSheet.create({
|
||||
modalContent: {
|
||||
padding: 22,
|
||||
width: '100%', // Ensure modal content takes full width
|
||||
width: '100%',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
minHeight: {
|
||||
minHeight: 260,
|
||||
minHeight: 420,
|
||||
},
|
||||
feeModalFooter: {
|
||||
paddingHorizontal: 16,
|
||||
padding: 16,
|
||||
},
|
||||
feeModalFooterSpacing: {
|
||||
paddingHorizontal: 16,
|
||||
padding: 24,
|
||||
marginVertical: 24,
|
||||
},
|
||||
inputContainer: {
|
||||
marginBottom: 10,
|
||||
width: '100%', // Ensure full width
|
||||
width: '100%',
|
||||
},
|
||||
input: {
|
||||
borderRadius: 4,
|
||||
padding: 8,
|
||||
marginVertical: 8,
|
||||
fontSize: 16,
|
||||
width: '100%', // Ensure full width
|
||||
width: '100%',
|
||||
},
|
||||
textLabel: {
|
||||
fontSize: 22,
|
||||
fontSize: 20,
|
||||
fontWeight: '600',
|
||||
marginBottom: 16,
|
||||
textAlign: 'center',
|
||||
|
@ -432,7 +427,8 @@ const styles = StyleSheet.create({
|
|||
successContainer: {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: 100,
|
||||
margin: 24,
|
||||
marginBottom: 48,
|
||||
},
|
||||
circle: {
|
||||
width: 60,
|
||||
|
@ -446,7 +442,4 @@ const styles = StyleSheet.create({
|
|||
color: 'white',
|
||||
fontSize: 30,
|
||||
},
|
||||
explanationScrollView: {
|
||||
maxHeight: 200,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -20,6 +20,8 @@ interface QRCodeComponentProps {
|
|||
onError?: () => void;
|
||||
}
|
||||
|
||||
const BORDER_WIDTH = 6;
|
||||
|
||||
const actionIcons: { [key: string]: ActionIcons } = {
|
||||
Share: {
|
||||
iconValue: 'square.and.arrow.up',
|
||||
|
@ -62,7 +64,7 @@ const QRCodeComponent: React.FC<QRCodeComponentProps> = ({
|
|||
onError = () => {},
|
||||
}) => {
|
||||
const qrCode = useRef<any>();
|
||||
const { colors } = useTheme();
|
||||
const { colors, dark } = useTheme();
|
||||
|
||||
const handleShareQRCode = () => {
|
||||
qrCode.current.toDataURL((data: string) => {
|
||||
|
@ -82,11 +84,17 @@ const QRCodeComponent: React.FC<QRCodeComponentProps> = ({
|
|||
}
|
||||
}, []);
|
||||
|
||||
// Adjust the size of the QR code to account for the border width
|
||||
const newSize = dark ? size - BORDER_WIDTH * 2 : size;
|
||||
const stylesHook = StyleSheet.create({
|
||||
container: { borderWidth: dark ? BORDER_WIDTH : 0 },
|
||||
});
|
||||
|
||||
const renderQRCode = (
|
||||
<QRCode
|
||||
value={value}
|
||||
{...(isLogoRendered ? { logo: require('../img/qr-code.png') } : {})}
|
||||
size={size}
|
||||
size={newSize}
|
||||
logoSize={logoSize}
|
||||
color="#000000"
|
||||
logoBackgroundColor={colors.brandingColor}
|
||||
|
@ -99,7 +107,7 @@ const QRCodeComponent: React.FC<QRCodeComponentProps> = ({
|
|||
|
||||
return (
|
||||
<View
|
||||
style={styles.qrCodeContainer}
|
||||
style={[styles.container, stylesHook.container]}
|
||||
testID="BitcoinAddressQRCodeContainer"
|
||||
accessibilityIgnoresInvertColors
|
||||
importantForAccessibility="no-hide-descendants"
|
||||
|
@ -120,5 +128,5 @@ const QRCodeComponent: React.FC<QRCodeComponentProps> = ({
|
|||
export default QRCodeComponent;
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
qrCodeContainer: { borderWidth: 6, borderRadius: 8, borderColor: '#FFFFFF' },
|
||||
container: { borderColor: '#FFFFFF' },
|
||||
});
|
||||
|
|
64
components/SeedWords.tsx
Normal file
|
@ -0,0 +1,64 @@
|
|||
import React from 'react';
|
||||
import { I18nManager, StyleSheet, Text, View } from 'react-native';
|
||||
|
||||
import { useTheme } from './themes';
|
||||
|
||||
const SeedWords = ({ seed }: { seed: string }) => {
|
||||
const words = seed.split(/\s/);
|
||||
const { colors } = useTheme();
|
||||
|
||||
const stylesHook = StyleSheet.create({
|
||||
word: {
|
||||
backgroundColor: colors.inputBackgroundColor,
|
||||
},
|
||||
wortText: {
|
||||
color: colors.labelText,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<View style={styles.secret}>
|
||||
{words.map((secret, index) => {
|
||||
const text = `${index + 1}. ${secret} `;
|
||||
return (
|
||||
<View style={[styles.word, stylesHook.word]} key={index}>
|
||||
<Text style={[styles.wortText, stylesHook.wortText]} textBreakStrategy="simple">
|
||||
{text}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
<Text style={styles.hiddenText} testID="Secret">
|
||||
{seed}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
word: {
|
||||
marginRight: 8,
|
||||
marginBottom: 8,
|
||||
paddingTop: 6,
|
||||
paddingBottom: 6,
|
||||
paddingLeft: 8,
|
||||
paddingRight: 8,
|
||||
borderRadius: 4,
|
||||
},
|
||||
wortText: {
|
||||
fontWeight: 'bold',
|
||||
textAlign: 'left',
|
||||
fontSize: 17,
|
||||
},
|
||||
secret: {
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'center',
|
||||
flexDirection: I18nManager.isRTL ? 'row-reverse' : 'row',
|
||||
},
|
||||
hiddenText: {
|
||||
height: 0,
|
||||
width: 0,
|
||||
},
|
||||
});
|
||||
|
||||
export default SeedWords;
|
|
@ -1,4 +1,4 @@
|
|||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { requireNativeComponent, View, StyleSheet, NativeSyntheticEvent } from 'react-native';
|
||||
|
||||
interface SegmentedControlProps {
|
||||
|
@ -21,9 +21,18 @@ interface NativeSegmentedControlProps {
|
|||
const NativeSegmentedControl = requireNativeComponent<NativeSegmentedControlProps>('CustomSegmentedControl');
|
||||
|
||||
const SegmentedControl: React.FC<SegmentedControlProps> = ({ values, selectedIndex, onChange }) => {
|
||||
const handleChange = (event: NativeSyntheticEvent<SegmentedControlEvent>) => {
|
||||
onChange(event.nativeEvent.selectedIndex);
|
||||
};
|
||||
const handleChange = useMemo(
|
||||
() => (event: NativeSyntheticEvent<SegmentedControlEvent>) => {
|
||||
if (event?.nativeEvent?.selectedIndex !== undefined) {
|
||||
onChange(event.nativeEvent.selectedIndex);
|
||||
}
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
|
||||
if (!Array.isArray(values) || values.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
|
|
|
@ -109,8 +109,8 @@ const SelectFeeModal = forwardRef<BottomModalHandle, SelectFeeModalProps>(
|
|||
});
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
present: async () => feeModalRef.current?.present(),
|
||||
dismiss: async () => feeModalRef.current?.dismiss(),
|
||||
present: async () => await feeModalRef.current?.present(),
|
||||
dismiss: async () => await feeModalRef.current?.dismiss(),
|
||||
}));
|
||||
|
||||
const options: Option[] = [
|
||||
|
@ -163,8 +163,8 @@ const SelectFeeModal = forwardRef<BottomModalHandle, SelectFeeModalProps>(
|
|||
|
||||
const handleSelectOption = async (fee: number | null, rate: number) => {
|
||||
setFeePrecalc(fp => ({ ...fp, current: fee }));
|
||||
await feeModalRef.current?.dismiss();
|
||||
setCustomFee(rate.toString());
|
||||
await feeModalRef.current?.dismiss();
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -340,14 +340,13 @@ const styles = StyleSheet.create({
|
|||
feeModalFooter: {
|
||||
paddingVertical: 46,
|
||||
flexDirection: 'row',
|
||||
|
||||
justifyContent: 'center',
|
||||
alignContent: 'center',
|
||||
alignItems: 'center',
|
||||
minHeight: 80,
|
||||
},
|
||||
feeModalFooterSpacing: {
|
||||
paddingHorizontal: 24,
|
||||
paddingHorizontal: 16,
|
||||
},
|
||||
memo: {
|
||||
flexDirection: 'row',
|
||||
|
|
73
components/TipBox.tsx
Normal file
|
@ -0,0 +1,73 @@
|
|||
import React from 'react';
|
||||
import { View, StyleSheet, ViewStyle } from 'react-native';
|
||||
import { useTheme } from './themes';
|
||||
import { BlueText } from '../BlueComponents';
|
||||
|
||||
interface TipBoxProps {
|
||||
number?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
additionalDescription?: string;
|
||||
containerStyle?: ViewStyle;
|
||||
}
|
||||
|
||||
const TipBox: React.FC<TipBoxProps> = ({ number, title, description, additionalDescription, containerStyle }) => {
|
||||
const { colors } = useTheme();
|
||||
const stylesHook = StyleSheet.create({
|
||||
tipBox: {
|
||||
backgroundColor: colors.ballOutgoingExpired,
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
marginBottom: 24,
|
||||
...containerStyle,
|
||||
},
|
||||
tipHeader: {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
marginBottom: number || title ? 16 : 0,
|
||||
},
|
||||
tipHeaderText: {
|
||||
marginLeft: 4,
|
||||
flex: 1,
|
||||
},
|
||||
description: {
|
||||
marginBottom: additionalDescription ? 16 : 0,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<View style={stylesHook.tipBox}>
|
||||
{(number || title) && (
|
||||
<View style={stylesHook.tipHeader}>
|
||||
{number && (
|
||||
<View style={styles.vaultKeyCircle}>
|
||||
<BlueText style={styles.vaultKeyText}>{number}</BlueText>
|
||||
</View>
|
||||
)}
|
||||
{title && (
|
||||
<BlueText bold style={stylesHook.tipHeaderText}>
|
||||
{title}
|
||||
</BlueText>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
{description && <BlueText style={stylesHook.description}>{description}</BlueText>}
|
||||
{additionalDescription && <BlueText>{additionalDescription}</BlueText>}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
vaultKeyCircle: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
vaultKeyText: {
|
||||
fontSize: 18,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
});
|
||||
|
||||
export default TipBox;
|
|
@ -1,26 +1,15 @@
|
|||
import React, { Ref, useCallback, useMemo } from 'react';
|
||||
import { Platform, Pressable, TouchableOpacity } from 'react-native';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { Platform, TouchableOpacity } from 'react-native';
|
||||
import { MenuView, MenuAction, NativeActionEvent } from '@react-native-menu/menu';
|
||||
import {
|
||||
ContextMenuView,
|
||||
RenderItem,
|
||||
OnPressMenuItemEventObject,
|
||||
MenuState,
|
||||
IconConfig,
|
||||
MenuElementConfig,
|
||||
} from 'react-native-ios-context-menu';
|
||||
import { ToolTipMenuProps, Action } from './types';
|
||||
import { useSettings } from '../hooks/context/useSettings';
|
||||
|
||||
const ToolTipMenu = React.memo((props: ToolTipMenuProps, ref?: Ref<any>) => {
|
||||
const ToolTipMenu = (props: ToolTipMenuProps) => {
|
||||
const {
|
||||
title = '',
|
||||
isMenuPrimaryAction = false,
|
||||
renderPreview,
|
||||
disabled = false,
|
||||
onPress,
|
||||
onMenuWillShow,
|
||||
onMenuWillHide,
|
||||
buttonStyle,
|
||||
onPressMenuItem,
|
||||
children,
|
||||
|
@ -30,50 +19,59 @@ const ToolTipMenu = React.memo((props: ToolTipMenuProps, ref?: Ref<any>) => {
|
|||
|
||||
const { language } = useSettings();
|
||||
|
||||
// Map Menu Items for iOS Context Menu
|
||||
const mapMenuItemForContextMenuView = useCallback((action: Action) => {
|
||||
if (!action.id) return null;
|
||||
return {
|
||||
actionKey: action.id.toString(),
|
||||
actionTitle: action.text,
|
||||
icon: action.icon?.iconValue ? ({ iconType: 'SYSTEM', iconValue: action.icon.iconValue } as IconConfig) : undefined,
|
||||
state: action.menuState ?? undefined,
|
||||
attributes: action.disabled ? ['disabled'] : [],
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Map Menu Items for RN Menu (supports subactions and displayInline)
|
||||
const mapMenuItemForMenuView = useCallback((action: Action): MenuAction | null => {
|
||||
if (!action.id) return null;
|
||||
|
||||
// Check for subactions
|
||||
const subactions =
|
||||
action.subactions?.map(subaction => ({
|
||||
id: subaction.id.toString(),
|
||||
title: subaction.text,
|
||||
subtitle: subaction.subtitle,
|
||||
image: subaction.icon?.iconValue ? subaction.icon.iconValue : undefined,
|
||||
state: subaction.menuState === undefined ? undefined : ((subaction.menuState ? 'on' : 'off') as MenuState),
|
||||
attributes: { disabled: subaction.disabled, destructive: subaction.destructive, hidden: subaction.hidden },
|
||||
})) || [];
|
||||
action.subactions?.map(subaction => {
|
||||
const subMenuItem: MenuAction = {
|
||||
id: subaction.id.toString(),
|
||||
title: subaction.text,
|
||||
subtitle: subaction.subtitle,
|
||||
image: subaction.icon?.iconValue ? subaction.icon.iconValue : undefined,
|
||||
attributes: { disabled: subaction.disabled, destructive: subaction.destructive, hidden: subaction.hidden },
|
||||
};
|
||||
if ('menuState' in subaction) {
|
||||
subMenuItem.state = subaction.menuState ? 'on' : 'off';
|
||||
}
|
||||
if (subaction.subactions && subaction.subactions.length > 0) {
|
||||
const deepSubactions = subaction.subactions.map(deepSub => {
|
||||
const deepMenuItem: MenuAction = {
|
||||
id: deepSub.id.toString(),
|
||||
title: deepSub.text,
|
||||
subtitle: deepSub.subtitle,
|
||||
image: deepSub.icon?.iconValue ? deepSub.icon.iconValue : undefined,
|
||||
attributes: { disabled: deepSub.disabled, destructive: deepSub.destructive, hidden: deepSub.hidden },
|
||||
};
|
||||
if ('menuState' in deepSub) {
|
||||
deepMenuItem.state = deepSub.menuState ? 'on' : 'off';
|
||||
}
|
||||
return deepMenuItem;
|
||||
});
|
||||
subMenuItem.subactions = deepSubactions;
|
||||
}
|
||||
return subMenuItem;
|
||||
}) || [];
|
||||
|
||||
return {
|
||||
const menuItem: MenuAction = {
|
||||
id: action.id.toString(),
|
||||
title: action.text,
|
||||
subtitle: action.subtitle,
|
||||
image: action.icon?.iconValue ? action.icon.iconValue : undefined,
|
||||
state: action.menuState === undefined ? undefined : ((action.menuState ? 'on' : 'off') as MenuState),
|
||||
attributes: { disabled: action.disabled, destructive: action.destructive, hidden: action.hidden },
|
||||
subactions: subactions.length > 0 ? subactions : undefined,
|
||||
displayInline: action.displayInline || false,
|
||||
};
|
||||
if ('menuState' in action) {
|
||||
menuItem.state = action.menuState ? 'on' : 'off';
|
||||
}
|
||||
if (subactions.length > 0) {
|
||||
menuItem.subactions = subactions;
|
||||
}
|
||||
return menuItem;
|
||||
}, []);
|
||||
|
||||
const contextMenuItems = useMemo(() => {
|
||||
const flattenedActions = props.actions.flat().filter(action => action.id);
|
||||
return flattenedActions.map(mapMenuItemForContextMenuView).filter(item => item !== null) as MenuElementConfig[];
|
||||
}, [props.actions, mapMenuItemForContextMenuView]);
|
||||
|
||||
const menuViewItemsIOS = useMemo(() => {
|
||||
return props.actions
|
||||
.map(actionGroup => {
|
||||
|
@ -100,13 +98,6 @@ const ToolTipMenu = React.memo((props: ToolTipMenuProps, ref?: Ref<any>) => {
|
|||
return mergedActions.map(mapMenuItemForMenuView).filter(item => item !== null) as MenuAction[];
|
||||
}, [props.actions, mapMenuItemForMenuView]);
|
||||
|
||||
const handlePressMenuItemForContextMenuView = useCallback(
|
||||
(event: OnPressMenuItemEventObject) => {
|
||||
onPressMenuItem(event.nativeEvent.actionKey);
|
||||
},
|
||||
[onPressMenuItem],
|
||||
);
|
||||
|
||||
const handlePressMenuItemForMenuView = useCallback(
|
||||
({ nativeEvent }: NativeActionEvent) => {
|
||||
onPressMenuItem(nativeEvent.event);
|
||||
|
@ -114,46 +105,6 @@ const ToolTipMenu = React.memo((props: ToolTipMenuProps, ref?: Ref<any>) => {
|
|||
[onPressMenuItem],
|
||||
);
|
||||
|
||||
const renderContextMenuView = () => {
|
||||
return (
|
||||
<ContextMenuView
|
||||
lazyPreview
|
||||
accessibilityLabel={props.accessibilityLabel}
|
||||
accessibilityHint={props.accessibilityHint}
|
||||
accessibilityRole={props.accessibilityRole}
|
||||
accessibilityState={props.accessibilityState}
|
||||
accessibilityLanguage={language}
|
||||
shouldEnableAggressiveCleanup
|
||||
internalCleanupMode="automatic"
|
||||
onPressMenuItem={handlePressMenuItemForContextMenuView}
|
||||
onMenuWillShow={onMenuWillShow}
|
||||
onMenuWillHide={onMenuWillHide}
|
||||
useActionSheetFallback={false}
|
||||
menuConfig={{
|
||||
menuTitle: title,
|
||||
menuItems: contextMenuItems,
|
||||
}}
|
||||
{...(renderPreview
|
||||
? {
|
||||
previewConfig: {
|
||||
previewType: 'CUSTOM',
|
||||
backgroundColor: 'white',
|
||||
},
|
||||
renderPreview: renderPreview as RenderItem,
|
||||
}
|
||||
: {})}
|
||||
>
|
||||
{onPress ? (
|
||||
<Pressable accessibilityRole="button" onPress={onPress} {...restProps}>
|
||||
{children}
|
||||
</Pressable>
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</ContextMenuView>
|
||||
);
|
||||
};
|
||||
|
||||
const renderMenuView = () => {
|
||||
return (
|
||||
<MenuView
|
||||
|
@ -179,7 +130,7 @@ const ToolTipMenu = React.memo((props: ToolTipMenuProps, ref?: Ref<any>) => {
|
|||
);
|
||||
};
|
||||
|
||||
return props.actions.length > 0 ? (Platform.OS === 'ios' && renderPreview ? renderContextMenuView() : renderMenuView()) : null;
|
||||
});
|
||||
return props.actions.length > 0 ? renderMenuView() : null;
|
||||
};
|
||||
|
||||
export default ToolTipMenu;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState, memo } from 'react';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
import Clipboard from '@react-native-clipboard/clipboard';
|
||||
import { Linking, View, ViewStyle } from 'react-native';
|
||||
|
@ -36,7 +36,7 @@ interface TransactionListItemProps {
|
|||
|
||||
type NavigationProps = NativeStackNavigationProp<DetailViewStackParamList>;
|
||||
|
||||
export const TransactionListItem: React.FC<TransactionListItemProps> = React.memo(
|
||||
export const TransactionListItem: React.FC<TransactionListItemProps> = memo(
|
||||
({ item, itemPriceUnit = BitcoinUnit.BTC, walletID, searchQuery, style, renderHighlightedText }) => {
|
||||
const [subtitleNumberOfLines, setSubtitleNumberOfLines] = useState(1);
|
||||
const { colors } = useTheme();
|
||||
|
@ -46,10 +46,10 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
|
|||
const { language, selectedBlockExplorer } = useSettings();
|
||||
const containerStyle = useMemo(
|
||||
() => ({
|
||||
backgroundColor: 'transparent',
|
||||
backgroundColor: colors.background,
|
||||
borderBottomColor: colors.lightBorder,
|
||||
}),
|
||||
[colors.lightBorder],
|
||||
[colors.background, colors.lightBorder],
|
||||
);
|
||||
|
||||
const combinedStyle = useMemo(() => [containerStyle, style], [containerStyle, style]);
|
||||
|
@ -81,28 +81,23 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
|
|||
return sub || undefined;
|
||||
}, [txMemo, item.confirmations, item.memo]);
|
||||
|
||||
const formattedAmount = useMemo(() => {
|
||||
return formatBalanceWithoutSuffix(item.value && item.value, itemPriceUnit, true).toString();
|
||||
}, [item.value, itemPriceUnit]);
|
||||
|
||||
const rowTitle = useMemo(() => {
|
||||
if (item.type === 'user_invoice' || item.type === 'payment_request') {
|
||||
if (isNaN(Number(item.value))) {
|
||||
item.value = 0;
|
||||
}
|
||||
const currentDate = new Date();
|
||||
const now = (currentDate.getTime() / 1000) | 0; // eslint-disable-line no-bitwise
|
||||
const now = Math.floor(currentDate.getTime() / 1000);
|
||||
const invoiceExpiration = item.timestamp! + item.expire_time!;
|
||||
|
||||
if (invoiceExpiration > now) {
|
||||
return formatBalanceWithoutSuffix(item.value && item.value, itemPriceUnit, true).toString();
|
||||
if (invoiceExpiration > now || item.ispaid) {
|
||||
return formattedAmount;
|
||||
} else {
|
||||
if (item.ispaid) {
|
||||
return formatBalanceWithoutSuffix(item.value && item.value, itemPriceUnit, true).toString();
|
||||
} else {
|
||||
return loc.lnd.expired;
|
||||
}
|
||||
return loc.lnd.expired;
|
||||
}
|
||||
} else {
|
||||
return formatBalanceWithoutSuffix(item.value && item.value, itemPriceUnit, true).toString();
|
||||
}
|
||||
}, [item, itemPriceUnit]);
|
||||
return formattedAmount;
|
||||
}, [item, formattedAmount]);
|
||||
|
||||
const rowTitleStyle = useMemo(() => {
|
||||
let color = colors.successColor;
|
||||
|
@ -164,6 +159,11 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
|
|||
label: loc.transactions.expired_transaction,
|
||||
icon: <TransactionExpiredIcon />,
|
||||
};
|
||||
} else if (!item.ispaid) {
|
||||
return {
|
||||
label: loc.transactions.expired_transaction,
|
||||
icon: <TransactionPendingIcon />,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
label: loc.transactions.incoming_transaction,
|
||||
|
@ -193,10 +193,9 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
|
|||
const { label: transactionTypeLabel, icon: avatar } = determineTransactionTypeAndAvatar();
|
||||
|
||||
const amountWithUnit = useMemo(() => {
|
||||
const amount = formatBalanceWithoutSuffix(item.value && item.value, itemPriceUnit, true).toString();
|
||||
const unit = itemPriceUnit === BitcoinUnit.BTC || itemPriceUnit === BitcoinUnit.SATS ? ` ${itemPriceUnit}` : ' ';
|
||||
return `${amount}${unit}`;
|
||||
}, [item.value, itemPriceUnit]);
|
||||
const unitSuffix = itemPriceUnit === BitcoinUnit.BTC || itemPriceUnit === BitcoinUnit.SATS ? ` ${itemPriceUnit}` : ' ';
|
||||
return `${formattedAmount}${unitSuffix}`;
|
||||
}, [formattedAmount, itemPriceUnit]);
|
||||
|
||||
useEffect(() => {
|
||||
setSubtitleNumberOfLines(1);
|
||||
|
@ -221,7 +220,7 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
|
|||
}
|
||||
const loaded = await LN.loadSuccessfulPayment(paymentHash);
|
||||
if (loaded) {
|
||||
navigate('ScanLndInvoiceRoot', {
|
||||
navigate('ScanLNDInvoiceRoot', {
|
||||
screen: 'LnurlPaySuccess',
|
||||
params: {
|
||||
paymentHash,
|
||||
|
@ -247,7 +246,19 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
|
|||
setSubtitleNumberOfLines(0);
|
||||
}, []);
|
||||
|
||||
const subtitleProps = useMemo(() => ({ numberOfLines: subtitleNumberOfLines }), [subtitleNumberOfLines]);
|
||||
const handleOnDetailsPress = useCallback(() => {
|
||||
if (walletID && item && item.hash) {
|
||||
navigate('TransactionDetails', { tx: item, hash: item.hash, walletID });
|
||||
} else {
|
||||
const lightningWallet = wallets.find(wallet => wallet?.getID() === item.walletID);
|
||||
if (lightningWallet) {
|
||||
navigate('LNDViewInvoice', {
|
||||
invoice: item,
|
||||
walletID: lightningWallet.getID(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [item, navigate, walletID, wallets]);
|
||||
|
||||
const handleOnCopyAmountTap = useCallback(() => Clipboard.setString(rowTitle.replace(/[\s\\-]/g, '')), [rowTitle]);
|
||||
const handleOnCopyTransactionID = useCallback(() => Clipboard.setString(item.hash), [item.hash]);
|
||||
|
@ -278,6 +289,8 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
|
|||
handleCopyOpenInBlockExplorerPress();
|
||||
} else if (id === CommonToolTipActions.CopyTXID.id) {
|
||||
handleOnCopyTransactionID();
|
||||
} else if (id === CommonToolTipActions.Details.id) {
|
||||
handleOnDetailsPress();
|
||||
}
|
||||
},
|
||||
[
|
||||
|
@ -285,31 +298,40 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
|
|||
handleOnCopyAmountTap,
|
||||
handleOnCopyNote,
|
||||
handleOnCopyTransactionID,
|
||||
handleOnDetailsPress,
|
||||
handleOnExpandNote,
|
||||
handleOnViewOnBlockExplorer,
|
||||
],
|
||||
);
|
||||
const toolTipActions = useMemo((): Action[] => {
|
||||
const actions: (Action | Action[])[] = [];
|
||||
|
||||
if (rowTitle !== loc.lnd.expired) {
|
||||
actions.push(CommonToolTipActions.CopyAmount);
|
||||
}
|
||||
|
||||
if (subtitle) {
|
||||
actions.push(CommonToolTipActions.CopyNote);
|
||||
}
|
||||
|
||||
if (item.hash) {
|
||||
actions.push(CommonToolTipActions.CopyTXID, CommonToolTipActions.CopyBlockExplorerLink, [CommonToolTipActions.OpenInBlockExplorer]);
|
||||
}
|
||||
|
||||
if (subtitle && subtitleNumberOfLines === 1) {
|
||||
actions.push([CommonToolTipActions.ExpandNote]);
|
||||
}
|
||||
const actions: (Action | Action[])[] = [
|
||||
{
|
||||
...CommonToolTipActions.CopyAmount,
|
||||
hidden: rowTitle === loc.lnd.expired,
|
||||
},
|
||||
{
|
||||
...CommonToolTipActions.CopyNote,
|
||||
hidden: !subtitle,
|
||||
},
|
||||
{
|
||||
...CommonToolTipActions.CopyTXID,
|
||||
hidden: !item.hash,
|
||||
},
|
||||
{
|
||||
...CommonToolTipActions.CopyBlockExplorerLink,
|
||||
hidden: !item.hash,
|
||||
},
|
||||
[{ ...CommonToolTipActions.OpenInBlockExplorer, hidden: !item.hash }, CommonToolTipActions.Details],
|
||||
[
|
||||
{
|
||||
...CommonToolTipActions.ExpandNote,
|
||||
hidden: subtitleNumberOfLines !== 1,
|
||||
},
|
||||
],
|
||||
];
|
||||
|
||||
return actions as Action[];
|
||||
}, [item.hash, subtitle, rowTitle, subtitleNumberOfLines]);
|
||||
}, [rowTitle, subtitle, item.hash, subtitleNumberOfLines]);
|
||||
|
||||
const accessibilityState = useMemo(() => {
|
||||
return {
|
||||
|
@ -317,6 +339,8 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
|
|||
};
|
||||
}, [subtitleNumberOfLines]);
|
||||
|
||||
const subtitleProps = useMemo(() => ({ numberOfLines: subtitleNumberOfLines }), [subtitleNumberOfLines]);
|
||||
|
||||
return (
|
||||
<ToolTipMenu
|
||||
isButton
|
||||
|
@ -338,8 +362,17 @@ export const TransactionListItem: React.FC<TransactionListItemProps> = React.mem
|
|||
rightTitle={rowTitle}
|
||||
rightTitleStyle={rowTitleStyle}
|
||||
containerStyle={combinedStyle}
|
||||
testID="TransactionListItem"
|
||||
/>
|
||||
</ToolTipMenu>
|
||||
);
|
||||
},
|
||||
(prevProps, nextProps) => {
|
||||
return (
|
||||
prevProps.item.hash === nextProps.item.hash &&
|
||||
prevProps.item.received === nextProps.item.received &&
|
||||
prevProps.itemPriceUnit === nextProps.itemPriceUnit &&
|
||||
prevProps.walletID === nextProps.walletID
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
@ -22,13 +22,13 @@ interface TransactionsNavigationHeaderProps {
|
|||
}
|
||||
|
||||
const TransactionsNavigationHeader: React.FC<TransactionsNavigationHeaderProps> = ({
|
||||
wallet: initialWallet,
|
||||
wallet,
|
||||
onWalletUnitChange,
|
||||
onManageFundsPressed,
|
||||
onWalletBalanceVisibilityChange,
|
||||
unit = BitcoinUnit.BTC,
|
||||
}) => {
|
||||
const [wallet, setWallet] = useState(initialWallet);
|
||||
const { hideBalance } = wallet;
|
||||
const [allowOnchainAddress, setAllowOnchainAddress] = useState(false);
|
||||
const { preferredFiatCurrency } = useSettings();
|
||||
|
||||
|
@ -44,10 +44,6 @@ const TransactionsNavigationHeader: React.FC<TransactionsNavigationHeaderProps>
|
|||
}
|
||||
}, [wallet]);
|
||||
|
||||
useEffect(() => {
|
||||
setWallet(initialWallet);
|
||||
}, [initialWallet]);
|
||||
|
||||
useEffect(() => {
|
||||
verifyIfWalletAllowsOnchainAddress();
|
||||
}, [wallet, verifyIfWalletAllowsOnchainAddress]);
|
||||
|
@ -60,8 +56,8 @@ const TransactionsNavigationHeader: React.FC<TransactionsNavigationHeaderProps>
|
|||
}, [unit, wallet]);
|
||||
|
||||
const handleBalanceVisibility = useCallback(() => {
|
||||
onWalletBalanceVisibilityChange?.(!wallet.hideBalance);
|
||||
}, [onWalletBalanceVisibilityChange, wallet.hideBalance]);
|
||||
onWalletBalanceVisibilityChange?.(!hideBalance);
|
||||
}, [onWalletBalanceVisibilityChange, hideBalance]);
|
||||
|
||||
const changeWalletBalanceUnit = () => {
|
||||
let newWalletPreferredUnit = wallet.getPreferredBalanceUnit();
|
||||
|
@ -112,17 +108,17 @@ const TransactionsNavigationHeader: React.FC<TransactionsNavigationHeaderProps>
|
|||
];
|
||||
}, []);
|
||||
|
||||
const balance = useMemo(() => {
|
||||
const hideBalance = wallet.hideBalance;
|
||||
const balanceFormatted =
|
||||
unit === BitcoinUnit.LOCAL_CURRENCY
|
||||
? formatBalance(wallet.getBalance(), unit, true)
|
||||
: formatBalanceWithoutSuffix(wallet.getBalance(), unit, true);
|
||||
return !hideBalance && balanceFormatted;
|
||||
}, [unit, wallet]);
|
||||
const currentBalance = wallet ? wallet.getBalance() : 0;
|
||||
const formattedBalance = useMemo(() => {
|
||||
return unit === BitcoinUnit.LOCAL_CURRENCY
|
||||
? formatBalance(currentBalance, unit, true)
|
||||
: formatBalanceWithoutSuffix(currentBalance, unit, true);
|
||||
}, [unit, currentBalance]);
|
||||
|
||||
const balance = !wallet.hideBalance && formattedBalance;
|
||||
|
||||
const toolTipWalletBalanceActions = useMemo(() => {
|
||||
return wallet.hideBalance
|
||||
return hideBalance
|
||||
? [
|
||||
{
|
||||
id: 'walletBalanceVisibility',
|
||||
|
@ -148,7 +144,7 @@ const TransactionsNavigationHeader: React.FC<TransactionsNavigationHeaderProps>
|
|||
},
|
||||
},
|
||||
];
|
||||
}, [wallet.hideBalance]);
|
||||
}, [hideBalance]);
|
||||
|
||||
const imageSource = useMemo(() => {
|
||||
switch (wallet.type) {
|
||||
|
@ -162,7 +158,7 @@ const TransactionsNavigationHeader: React.FC<TransactionsNavigationHeaderProps>
|
|||
}, [wallet.type]);
|
||||
|
||||
useAnimateOnChange(balance);
|
||||
useAnimateOnChange(wallet.hideBalance);
|
||||
useAnimateOnChange(hideBalance);
|
||||
useAnimateOnChange(unit);
|
||||
useAnimateOnChange(wallet.getID?.());
|
||||
|
||||
|
@ -187,7 +183,7 @@ const TransactionsNavigationHeader: React.FC<TransactionsNavigationHeaderProps>
|
|||
actions={toolTipWalletBalanceActions}
|
||||
>
|
||||
<View style={styles.walletBalance}>
|
||||
{wallet.hideBalance ? (
|
||||
{hideBalance ? (
|
||||
<BlurredBalanceView />
|
||||
) : (
|
||||
<View>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { forwardRef, useCallback, useImperativeHandle, useRef } from 'react';
|
||||
import React, { forwardRef, useCallback, useImperativeHandle, useMemo, useRef } from 'react';
|
||||
import {
|
||||
Animated,
|
||||
FlatList,
|
||||
|
@ -98,8 +98,12 @@ interface WalletCarouselItemProps {
|
|||
isSelectedWallet?: boolean;
|
||||
customStyle?: ViewStyle;
|
||||
horizontal?: boolean;
|
||||
isPlaceHolder?: boolean;
|
||||
searchQuery?: string;
|
||||
renderHighlightedText?: (text: string, query: string) => JSX.Element;
|
||||
animationsEnabled?: boolean;
|
||||
onPressIn?: () => void;
|
||||
onPressOut?: () => void;
|
||||
}
|
||||
|
||||
const iStyles = StyleSheet.create({
|
||||
|
@ -161,19 +165,6 @@ const iStyles = StyleSheet.create({
|
|||
},
|
||||
});
|
||||
|
||||
interface WalletCarouselItemProps {
|
||||
item: TWallet;
|
||||
onPress: (item: TWallet) => void;
|
||||
handleLongPress?: () => void;
|
||||
isSelectedWallet?: boolean;
|
||||
customStyle?: ViewStyle;
|
||||
horizontal?: boolean;
|
||||
isPlaceHolder?: boolean;
|
||||
searchQuery?: string;
|
||||
renderHighlightedText?: (text: string, query: string) => JSX.Element;
|
||||
animationsEnabled?: boolean;
|
||||
}
|
||||
|
||||
export const WalletCarouselItem: React.FC<WalletCarouselItemProps> = React.memo(
|
||||
({
|
||||
item,
|
||||
|
@ -186,6 +177,8 @@ export const WalletCarouselItem: React.FC<WalletCarouselItemProps> = React.memo(
|
|||
renderHighlightedText,
|
||||
animationsEnabled = true,
|
||||
isPlaceHolder = false,
|
||||
onPressIn,
|
||||
onPressOut,
|
||||
}) => {
|
||||
const scaleValue = useRef(new Animated.Value(1.0)).current;
|
||||
const { colors } = useTheme();
|
||||
|
@ -194,32 +187,31 @@ export const WalletCarouselItem: React.FC<WalletCarouselItemProps> = React.memo(
|
|||
const itemWidth = width * 0.82 > 375 ? 375 : width * 0.82;
|
||||
const { isLargeScreen } = useIsLargeScreen();
|
||||
|
||||
const springConfig = useMemo(() => ({ useNativeDriver: true, tension: 100 }), []);
|
||||
const animateScale = useCallback(
|
||||
(toValue: number, callback?: () => void) => {
|
||||
Animated.spring(scaleValue, { toValue, ...springConfig }).start(callback);
|
||||
},
|
||||
[scaleValue, springConfig],
|
||||
);
|
||||
|
||||
const onPressedIn = useCallback(() => {
|
||||
if (animationsEnabled) {
|
||||
Animated.spring(scaleValue, {
|
||||
toValue: 0.95,
|
||||
useNativeDriver: true,
|
||||
friction: 3,
|
||||
tension: 100,
|
||||
}).start();
|
||||
animateScale(0.95);
|
||||
}
|
||||
}, [scaleValue, animationsEnabled]);
|
||||
if (onPressIn) onPressIn();
|
||||
}, [animateScale, animationsEnabled, onPressIn]);
|
||||
|
||||
const onPressedOut = useCallback(() => {
|
||||
if (animationsEnabled) {
|
||||
Animated.spring(scaleValue, {
|
||||
toValue: 1.0,
|
||||
useNativeDriver: true,
|
||||
friction: 3,
|
||||
tension: 100,
|
||||
}).start();
|
||||
animateScale(1.0);
|
||||
}
|
||||
}, [scaleValue, animationsEnabled]);
|
||||
if (onPressOut) onPressOut();
|
||||
}, [animateScale, animationsEnabled, onPressOut]);
|
||||
|
||||
const handlePress = useCallback(() => {
|
||||
onPressedOut();
|
||||
onPress(item);
|
||||
}, [item, onPress, onPressedOut]);
|
||||
}, [item, onPress]);
|
||||
|
||||
const opacity = isSelectedWallet === false ? 0.5 : 1.0;
|
||||
let image;
|
||||
|
@ -261,6 +253,8 @@ export const WalletCarouselItem: React.FC<WalletCarouselItemProps> = React.memo(
|
|||
if (handleLongPress) handleLongPress();
|
||||
}}
|
||||
onPress={handlePress}
|
||||
delayHoverIn={0}
|
||||
delayHoverOut={0}
|
||||
>
|
||||
<View style={[iStyles.shadowContainer, { backgroundColor: colors.background, shadowColor: colors.shadowColor }]}>
|
||||
<LinearGradient colors={WalletGradient.gradientsFor(item.type)} style={iStyles.grad}>
|
||||
|
@ -356,6 +350,10 @@ const WalletsCarousel = forwardRef<FlatListRefType, WalletsCarouselProps>((props
|
|||
renderHighlightedText,
|
||||
isFlatList = true,
|
||||
} = props;
|
||||
|
||||
const { width } = useWindowDimensions();
|
||||
const itemWidth = React.useMemo(() => (width * 0.82 > 375 ? 375 : width * 0.82), [width]);
|
||||
|
||||
const renderItem = useCallback(
|
||||
({ item, index }: ListRenderItemInfo<TWallet>) =>
|
||||
item ? (
|
||||
|
@ -373,7 +371,6 @@ const WalletsCarousel = forwardRef<FlatListRefType, WalletsCarouselProps>((props
|
|||
);
|
||||
|
||||
const flatListRef = useRef<FlatList<any>>(null);
|
||||
|
||||
useImperativeHandle(ref, (): any => {
|
||||
return {
|
||||
scrollToEnd: (params: { animated?: boolean | null | undefined } | undefined) => flatListRef.current?.scrollToEnd(params),
|
||||
|
@ -395,10 +392,8 @@ const WalletsCarousel = forwardRef<FlatListRefType, WalletsCarouselProps>((props
|
|||
getNativeScrollRef: () => flatListRef.current?.getNativeScrollRef(),
|
||||
};
|
||||
}, []);
|
||||
|
||||
const onScrollToIndexFailed = (error: { averageItemLength: number; index: number }): void => {
|
||||
console.debug('onScrollToIndexFailed');
|
||||
console.debug(error);
|
||||
console.debug('onScrollToIndexFailed', error);
|
||||
flatListRef.current?.scrollToOffset({ offset: error.averageItemLength * error.index, animated: true });
|
||||
setTimeout(() => {
|
||||
if (data.length !== 0 && flatListRef.current !== null) {
|
||||
|
@ -407,16 +402,16 @@ const WalletsCarousel = forwardRef<FlatListRefType, WalletsCarouselProps>((props
|
|||
}, 100);
|
||||
};
|
||||
|
||||
const { width } = useWindowDimensions();
|
||||
const sliderHeight = 195;
|
||||
const itemWidth = width * 0.82 > 375 ? 375 : width * 0.82;
|
||||
|
||||
const keyExtractor = useCallback((item: TWallet, index: number) => (item?.getID ? item.getID() : index.toString()), []);
|
||||
|
||||
return isFlatList ? (
|
||||
<FlatList
|
||||
ref={flatListRef}
|
||||
renderItem={renderItem}
|
||||
extraData={data}
|
||||
keyExtractor={(_, index) => index.toString()}
|
||||
keyExtractor={keyExtractor}
|
||||
showsVerticalScrollIndicator={false}
|
||||
pagingEnabled={horizontal}
|
||||
disableIntervalMomentum={horizontal}
|
||||
|
@ -427,6 +422,7 @@ const WalletsCarousel = forwardRef<FlatListRefType, WalletsCarouselProps>((props
|
|||
showsHorizontalScrollIndicator={false}
|
||||
initialNumToRender={10}
|
||||
scrollEnabled={scrollEnabled}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
ListHeaderComponent={ListHeaderComponent}
|
||||
style={{ minHeight: sliderHeight + 12 }}
|
||||
onScrollToIndexFailed={onScrollToIndexFailed}
|
||||
|
|
|
@ -40,9 +40,7 @@ const AddressItem = ({ item, balanceUnit, walletID, allowSignVerifyMessage }: Ad
|
|||
borderBottomColor: colors.lightBorder,
|
||||
backgroundColor: colors.elevated,
|
||||
},
|
||||
list: {
|
||||
color: colors.buttonTextColor,
|
||||
},
|
||||
|
||||
index: {
|
||||
color: colors.alternativeTextColor,
|
||||
},
|
||||
|
@ -151,24 +149,29 @@ const AddressItem = ({ item, balanceUnit, walletID, allowSignVerifyMessage }: Ad
|
|||
title={item.address}
|
||||
actions={menuActions}
|
||||
onPressMenuItem={onToolTipPress}
|
||||
// Revisit once RNMenu has renderPreview prop
|
||||
renderPreview={renderPreview}
|
||||
onPress={navigateToReceive}
|
||||
isButton
|
||||
>
|
||||
<ListItem key={item.key} containerStyle={stylesHook.container}>
|
||||
<ListItem.Content style={stylesHook.list}>
|
||||
<ListItem.Title style={stylesHook.list} numberOfLines={1} ellipsizeMode="middle">
|
||||
<Text style={[styles.index, stylesHook.index]}>{item.index + 1}</Text>{' '}
|
||||
<Text style={[stylesHook.address, styles.address]}>{item.address}</Text>
|
||||
</ListItem.Title>
|
||||
<View style={styles.subtitle}>
|
||||
<Text style={[stylesHook.list, styles.balance, stylesHook.balance]}>{balance}</Text>
|
||||
<ListItem.Content>
|
||||
<View style={styles.row}>
|
||||
<View style={styles.leftSection}>
|
||||
<Text style={[styles.index, stylesHook.index]}>{item.index}</Text>
|
||||
</View>
|
||||
<View style={styles.middleSection}>
|
||||
<Text style={[stylesHook.address, styles.address]} numberOfLines={1} ellipsizeMode="middle">
|
||||
{item.address}
|
||||
</Text>
|
||||
<Text style={[stylesHook.balance, styles.balance]}>{balance}</Text>
|
||||
</View>
|
||||
</View>
|
||||
</ListItem.Content>
|
||||
<View>
|
||||
<View style={styles.rightContainer}>
|
||||
<AddressTypeBadge isInternal={item.isInternal} hasTransactions={hasTransactions} />
|
||||
<Text style={[stylesHook.list, styles.balance, stylesHook.balance]}>
|
||||
{loc.addresses.transactions}: {item.transactions}
|
||||
<Text style={[stylesHook.balance, styles.balance]}>
|
||||
{loc.addresses.transactions}: {item.transactions ?? 0}
|
||||
</Text>
|
||||
</View>
|
||||
</ListItem>
|
||||
|
@ -179,20 +182,27 @@ const AddressItem = ({ item, balanceUnit, walletID, allowSignVerifyMessage }: Ad
|
|||
const styles = StyleSheet.create({
|
||||
address: {
|
||||
fontWeight: 'bold',
|
||||
marginHorizontal: 40,
|
||||
marginHorizontal: 4,
|
||||
},
|
||||
index: {
|
||||
fontSize: 15,
|
||||
},
|
||||
balance: {
|
||||
marginTop: 8,
|
||||
marginLeft: 14,
|
||||
marginTop: 4,
|
||||
},
|
||||
subtitle: {
|
||||
flex: 1,
|
||||
row: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
},
|
||||
leftSection: {
|
||||
marginRight: 8,
|
||||
},
|
||||
middleSection: {
|
||||
flex: 1,
|
||||
},
|
||||
rightContainer: {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'flex-end',
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { NativeStackNavigationOptions } from '@react-navigation/native-stack';
|
||||
import React from 'react';
|
||||
import { Image, Keyboard, StyleSheet, TouchableOpacity } from 'react-native';
|
||||
import { Image, Keyboard, Platform, StyleSheet, TouchableOpacity } from 'react-native';
|
||||
|
||||
import loc from '../loc';
|
||||
import { Theme } from './themes';
|
||||
|
@ -59,7 +59,6 @@ const navigationStyle = (
|
|||
{
|
||||
closeButtonPosition,
|
||||
onCloseButtonPressed,
|
||||
headerBackVisible = true,
|
||||
...opts
|
||||
}: NativeStackNavigationOptions & {
|
||||
closeButtonPosition?: CloseButtonPosition;
|
||||
|
@ -78,11 +77,6 @@ const navigationStyle = (
|
|||
let headerRight;
|
||||
let headerLeft;
|
||||
|
||||
if (!headerBackVisible) {
|
||||
headerLeft = () => <></>;
|
||||
opts.headerLeft = headerLeft;
|
||||
}
|
||||
|
||||
if (closeButton === CloseButtonPosition.Right) {
|
||||
headerRight = () => (
|
||||
<TouchableOpacity
|
||||
|
@ -108,17 +102,24 @@ const navigationStyle = (
|
|||
</TouchableOpacity>
|
||||
);
|
||||
}
|
||||
|
||||
let options: NativeStackNavigationOptions = {
|
||||
const baseHeaderStyle = {
|
||||
headerShadowVisible: false,
|
||||
headerTitleStyle: {
|
||||
fontWeight: '600',
|
||||
fontWeight: '600' as const,
|
||||
color: theme.colors.foregroundColor,
|
||||
},
|
||||
headerBackTitleVisible: false,
|
||||
headerTintColor: theme.colors.foregroundColor,
|
||||
headerBackButtonDisplayMode: 'minimal',
|
||||
};
|
||||
const isLeftCloseButtonAndroid = closeButton === CloseButtonPosition.Left && Platform.OS === 'android';
|
||||
|
||||
const leftCloseButtonStyle = isLeftCloseButtonAndroid ? { headerBackImageSource: theme.closeImage } : { headerLeft };
|
||||
|
||||
let options: NativeStackNavigationOptions = {
|
||||
...baseHeaderStyle,
|
||||
...leftCloseButtonStyle,
|
||||
headerBackButtonDisplayMode: 'minimal',
|
||||
headerRight,
|
||||
headerLeft,
|
||||
...opts,
|
||||
};
|
||||
|
||||
|
|
|
@ -86,6 +86,7 @@ export const BlueDarkTheme: Theme = {
|
|||
customHeader: '#000000',
|
||||
brandingColor: '#000000',
|
||||
borderTopColor: '#9aa0aa',
|
||||
background: '#000000',
|
||||
foregroundColor: '#ffffff',
|
||||
buttonDisabledBackgroundColor: '#3A3A3C',
|
||||
buttonBackgroundColor: '#3A3A3C',
|
||||
|
|
|
@ -41,37 +41,37 @@ platform :android do
|
|||
Dir.chdir(project_root) do
|
||||
build_number = ENV['BUILD_NUMBER']
|
||||
UI.user_error!("BUILD_NUMBER environment variable is missing") if build_number.nil?
|
||||
|
||||
|
||||
# Extract versionName from build.gradle
|
||||
version_name = sh("grep versionName android/app/build.gradle | awk '{print $2}' | tr -d '\"'").strip
|
||||
|
||||
UI.user_error!("Failed to extract versionName from build.gradle") if version_name.nil? || version_name.empty?
|
||||
|
||||
# Update versionCode in build.gradle
|
||||
UI.message("Updating versionCode in build.gradle to #{build_number}...")
|
||||
build_gradle_path = "android/app/build.gradle"
|
||||
build_gradle_contents = File.read(build_gradle_path)
|
||||
new_build_gradle_contents = build_gradle_contents.gsub(/versionCode\s+\d+/, "versionCode #{build_number}")
|
||||
File.write(build_gradle_path, new_build_gradle_contents)
|
||||
|
||||
# Determine branch name
|
||||
branch_name = ENV['GITHUB_HEAD_REF'] || `git rev-parse --abbrev-ref HEAD`.strip.gsub(/[\/\\:?*"<>|]/, '_')
|
||||
|
||||
# Determine branch name and sanitize it
|
||||
branch_name = ENV['GITHUB_HEAD_REF'] || `git rev-parse --abbrev-ref HEAD`.strip
|
||||
branch_name = branch_name.gsub(/[^a-zA-Z0-9_-]/, '_') # Replace non-alphanumeric characters with underscore
|
||||
branch_name = 'master' if branch_name.nil? || branch_name.empty?
|
||||
|
||||
# Define APK name based on branch
|
||||
signed_apk_name = branch_name != 'master' ? "BlueWallet-#{version_name}-#{build_number}-#{branch_name}.apk" : "BlueWallet-#{version_name}-#{build_number}.apk"
|
||||
|
||||
# Build APK
|
||||
UI.message("Building APK...")
|
||||
sh("cd android && ./gradlew assembleRelease")
|
||||
UI.message("APK build completed.")
|
||||
|
||||
|
||||
# Define APK name based on branch
|
||||
signed_apk_name = branch_name != 'master' ?
|
||||
"BlueWallet-#{version_name}-#{build_number}-#{branch_name}".gsub(/[\/\\:?*"<>|]/, '_') + ".apk" :
|
||||
"BlueWallet-#{version_name}-#{build_number}.apk"
|
||||
|
||||
"BlueWallet-#{version_name}-#{build_number}-#{branch_name}.apk" :
|
||||
"BlueWallet-#{version_name}-#{build_number}.apk"
|
||||
|
||||
# Define paths
|
||||
unsigned_apk_path = "android/app/build/outputs/apk/release/app-release-unsigned.apk"
|
||||
signed_apk_path = "android/app/build/outputs/apk/release/#{signed_apk_name}"
|
||||
|
||||
# Build APK
|
||||
UI.message("Building APK...")
|
||||
sh("cd android && ./gradlew assembleRelease --no-daemon")
|
||||
UI.message("APK build completed.")
|
||||
|
||||
# Rename APK
|
||||
if File.exist?(unsigned_apk_path)
|
||||
UI.message("Renaming APK to #{signed_apk_name}...")
|
||||
|
@ -81,14 +81,16 @@ platform :android do
|
|||
UI.error("Unsigned APK not found at path: #{unsigned_apk_path}")
|
||||
next
|
||||
end
|
||||
|
||||
|
||||
# Sign APK
|
||||
UI.message("Signing APK with apksigner...")
|
||||
apksigner_path = "#{ENV['ANDROID_HOME']}/build-tools/34.0.0/apksigner"
|
||||
apksigner_path = Dir.glob("#{ENV['ANDROID_HOME']}/build-tools/*/apksigner").sort.last
|
||||
UI.user_error!("apksigner not found in Android build-tools") if apksigner_path.nil? || apksigner_path.empty?
|
||||
sh("#{apksigner_path} sign --ks #{project_root}/bluewallet-release-key.keystore --ks-pass=pass:#{ENV['KEYSTORE_PASSWORD']} #{signed_apk_path}")
|
||||
UI.message("APK signed successfully: #{signed_apk_path}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
desc "Upload APK to BrowserStack and post result as PR comment"
|
||||
lane :upload_to_browserstack_and_comment do
|
||||
|
@ -126,16 +128,18 @@ platform :android do
|
|||
|
||||
You can test it on the following devices:
|
||||
|
||||
- [Google Pixel 5 (Android 12.0)](https://app-live.browserstack.com/dashboard#os=android&os_version=12.0&device=Google+Pixel+5&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true)
|
||||
- [Google Pixel 7 (Android 13.0)](https://app-live.browserstack.com/dashboard#os=android&os_version=13.0&device=Google+Pixel+7&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true)
|
||||
- [Google Pixel 8 (Android 14.0)](https://app-live.browserstack.com/dashboard#os=android&os_version=14.0&device=Google+Pixel+8&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true)
|
||||
- [Google Pixel 3a (Android 9.0)](https://app-live.browserstack.com/dashboard#os=android&os_version=9.0&device=Google+Pixel+3a&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true)
|
||||
- [Google Pixel 9 (Android 15)](https://app-live.browserstack.com/dashboard#os=android&os_version=15.0&device=Google+Pixel+8&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true&browser=chrome)
|
||||
- [Google Pixel 8 (Android 14)](https://app-live.browserstack.com/dashboard#os=android&os_version=14.0&device=Google+Pixel+8&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true&browser=chrome)
|
||||
- [Google Pixel 7 (Android 13)](https://app-live.browserstack.com/dashboard#os=android&os_version=13.0&device=Google+Pixel+7&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true&browser=chrome)
|
||||
- [Google Pixel 5 (Android 12)](https://app-live.browserstack.com/dashboard#os=android&os_version=12.0&device=Google+Pixel+5&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true&browser=chrome)
|
||||
- [Google Pixel 3a (Android 9)](https://app-live.browserstack.com/dashboard#os=android&os_version=9.0&device=Google+Pixel+3a&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true&browser=chrome)
|
||||
|
||||
- [Samsung Galaxy Z Fold 5 (Android 13.0)](https://app-live.browserstack.com/dashboard#os=android&os_version=13.0&device=Samsung+Galaxy+Z+Fold+5&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true)
|
||||
- [Samsung Galaxy Z Fold 6 (Android 14.0)](https://app-live.browserstack.com/dashboard#os=android&os_version=14.0&device=Samsung+Galaxy+Z+Fold+6&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true)
|
||||
- [Samsung Galaxy Tab S9 (Android 13.0)](https://app-live.browserstack.com/dashboard#os=android&os_version=13.0&device=Samsung+Galaxy+Tab+S9&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true)
|
||||
- [Samsung Galaxy Note 9 (Android 8.1)](https://app-live.browserstack.com/dashboard#os=android&os_version=8.1&device=Samsung+Galaxy+Note+9&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true)
|
||||
- [Samsung Galaxy Z Fold 6 (Android 14)](https://app-live.browserstack.com/dashboard#os=android&os_version=14.0&device=Samsung+Galaxy+Z+Fold+6&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true&browser=chrome)
|
||||
- [Samsung Galaxy Z Fold 5 (Android 13)](https://app-live.browserstack.com/dashboard#os=android&os_version=13.0&device=Samsung+Galaxy+Z+Fold+5&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true&browser=chrome)
|
||||
- [Samsung Galaxy Tab S9 (Android 13)](https://app-live.browserstack.com/dashboard#os=android&os_version=13.0&device=Samsung+Galaxy+Tab+S9&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true&browser=chrome)
|
||||
- [Samsung Galaxy Note 9 (Android 8.1)](https://app-live.browserstack.com/dashboard#os=android&os_version=8.1&device=Samsung+Galaxy+Note+9&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true&browser=chrome)
|
||||
|
||||
- [OnePlus 11R (Android 13)](https://app-live.browserstack.com/dashboard#os=android&os_version=13.0&device=OnePlus+11R&app_hashed_id=#{browserstack_hashed_id}&scale_to_fit=true&speed=1&start=true&browser=chrome)
|
||||
**Filename**: [#{apk_filename}](#{apk_download_url})
|
||||
**BrowserStack App URL**: #{app_url}
|
||||
COMMENT
|
||||
|
@ -188,6 +192,40 @@ end
|
|||
# ===========================
|
||||
|
||||
platform :ios do
|
||||
# Add helper methods for error handling and retries
|
||||
def ensure_env_vars(vars)
|
||||
vars.each do |var|
|
||||
UI.user_error!("#{var} environment variable is missing") if ENV[var].nil? || ENV[var].empty?
|
||||
end
|
||||
end
|
||||
|
||||
def log_success(message)
|
||||
UI.success("âś… #{message}")
|
||||
end
|
||||
|
||||
def log_error(message)
|
||||
UI.error("❌ #{message}")
|
||||
end
|
||||
|
||||
# Method to safely call actions with retry logic
|
||||
def with_retry(max_attempts = 3, action_name = "")
|
||||
attempts = 0
|
||||
begin
|
||||
attempts += 1
|
||||
yield
|
||||
rescue => e
|
||||
if attempts < max_attempts
|
||||
wait_time = 10 * attempts
|
||||
log_error("Attempt #{attempts}/#{max_attempts} for #{action_name} failed: #{e.message}")
|
||||
UI.message("Retrying in #{wait_time} seconds...")
|
||||
sleep(wait_time)
|
||||
retry
|
||||
else
|
||||
log_error("#{action_name} failed after #{max_attempts} attempts: #{e.message}")
|
||||
raise e
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
desc "Register new devices from a file"
|
||||
lane :register_devices_from_txt do
|
||||
|
@ -234,26 +272,33 @@ platform :ios do
|
|||
|
||||
desc "Synchronize certificates and provisioning profiles"
|
||||
lane :setup_provisioning_profiles do
|
||||
required_vars = ["GIT_ACCESS_TOKEN", "GIT_URL", "ITC_TEAM_ID", "ITC_TEAM_NAME", "KEYCHAIN_PASSWORD"]
|
||||
ensure_env_vars(required_vars)
|
||||
|
||||
UI.message("Setting up provisioning profiles...")
|
||||
|
||||
platform = "ios"
|
||||
|
||||
|
||||
# Iterate over app identifiers to fetch provisioning profiles
|
||||
app_identifiers.each do |app_identifier|
|
||||
match(
|
||||
git_basic_authorization: ENV["GIT_ACCESS_TOKEN"],
|
||||
git_url: ENV["GIT_URL"],
|
||||
type: "appstore",
|
||||
clone_branch_directly: true, # Skip if the branch already exists
|
||||
platform: platform,
|
||||
app_identifier: app_identifier,
|
||||
team_id: ENV["ITC_TEAM_ID"],
|
||||
team_name: ENV["ITC_TEAM_NAME"],
|
||||
readonly: true,
|
||||
keychain_name: "temp_keychain",
|
||||
keychain_password: ENV["KEYCHAIN_PASSWORD"]
|
||||
)
|
||||
with_retry(3, "Fetching provisioning profile for #{app_identifier}") do
|
||||
UI.message("Fetching provisioning profile for #{app_identifier}...")
|
||||
match(
|
||||
git_basic_authorization: ENV["GIT_ACCESS_TOKEN"],
|
||||
git_url: ENV["GIT_URL"],
|
||||
type: "appstore",
|
||||
clone_branch_directly: true,
|
||||
platform: "ios",
|
||||
app_identifier: app_identifier,
|
||||
team_id: ENV["ITC_TEAM_ID"],
|
||||
team_name: ENV["ITC_TEAM_NAME"],
|
||||
readonly: true,
|
||||
keychain_name: "temp_keychain",
|
||||
keychain_password: ENV["KEYCHAIN_PASSWORD"]
|
||||
)
|
||||
log_success("Successfully fetched provisioning profile for #{app_identifier}")
|
||||
end
|
||||
end
|
||||
|
||||
log_success("All provisioning profiles set up")
|
||||
end
|
||||
|
||||
desc "Fetch development certificates and provisioning profiles for Mac Catalyst"
|
||||
|
@ -398,48 +443,114 @@ lane :upload_bugsnag_sourcemaps do
|
|||
end
|
||||
|
||||
desc "Build the iOS app"
|
||||
lane :build_app_lane do
|
||||
Dir.chdir(project_root) do
|
||||
UI.message("Building the application from: #{Dir.pwd}")
|
||||
lane :build_app_lane do
|
||||
Dir.chdir(project_root) do
|
||||
UI.message("Building the application from: #{Dir.pwd}")
|
||||
|
||||
workspace_path = File.join(project_root, "ios", "BlueWallet.xcworkspace")
|
||||
export_options_path = File.join(project_root, "ios", "export_options.plist")
|
||||
workspace_path = File.join(project_root, "ios", "BlueWallet.xcworkspace")
|
||||
export_options_path = File.join(project_root, "ios", "export_options.plist")
|
||||
|
||||
clear_derived_data_lane
|
||||
|
||||
begin
|
||||
build_ios_app(
|
||||
scheme: "BlueWallet",
|
||||
workspace: workspace_path,
|
||||
export_method: "app-store",
|
||||
include_bitcode: false,
|
||||
configuration: "Release",
|
||||
skip_profile_detection: false,
|
||||
include_symbols: true,
|
||||
export_team_id: ENV["ITC_TEAM_ID"],
|
||||
export_options: export_options_path,
|
||||
output_directory: File.join(project_root, "ios", "build"),
|
||||
output_name: "BlueWallet_#{ENV['PROJECT_VERSION']}_#{ENV['NEW_BUILD_NUMBER']}.ipa",
|
||||
buildlog_path: File.join(project_root, "ios", "build_logs"),
|
||||
silent: false,
|
||||
clean: true
|
||||
)
|
||||
rescue => e
|
||||
UI.user_error!("build_ios_app failed: #{e.message}")
|
||||
end
|
||||
clear_derived_data_lane
|
||||
|
||||
# Determine which iOS version to use
|
||||
ios_version = determine_ios_version
|
||||
|
||||
# Use File.join to construct paths without extra slashes
|
||||
ipa_path = lane_context[SharedValues::IPA_OUTPUT_PATH]
|
||||
UI.message("Using iOS version: #{ios_version}")
|
||||
UI.message("Using export options from: #{export_options_path}")
|
||||
|
||||
# Define the IPA output path before building
|
||||
ipa_directory = File.join(project_root, "ios", "build")
|
||||
ipa_name = "BlueWallet_#{ENV['PROJECT_VERSION']}_#{ENV['NEW_BUILD_NUMBER']}.ipa"
|
||||
ipa_path = File.join(ipa_directory, ipa_name)
|
||||
|
||||
begin
|
||||
build_ios_app(
|
||||
scheme: "BlueWallet",
|
||||
workspace: workspace_path,
|
||||
export_method: "app-store",
|
||||
export_options: export_options_path,
|
||||
output_directory: ipa_directory,
|
||||
output_name: ipa_name,
|
||||
buildlog_path: File.join(project_root, "ios", "build_logs"),
|
||||
)
|
||||
rescue => e
|
||||
UI.user_error!("build_ios_app failed: #{e.message}")
|
||||
end
|
||||
|
||||
if ipa_path && File.exist?(ipa_path)
|
||||
UI.message("IPA successfully found at: #{ipa_path}")
|
||||
# Check for IPA path from both our defined path and fastlane's context
|
||||
ipa_path = lane_context[SharedValues::IPA_OUTPUT_PATH] || ipa_path
|
||||
|
||||
# Ensure the directory exists
|
||||
FileUtils.mkdir_p(File.dirname(ipa_path)) unless Dir.exist?(File.dirname(ipa_path))
|
||||
|
||||
if ipa_path && File.exist?(ipa_path)
|
||||
UI.message("IPA successfully found at: #{ipa_path}")
|
||||
else
|
||||
# Try to find any IPA file as fallback
|
||||
Dir.chdir(project_root) do
|
||||
fallback_ipa = Dir.glob("**/*.ipa").first
|
||||
if fallback_ipa
|
||||
ipa_path = File.join(project_root, fallback_ipa)
|
||||
UI.message("Found fallback IPA at: #{ipa_path}")
|
||||
else
|
||||
UI.user_error!("No IPA file found after build")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Set both environment variable and GitHub Actions output
|
||||
ENV['IPA_OUTPUT_PATH'] = ipa_path
|
||||
sh("echo 'IPA_OUTPUT_PATH=#{ipa_path}' >> $GITHUB_ENV") # Export for GitHub Actions
|
||||
else
|
||||
UI.user_error!("IPA not found after build_ios_app.")
|
||||
# Set both standard output format and the newer GITHUB_OUTPUT format
|
||||
sh("echo 'ipa_output_path=#{ipa_path}' >> $GITHUB_OUTPUT") if ENV['GITHUB_OUTPUT']
|
||||
sh("echo ::set-output name=ipa_output_path::#{ipa_path}")
|
||||
|
||||
# Also write path to a file that can be read by subsequent steps
|
||||
ipa_path_file = "#{ipa_directory}/ipa_path.txt"
|
||||
File.write(ipa_path_file, ipa_path)
|
||||
UI.success("Saved IPA path to: #{ipa_path_file}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
desc "Delete temporary keychain"
|
||||
lane :delete_temp_keychain do
|
||||
UI.message("Deleting temporary keychain...")
|
||||
|
||||
delete_keychain(
|
||||
name: "temp_keychain"
|
||||
) if File.exist?(File.expand_path("~/Library/Keychains/temp_keychain-db"))
|
||||
|
||||
UI.message("Temporary keychain deleted successfully.")
|
||||
end
|
||||
|
||||
# Helper method to determine which iOS version to use
|
||||
private_lane :determine_ios_version do
|
||||
# Get available iOS simulator runtimes
|
||||
runtimes_output = sh("xcrun simctl list runtimes 2>&1", log: false) rescue ""
|
||||
|
||||
if runtimes_output.include?("iOS")
|
||||
# Extract available iOS versions
|
||||
ios_versions = runtimes_output.scan(/iOS ([0-9.]+)/)
|
||||
.flatten
|
||||
.map { |v| Gem::Version.new(v) }
|
||||
.sort
|
||||
.reverse
|
||||
|
||||
if ios_versions.any?
|
||||
latest_version = ios_versions.first.to_s
|
||||
UI.success("Found iOS simulator version: #{latest_version}")
|
||||
latest_version # Implicit return - last expression is returned
|
||||
else
|
||||
# Default to a reasonable iOS version if none found
|
||||
UI.important("No iOS simulator versions found. Using default version.")
|
||||
"17.6" # Implicit return
|
||||
end
|
||||
else
|
||||
# Default to a reasonable iOS version if no iOS runtimes
|
||||
UI.important("No iOS simulator runtimes found. Using default version.")
|
||||
"17.6" # Implicit return
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
# ===========================
|
||||
# Global Lanes
|
||||
|
@ -589,5 +700,4 @@ lane :update_release_notes do |options|
|
|||
UI.error("No localization found for locale #{locale}")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -3,33 +3,39 @@
|
|||
# URL of the Git repository to store the certificates
|
||||
git_url(ENV["GIT_URL"])
|
||||
|
||||
# Define the type of match to run, could be one of 'appstore', 'adhoc', 'development', or 'enterprise'.
|
||||
# For example, use 'appstore' for App Store builds, 'adhoc' for Ad Hoc distribution,
|
||||
# 'development' for development builds, and 'enterprise' for In-House (enterprise) distribution.
|
||||
type("appstore")
|
||||
# Define the type of match to run
|
||||
# Default to "appstore" but can be overridden
|
||||
type(ENV["MATCH_TYPE"] || "appstore")
|
||||
|
||||
app_identifier(["io.bluewallet.bluewallet", "io.bluewallet.bluewallet.watch", "io.bluewallet.bluewallet.watch.extension", "io.bluewallet.bluewallet.Stickers", "io.bluewallet.bluewallet.MarketWidget"]) # Replace with your app identifiers
|
||||
# App identifiers for all BlueWallet apps
|
||||
app_identifier([
|
||||
"io.bluewallet.bluewallet",
|
||||
"io.bluewallet.bluewallet.watch",
|
||||
"io.bluewallet.bluewallet.watch.extension",
|
||||
"io.bluewallet.bluewallet.Stickers",
|
||||
"io.bluewallet.bluewallet.MarketWidget"
|
||||
])
|
||||
|
||||
# List of app identifiers to create provisioning profiles for.
|
||||
# Replace with your app's bundle identifier(s).
|
||||
|
||||
# Your Apple Developer account email address.
|
||||
# Your Apple Developer account email address
|
||||
username(ENV["APPLE_ID"])
|
||||
|
||||
# The ID of your Apple Developer team if you're part of multiple teams
|
||||
# The ID of your Apple Developer team
|
||||
team_id(ENV["ITC_TEAM_ID"])
|
||||
|
||||
# Set this to true if match should only read existing certificates and profiles
|
||||
# and not create new ones.
|
||||
readonly(true)
|
||||
# Set readonly based on environment (default to true for safety)
|
||||
# Set to false explicitly when new profiles need to be created
|
||||
readonly(ENV["MATCH_READONLY"] == "false" ? false : true)
|
||||
|
||||
# Optional: The Git branch that is used for match.
|
||||
# Default is 'master'.
|
||||
|
||||
# Optional: Path to a specific SSH key to be used by match.
|
||||
# Only needed if you're using a private repository and match needs to use SSH keys for authentication.
|
||||
# ssh_key("/path/to/your/private/key")
|
||||
|
||||
# Optional: Define the platform to use, can be 'ios', 'macos', or 'tvos'.
|
||||
# For React Native projects, you'll typically use 'ios'.
|
||||
# Define the platform to use
|
||||
platform("ios")
|
||||
|
||||
# Git basic authentication through access token
|
||||
# This is useful for CI/CD environments where SSH keys aren't available
|
||||
git_basic_authorization(ENV["GIT_ACCESS_TOKEN"])
|
||||
|
||||
# Storage mode (git by default)
|
||||
storage_mode("git")
|
||||
|
||||
# Optional: The Git branch that is used for match
|
||||
# Default is 'master'
|
||||
# branch("main")
|
||||
|
|
|
@ -4,3 +4,4 @@
|
|||
|
||||
gem 'fastlane-plugin-browserstack'
|
||||
gem 'fastlane-plugin-bugsnag_sourcemaps_upload'
|
||||
gem "fastlane-plugin-bugsnag"
|
||||
|
|
1
gesture-handler.js
Normal file
|
@ -0,0 +1 @@
|
|||
// Don't import react-native-gesture-handler on web
|
2
gesture-handler.native.js
Normal file
|
@ -0,0 +1,2 @@
|
|||
// Only import react-native-gesture-handler on native platforms
|
||||
import 'react-native-gesture-handler';
|
|
@ -1,54 +1,6 @@
|
|||
import { Platform } from 'react-native';
|
||||
import { check, request, PERMISSIONS, RESULTS } from 'react-native-permissions';
|
||||
import { navigationRef } from '../NavigationService';
|
||||
|
||||
/**
|
||||
* Helper function that navigates to ScanQR screen, and returns promise that will resolve with the result of a scan,
|
||||
* and then navigates back. If QRCode scan was closed, promise resolves to null.
|
||||
*
|
||||
* @param currentScreenName {string}
|
||||
* @param showFileImportButton {boolean}
|
||||
*
|
||||
* @param onDismiss {function} - if camera is closed via X button it gets triggered
|
||||
* @param useMerge {boolean} - if true, will merge the new screen with the current screen, otherwise will replace the current screen
|
||||
* @return {Promise<string>}
|
||||
*/
|
||||
function scanQrHelper(
|
||||
currentScreenName: string,
|
||||
showFileImportButton = true,
|
||||
onDismiss?: () => void,
|
||||
useMerge = true,
|
||||
): Promise<string | null> {
|
||||
return requestCameraAuthorization().then(() => {
|
||||
return new Promise(resolve => {
|
||||
let params = {};
|
||||
|
||||
if (useMerge) {
|
||||
const onBarScanned = function (data: any) {
|
||||
setTimeout(() => resolve(data.data || data), 1);
|
||||
navigationRef.navigate({ name: currentScreenName, params: data, merge: true });
|
||||
};
|
||||
|
||||
params = {
|
||||
showFileImportButton: Boolean(showFileImportButton),
|
||||
onDismiss,
|
||||
onBarScanned,
|
||||
};
|
||||
} else {
|
||||
params = { launchedBy: currentScreenName, showFileImportButton: Boolean(showFileImportButton) };
|
||||
}
|
||||
|
||||
navigationRef.navigate({
|
||||
name: 'ScanQRCodeRoot',
|
||||
params: {
|
||||
screen: 'ScanQRCode',
|
||||
params,
|
||||
},
|
||||
merge: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
import { navigationRef } from '../NavigationService.ts';
|
||||
|
||||
const isCameraAuthorizationStatusGranted = async () => {
|
||||
const status = await check(Platform.OS === 'android' ? PERMISSIONS.ANDROID.CAMERA : PERMISSIONS.IOS.CAMERA);
|
||||
|
@ -59,4 +11,17 @@ const requestCameraAuthorization = () => {
|
|||
return request(Platform.OS === 'android' ? PERMISSIONS.ANDROID.CAMERA : PERMISSIONS.IOS.CAMERA);
|
||||
};
|
||||
|
||||
export { scanQrHelper, isCameraAuthorizationStatusGranted, requestCameraAuthorization };
|
||||
const scanQrHelper = async (): Promise<string> => {
|
||||
await requestCameraAuthorization();
|
||||
return new Promise(resolve => {
|
||||
if (navigationRef.isReady()) {
|
||||
navigationRef.current?.navigate('ScanQRCode', {
|
||||
onBarScanned: (data: string) => {
|
||||
resolve(data);
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export { isCameraAuthorizationStatusGranted, requestCameraAuthorization, scanQrHelper };
|
||||
|
|
23
helpers/screenProtect.ts
Normal file
|
@ -0,0 +1,23 @@
|
|||
// import { enableSecureView, disableSecureView, forbidAndroidShare, allowAndroidShare } from 'react-native-prevent-screenshot-ios-android';
|
||||
// import { Platform } from 'react-native';
|
||||
// import { isDesktop } from '../blue_modules/environment';
|
||||
|
||||
export const enableScreenProtect = () => {
|
||||
// if (isDesktop) return;
|
||||
// if (Platform.OS === 'ios') {
|
||||
// enableSecureView();
|
||||
// } else if (Platform.OS === 'android') {
|
||||
// forbidAndroidShare();
|
||||
// }
|
||||
};
|
||||
|
||||
export const disableScreenProtect = () => {
|
||||
// if (isDesktop) return;
|
||||
// if (Platform.OS === 'ios') {
|
||||
// disableSecureView();
|
||||
// } else if (Platform.OS === 'android') {
|
||||
// allowAndroidShare();
|
||||
// }
|
||||
};
|
||||
|
||||
// CURRENTLY UNUSED AS WE WAIT FOR NAV 7 SUPPORT
|
|
@ -1,5 +1,3 @@
|
|||
import 'react-native-gesture-handler'; // should be on top
|
||||
|
||||
import { CommonActions } from '@react-navigation/native';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { AppState, AppStateStatus, Linking } from 'react-native';
|
||||
|
@ -21,25 +19,42 @@ import loc from '../loc';
|
|||
import { Chain } from '../models/bitcoinUnits';
|
||||
import { navigationRef } from '../NavigationService';
|
||||
import ActionSheet from '../screen/ActionSheet';
|
||||
import { useStorage } from '../hooks/context/useStorage';
|
||||
import { useStorage } from './context/useStorage';
|
||||
import RNQRGenerator from 'rn-qr-generator';
|
||||
import presentAlert from './Alert';
|
||||
import useMenuElements from '../hooks/useMenuElements';
|
||||
import useWidgetCommunication from '../hooks/useWidgetCommunication';
|
||||
import useWatchConnectivity from '../hooks/useWatchConnectivity';
|
||||
import useDeviceQuickActions from '../hooks/useDeviceQuickActions';
|
||||
import useHandoffListener from '../hooks/useHandoffListener';
|
||||
import presentAlert from '../components/Alert';
|
||||
import useWidgetCommunication from './useWidgetCommunication';
|
||||
import useWatchConnectivity from './useWatchConnectivity';
|
||||
import useDeviceQuickActions from './useDeviceQuickActions';
|
||||
import useHandoffListener from './useHandoffListener';
|
||||
import useMenuElements from './useMenuElements';
|
||||
|
||||
const ClipboardContentType = Object.freeze({
|
||||
BITCOIN: 'BITCOIN',
|
||||
LIGHTNING: 'LIGHTNING',
|
||||
});
|
||||
|
||||
const CompanionDelegates = () => {
|
||||
const { wallets, addWallet, saveToDisk, fetchAndSaveWalletTransactions, refreshAllWalletTransactions, setSharedCosigner } = useStorage();
|
||||
/**
|
||||
* Hook that initializes all companion listeners and functionality without rendering a component
|
||||
*/
|
||||
const useCompanionListeners = (skipIfNotInitialized = true) => {
|
||||
const {
|
||||
wallets,
|
||||
addWallet,
|
||||
saveToDisk,
|
||||
fetchAndSaveWalletTransactions,
|
||||
refreshAllWalletTransactions,
|
||||
setSharedCosigner,
|
||||
walletsInitialized,
|
||||
} = useStorage();
|
||||
const appState = useRef<AppStateStatus>(AppState.currentState);
|
||||
const clipboardContent = useRef<undefined | string>();
|
||||
|
||||
// We need to call hooks unconditionally before any conditional logic
|
||||
// We'll use this check inside the effects to conditionally run logic
|
||||
const shouldActivateListeners = !skipIfNotInitialized || walletsInitialized;
|
||||
|
||||
// Initialize other hooks regardless of activation status
|
||||
// They'll handle their own conditional logic internally
|
||||
useWatchConnectivity();
|
||||
useWidgetCommunication();
|
||||
useMenuElements();
|
||||
|
@ -47,6 +62,8 @@ const CompanionDelegates = () => {
|
|||
useHandoffListener();
|
||||
|
||||
const processPushNotifications = useCallback(async () => {
|
||||
if (!shouldActivateListeners) return false;
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
try {
|
||||
const notifications2process = await getStoredNotifications();
|
||||
|
@ -166,45 +183,58 @@ const CompanionDelegates = () => {
|
|||
console.error('Failed to process push notifications:', error);
|
||||
}
|
||||
return false;
|
||||
}, [fetchAndSaveWalletTransactions, refreshAllWalletTransactions, wallets]);
|
||||
}, [fetchAndSaveWalletTransactions, refreshAllWalletTransactions, wallets, shouldActivateListeners]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldActivateListeners) return;
|
||||
|
||||
initializeNotifications(processPushNotifications);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
}, [shouldActivateListeners]);
|
||||
|
||||
const handleOpenURL = useCallback(
|
||||
async (event: { url: string }): Promise<void> => {
|
||||
const { url } = event;
|
||||
if (!shouldActivateListeners) return;
|
||||
|
||||
if (url) {
|
||||
const decodedUrl = decodeURIComponent(url);
|
||||
const fileName = decodedUrl.split('/').pop()?.toLowerCase();
|
||||
|
||||
if (fileName && /\.(jpe?g|png)$/i.test(fileName)) {
|
||||
try {
|
||||
if (!event.url) return;
|
||||
let decodedUrl: string;
|
||||
try {
|
||||
decodedUrl = decodeURIComponent(event.url);
|
||||
} catch (e) {
|
||||
console.error('Failed to decode URL, using original', e);
|
||||
decodedUrl = event.url;
|
||||
}
|
||||
const fileName = decodedUrl.split('/').pop()?.toLowerCase() || '';
|
||||
if (/\.(jpe?g|png)$/i.test(fileName)) {
|
||||
let qrResult;
|
||||
try {
|
||||
const values = await RNQRGenerator.detect({
|
||||
uri: decodedUrl,
|
||||
});
|
||||
|
||||
if (values && values.values.length > 0) {
|
||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
|
||||
DeeplinkSchemaMatch.navigationRouteFor(
|
||||
{ url: values.values[0] },
|
||||
(value: [string, any]) => navigationRef.navigate(...value),
|
||||
{
|
||||
wallets,
|
||||
addWallet,
|
||||
saveToDisk,
|
||||
setSharedCosigner,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
|
||||
presentAlert({ message: loc.send.qr_error_no_qrcode });
|
||||
qrResult = await RNQRGenerator.detect({ uri: decodedUrl });
|
||||
} catch (e) {
|
||||
console.error('QR detection first attempt failed:', e);
|
||||
}
|
||||
if (!qrResult || !qrResult.values || qrResult.values.length === 0) {
|
||||
const altUrl = decodedUrl.replace(/^file:\/\//, '');
|
||||
try {
|
||||
qrResult = await RNQRGenerator.detect({ uri: altUrl });
|
||||
} catch (e) {
|
||||
console.error('QR detection second attempt failed:', e);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error detecting QR code:', error);
|
||||
}
|
||||
if (qrResult?.values?.length) {
|
||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationSuccess);
|
||||
DeeplinkSchemaMatch.navigationRouteFor(
|
||||
{ url: qrResult.values[0] },
|
||||
(value: [string, any]) => navigationRef.navigate(...value),
|
||||
{
|
||||
wallets,
|
||||
addWallet,
|
||||
saveToDisk,
|
||||
setSharedCosigner,
|
||||
},
|
||||
);
|
||||
} else {
|
||||
throw new Error(loc.send.qr_error_no_qrcode);
|
||||
}
|
||||
} else {
|
||||
DeeplinkSchemaMatch.navigationRouteFor(event, (value: [string, any]) => navigationRef.navigate(...value), {
|
||||
|
@ -214,12 +244,19 @@ const CompanionDelegates = () => {
|
|||
setSharedCosigner,
|
||||
});
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('Error in handleOpenURL:', err);
|
||||
triggerHapticFeedback(HapticFeedbackTypes.NotificationError);
|
||||
presentAlert({ message: err.message || loc.send.qr_error_no_qrcode });
|
||||
}
|
||||
},
|
||||
[wallets, addWallet, saveToDisk, setSharedCosigner],
|
||||
[wallets, addWallet, saveToDisk, setSharedCosigner, shouldActivateListeners],
|
||||
);
|
||||
|
||||
const showClipboardAlert = useCallback(
|
||||
({ contentType }: { contentType: undefined | string }) => {
|
||||
if (!shouldActivateListeners) return;
|
||||
|
||||
triggerHapticFeedback(HapticFeedbackTypes.ImpactLight);
|
||||
getClipboardContent().then(clipboard => {
|
||||
if (!clipboard) return;
|
||||
|
@ -242,12 +279,13 @@ const CompanionDelegates = () => {
|
|||
);
|
||||
});
|
||||
},
|
||||
[handleOpenURL],
|
||||
[handleOpenURL, shouldActivateListeners],
|
||||
);
|
||||
|
||||
const handleAppStateChange = useCallback(
|
||||
async (nextAppState: AppStateStatus | undefined) => {
|
||||
if (wallets.length === 0) return;
|
||||
if (!shouldActivateListeners || wallets.length === 0) return;
|
||||
|
||||
if ((appState.current.match(/background/) && nextAppState === 'active') || nextAppState === undefined) {
|
||||
setTimeout(() => A(A.ENUM.APP_UNSUSPENDED), 2000);
|
||||
updateExchangeRate();
|
||||
|
@ -287,10 +325,12 @@ const CompanionDelegates = () => {
|
|||
appState.current = nextAppState;
|
||||
}
|
||||
},
|
||||
[processPushNotifications, showClipboardAlert, wallets],
|
||||
[processPushNotifications, showClipboardAlert, wallets, shouldActivateListeners],
|
||||
);
|
||||
|
||||
const addListeners = useCallback(() => {
|
||||
if (!shouldActivateListeners) return { urlSubscription: null, appStateSubscription: null };
|
||||
|
||||
const urlSubscription = Linking.addEventListener('url', handleOpenURL);
|
||||
const appStateSubscription = AppState.addEventListener('change', handleAppStateChange);
|
||||
|
||||
|
@ -298,18 +338,16 @@ const CompanionDelegates = () => {
|
|||
urlSubscription,
|
||||
appStateSubscription,
|
||||
};
|
||||
}, [handleOpenURL, handleAppStateChange]);
|
||||
}, [handleOpenURL, handleAppStateChange, shouldActivateListeners]);
|
||||
|
||||
useEffect(() => {
|
||||
const subscriptions = addListeners();
|
||||
|
||||
return () => {
|
||||
subscriptions.urlSubscription?.remove();
|
||||
subscriptions.appStateSubscription?.remove();
|
||||
subscriptions.urlSubscription?.remove?.();
|
||||
subscriptions.appStateSubscription?.remove?.();
|
||||
};
|
||||
}, [addListeners]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export default CompanionDelegates;
|
||||
export default useCompanionListeners;
|
|
@ -1,23 +1,27 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import debounce from '../blue_modules/debounce';
|
||||
|
||||
const useDebounce = <T>(value: T, delay: number): T => {
|
||||
// Overload signatures
|
||||
function useDebounce<T extends (...args: any[]) => any>(callback: T, delay: number): T;
|
||||
function useDebounce<T>(value: T, delay: number): T;
|
||||
|
||||
function useDebounce<T>(value: T, delay: number): T {
|
||||
const isFn = typeof value === 'function';
|
||||
|
||||
const debouncedFunction = useMemo(() => {
|
||||
return isFn ? debounce(value as unknown as (...args: any[]) => any, delay) : null;
|
||||
}, [isFn, value, delay]);
|
||||
|
||||
const [debouncedValue, setDebouncedValue] = useState<T>(value);
|
||||
|
||||
useEffect(() => {
|
||||
const handler = debounce((val: T) => {
|
||||
setDebouncedValue(val);
|
||||
}, delay);
|
||||
if (!isFn) {
|
||||
const handler = setTimeout(() => setDebouncedValue(value), delay);
|
||||
return () => clearTimeout(handler);
|
||||
}
|
||||
}, [isFn, value, delay]);
|
||||
|
||||
handler(value);
|
||||
|
||||
|
||||
return () => {
|
||||
handler.cancel();
|
||||
};
|
||||
}, [value, delay]);
|
||||
|
||||
return debouncedValue;
|
||||
};
|
||||
return isFn ? (debouncedFunction as unknown as T) : debouncedValue;
|
||||
}
|
||||
|
||||
export default useDebounce;
|
||||
|
|
|
@ -3,9 +3,16 @@ import { navigationRef } from '../NavigationService';
|
|||
import { presentWalletExportReminder } from '../helpers/presentWalletExportReminder';
|
||||
import { unlockWithBiometrics, useBiometrics } from './useBiometrics';
|
||||
import { useStorage } from './context/useStorage';
|
||||
import { requestCameraAuthorization } from '../helpers/scan-qr';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
// List of screens that require biometrics
|
||||
const requiresBiometrics = ['WalletExportRoot', 'WalletXpubRoot', 'ViewEditMultisigCosignersRoot', 'ExportMultisigCoordinationSetupRoot'];
|
||||
const requiresBiometrics = [
|
||||
'WalletExportRoot',
|
||||
'WalletXpubRoot',
|
||||
'ViewEditMultisigCosigners',
|
||||
'ExportMultisigCoordinationSetupRoot',
|
||||
];
|
||||
|
||||
// List of screens that require wallet export to be saved
|
||||
const requiresWalletExportIsSaved = ['ReceiveDetailsRoot', 'WalletAddresses'];
|
||||
|
@ -15,96 +22,125 @@ export const useExtendedNavigation = <T extends NavigationProp<ParamListBase>>()
|
|||
const { wallets, saveToDisk } = useStorage();
|
||||
const { isBiometricUseEnabled } = useBiometrics();
|
||||
|
||||
const enhancedNavigate: NavigationProp<ParamListBase>['navigate'] = (
|
||||
screenOrOptions: any,
|
||||
params?: any,
|
||||
options?: { merge?: boolean },
|
||||
) => {
|
||||
let screenName: string;
|
||||
if (typeof screenOrOptions === 'string') {
|
||||
screenName = screenOrOptions;
|
||||
} else if (typeof screenOrOptions === 'object' && 'name' in screenOrOptions) {
|
||||
screenName = screenOrOptions.name;
|
||||
params = screenOrOptions.params; // Assign params from object if present
|
||||
} else {
|
||||
throw new Error('Invalid navigation options');
|
||||
}
|
||||
const enhancedNavigate = useCallback(
|
||||
(
|
||||
...args:
|
||||
| [string]
|
||||
| [string, object | undefined]
|
||||
| [string, object | undefined, { merge?: boolean }]
|
||||
| [{ name: string; params?: object; path?: string; merge?: boolean }]
|
||||
) => {
|
||||
let screenOrOptions: any;
|
||||
let params: any;
|
||||
let options: { merge?: boolean } | undefined;
|
||||
|
||||
const isRequiresBiometrics = requiresBiometrics.includes(screenName);
|
||||
const isRequiresWalletExportIsSaved = requiresWalletExportIsSaved.includes(screenName);
|
||||
|
||||
const proceedWithNavigation = () => {
|
||||
console.log('Proceeding with navigation to', screenName);
|
||||
if (navigationRef.current?.isReady()) {
|
||||
if (typeof screenOrOptions === 'string') {
|
||||
originalNavigation.navigate({ name: screenOrOptions, params, merge: options?.merge });
|
||||
} else {
|
||||
originalNavigation.navigate({ ...screenOrOptions, params, merge: options?.merge });
|
||||
}
|
||||
if (typeof args[0] === 'string') {
|
||||
screenOrOptions = args[0];
|
||||
params = args[1];
|
||||
options = args[2];
|
||||
} else {
|
||||
screenOrOptions = args[0];
|
||||
}
|
||||
let screenName: string;
|
||||
if (typeof screenOrOptions === 'string') {
|
||||
screenName = screenOrOptions;
|
||||
} else if (typeof screenOrOptions === 'object' && 'name' in screenOrOptions) {
|
||||
screenName = screenOrOptions.name;
|
||||
params = screenOrOptions.params; // Assign params from object if present
|
||||
} else {
|
||||
throw new Error('Invalid navigation options');
|
||||
}
|
||||
};
|
||||
|
||||
(async () => {
|
||||
if (isRequiresBiometrics) {
|
||||
const isBiometricsEnabled = await isBiometricUseEnabled();
|
||||
if (isBiometricsEnabled) {
|
||||
const isAuthenticated = await unlockWithBiometrics();
|
||||
if (isAuthenticated) {
|
||||
proceedWithNavigation();
|
||||
return;
|
||||
const isRequiresBiometrics = requiresBiometrics.includes(screenName);
|
||||
const isRequiresWalletExportIsSaved = requiresWalletExportIsSaved.includes(screenName);
|
||||
|
||||
const proceedWithNavigation = () => {
|
||||
console.log('Proceeding with navigation to', screenName);
|
||||
if (navigationRef.current?.isReady()) {
|
||||
if (typeof screenOrOptions === 'string') {
|
||||
originalNavigation.navigate({ name: screenOrOptions, params, merge: options?.merge });
|
||||
} else {
|
||||
console.error('Biometric authentication failed');
|
||||
// Decide if navigation should proceed or not after failed authentication
|
||||
return; // Prevent proceeding with the original navigation if bio fails
|
||||
originalNavigation.navigate({ ...screenOrOptions, params, merge: options?.merge });
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isRequiresWalletExportIsSaved) {
|
||||
console.log('Checking if wallet export is saved');
|
||||
let walletID: string | undefined;
|
||||
if (params && params.walletID) {
|
||||
walletID = params.walletID;
|
||||
} else if (params && params.params && params.params.walletID) {
|
||||
walletID = params.params.walletID;
|
||||
}
|
||||
if (!walletID) {
|
||||
};
|
||||
|
||||
(async () => {
|
||||
// NEW: If the current (active) screen is 'ScanQRCode', bypass all checks.
|
||||
const currentRouteName = navigationRef.current?.getCurrentRoute()?.name;
|
||||
if (currentRouteName === 'ScanQRCode') {
|
||||
proceedWithNavigation();
|
||||
return;
|
||||
}
|
||||
const wallet = wallets.find(w => w.getID() === walletID);
|
||||
if (wallet && !wallet.getUserHasSavedExport()) {
|
||||
try {
|
||||
await presentWalletExportReminder();
|
||||
wallet.setUserHasSavedExport(true);
|
||||
await saveToDisk(); // Assuming saveToDisk() returns a Promise.
|
||||
|
||||
if (isRequiresBiometrics) {
|
||||
const isBiometricsEnabled = await isBiometricUseEnabled();
|
||||
if (isBiometricsEnabled) {
|
||||
const isAuthenticated = await unlockWithBiometrics();
|
||||
if (isAuthenticated) {
|
||||
proceedWithNavigation();
|
||||
return;
|
||||
} else {
|
||||
console.error('Biometric authentication failed');
|
||||
// Do not proceed if authentication fails.
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isRequiresWalletExportIsSaved) {
|
||||
console.log('Checking if wallet export is saved');
|
||||
let walletID: string | undefined;
|
||||
if (params && params.walletID) {
|
||||
walletID = params.walletID;
|
||||
} else if (params && params.params && params.params.walletID) {
|
||||
walletID = params.params.walletID;
|
||||
}
|
||||
if (!walletID) {
|
||||
proceedWithNavigation();
|
||||
} catch (error) {
|
||||
if (error) {
|
||||
return;
|
||||
}
|
||||
const wallet = wallets.find(w => w.getID() === walletID);
|
||||
if (wallet && !wallet.getUserHasSavedExport()) {
|
||||
try {
|
||||
await presentWalletExportReminder();
|
||||
wallet.setUserHasSavedExport(true);
|
||||
await saveToDisk();
|
||||
proceedWithNavigation();
|
||||
} catch (error) {
|
||||
// If there was an error (or the user cancelled), navigate to the wallet export screen.
|
||||
originalNavigation.navigate('WalletExportRoot', {
|
||||
screen: 'WalletExport',
|
||||
params: { walletID },
|
||||
});
|
||||
}
|
||||
return; // Do not proceed with the original navigation if reminder was shown.
|
||||
}
|
||||
|
||||
return; // Prevent proceeding with the original navigation if the reminder is shown
|
||||
}
|
||||
}
|
||||
proceedWithNavigation();
|
||||
})();
|
||||
};
|
||||
|
||||
const navigateToWalletsList = () => {
|
||||
// If the target screen is ScanQRCode, request camera authorization.
|
||||
if (screenName === 'ScanQRCode') {
|
||||
await requestCameraAuthorization();
|
||||
}
|
||||
proceedWithNavigation();
|
||||
})();
|
||||
},
|
||||
[originalNavigation, isBiometricUseEnabled, wallets, saveToDisk],
|
||||
);
|
||||
|
||||
const navigateToWalletsList = useCallback(() => {
|
||||
enhancedNavigate('WalletsList');
|
||||
}
|
||||
}, [enhancedNavigate]);
|
||||
|
||||
return {
|
||||
...originalNavigation,
|
||||
navigate: enhancedNavigate,
|
||||
navigateToWalletsList,
|
||||
};
|
||||
return useMemo(
|
||||
() => ({
|
||||
...originalNavigation,
|
||||
navigate: enhancedNavigate,
|
||||
navigateToWalletsList,
|
||||
}),
|
||||
[originalNavigation, enhancedNavigate, navigateToWalletsList],
|
||||
);
|
||||
};
|
||||
|
||||
// Usage example:
|
||||
// type NavigationProps = NativeStackNavigationProp<SendDetailsStackParamList, 'SendDetails'>;
|
||||
// const navigation = useExtendedNavigation<NavigationProps>();
|
||||
// const navigation = useExtendedNavigation<NavigationProps>();
|
|
@ -23,20 +23,25 @@ const useHandoffListener = () => {
|
|||
|
||||
const handleUserActivity = useCallback(
|
||||
(data: UserActivityData) => {
|
||||
if (!data || !data.activityType) {
|
||||
console.debug(`Invalid handoff data received: ${data ? JSON.stringify(data) : 'No data provided'}`);
|
||||
return;
|
||||
}
|
||||
const { activityType, userInfo } = data;
|
||||
const modifiedUserInfo = { ...(userInfo || {}), type: activityType };
|
||||
try {
|
||||
if (activityType === HandOffActivityType.ReceiveOnchain) {
|
||||
if (activityType === HandOffActivityType.ReceiveOnchain && modifiedUserInfo.address) {
|
||||
navigate('ReceiveDetailsRoot', {
|
||||
screen: 'ReceiveDetails',
|
||||
params: { address: userInfo.address },
|
||||
params: { address: modifiedUserInfo.address, type: activityType },
|
||||
});
|
||||
} else if (activityType === HandOffActivityType.Xpub) {
|
||||
} else if (activityType === HandOffActivityType.Xpub && modifiedUserInfo.xpub) {
|
||||
navigate('WalletXpubRoot', {
|
||||
screen: 'WalletXpub',
|
||||
params: { xpub: userInfo.xpub },
|
||||
params: { xpub: modifiedUserInfo.xpub, type: activityType },
|
||||
});
|
||||
} else {
|
||||
console.debug(`Unhandled activity type: ${activityType}`);
|
||||
console.debug(`Unhandled or incomplete activity type/data: ${activityType}`, modifiedUserInfo);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error handling user activity:', error);
|
||||
|
@ -50,9 +55,13 @@ const useHandoffListener = () => {
|
|||
|
||||
const activitySubscription = eventEmitter?.addListener('onUserActivityOpen', handleUserActivity);
|
||||
|
||||
EventEmitter.getMostRecentUserActivity?.()
|
||||
.then(handleUserActivity)
|
||||
.catch(() => console.debug('No userActivity object sent'));
|
||||
if (EventEmitter && EventEmitter.getMostRecentUserActivity) {
|
||||
EventEmitter.getMostRecentUserActivity()
|
||||
.then(handleUserActivity)
|
||||
.catch(() => console.debug('No valid user activity object received'));
|
||||
} else {
|
||||
console.debug('EventEmitter native module is not available.');
|
||||
}
|
||||
|
||||
return () => {
|
||||
activitySubscription?.remove();
|
||||
|
|
|
@ -1,68 +1,168 @@
|
|||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { useEffect, useCallback } from 'react';
|
||||
import { NativeEventEmitter, NativeModules, Platform } from 'react-native';
|
||||
import { navigationRef } from '../NavigationService';
|
||||
import { CommonActions } from '@react-navigation/native';
|
||||
import * as NavigationService from '../NavigationService';
|
||||
import { useStorage } from './context/useStorage';
|
||||
|
||||
/*
|
||||
Hook for managing iPadOS and macOS menu actions with keyboard shortcuts.
|
||||
Uses MenuElementsEmitter for event handling.
|
||||
Uses MenuElementsEmitter for event handling and navigation state.
|
||||
*/
|
||||
|
||||
type MenuActionHandler = () => void;
|
||||
|
||||
// Singleton setup - initialize once at module level
|
||||
const { MenuElementsEmitter } = NativeModules;
|
||||
const eventEmitter =
|
||||
(Platform.OS === 'ios' || Platform.OS === 'macos') && MenuElementsEmitter ? new NativeEventEmitter(MenuElementsEmitter) : null;
|
||||
let eventEmitter: NativeEventEmitter | null = null;
|
||||
let listenersInitialized = false;
|
||||
|
||||
const useMenuElements = () => {
|
||||
const { walletsInitialized } = useStorage();
|
||||
const reloadTransactionsMenuActionRef = useRef<() => void>(() => {});
|
||||
// Registry for transaction handlers by screen ID
|
||||
const handlerRegistry = new Map<string, MenuActionHandler>();
|
||||
|
||||
const setReloadTransactionsMenuActionFunction = useCallback((newFunction: () => void) => {
|
||||
console.debug('Setting reloadTransactionsMenuActionFunction.');
|
||||
reloadTransactionsMenuActionRef.current = newFunction;
|
||||
}, []);
|
||||
// Store subscription references for proper cleanup
|
||||
let subscriptions: { remove: () => void }[] = [];
|
||||
|
||||
const dispatchNavigate = useCallback((routeName: string, screen?: string) => {
|
||||
NavigationService.dispatch(CommonActions.navigate({ name: routeName, params: screen ? { screen } : undefined }));
|
||||
}, []);
|
||||
// Create a more robust emitter with error handling
|
||||
try {
|
||||
if (Platform.OS === 'ios' && MenuElementsEmitter) {
|
||||
eventEmitter = new NativeEventEmitter(MenuElementsEmitter);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[MenuElements] Failed to initialize event emitter: ', error);
|
||||
eventEmitter = null;
|
||||
}
|
||||
|
||||
const eventActions = useMemo(
|
||||
() => ({
|
||||
openSettings: () => dispatchNavigate('Settings'),
|
||||
addWallet: () => dispatchNavigate('AddWalletRoot'),
|
||||
importWallet: () => dispatchNavigate('AddWalletRoot', 'ImportWallet'),
|
||||
reloadTransactions: () => {
|
||||
console.debug('Calling reloadTransactionsMenuActionFunction');
|
||||
reloadTransactionsMenuActionRef.current?.();
|
||||
},
|
||||
}),
|
||||
[dispatchNavigate],
|
||||
);
|
||||
/**
|
||||
* Safely navigate using multiple fallback approaches
|
||||
*/
|
||||
function safeNavigate(routeName: string, params?: Record<string, any>): void {
|
||||
try {
|
||||
if (navigationRef.current?.isReady()) {
|
||||
navigationRef.current.navigate(routeName as never, params as never);
|
||||
return;
|
||||
}
|
||||
|
||||
if (navigationRef.isReady()) {
|
||||
navigationRef.dispatch(
|
||||
CommonActions.navigate({
|
||||
name: routeName,
|
||||
params,
|
||||
}),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[MenuElements] Navigation error:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup event listeners to prevent memory leaks
|
||||
function cleanupListeners(): void {
|
||||
if (subscriptions.length > 0) {
|
||||
subscriptions.forEach(subscription => {
|
||||
try {
|
||||
subscription.remove();
|
||||
} catch (e) {
|
||||
console.warn('[MenuElements] Error removing subscription:', e);
|
||||
}
|
||||
});
|
||||
subscriptions = [];
|
||||
listenersInitialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
function initializeListeners(): void {
|
||||
if (!eventEmitter || listenersInitialized) return;
|
||||
|
||||
cleanupListeners();
|
||||
|
||||
// Navigation actions
|
||||
const globalActions = {
|
||||
navigateToSettings: (): void => {
|
||||
safeNavigate('Settings');
|
||||
},
|
||||
|
||||
navigateToAddWallet: (): void => {
|
||||
safeNavigate('AddWalletRoot');
|
||||
},
|
||||
|
||||
navigateToImportWallet: (): void => {
|
||||
safeNavigate('AddWalletRoot', { screen: 'ImportWallet' });
|
||||
},
|
||||
|
||||
executeReloadTransactions: (): void => {
|
||||
const currentRoute = navigationRef.current?.getCurrentRoute();
|
||||
if (!currentRoute) return;
|
||||
|
||||
const screenName = currentRoute.name;
|
||||
const params = (currentRoute.params as { walletID?: string }) || {};
|
||||
const walletID = params.walletID;
|
||||
|
||||
const specificKey = walletID ? `${screenName}-${walletID}` : null;
|
||||
|
||||
const specificHandler = specificKey ? handlerRegistry.get(specificKey) : undefined;
|
||||
const genericHandler = handlerRegistry.get(screenName);
|
||||
const handler = specificHandler || genericHandler;
|
||||
|
||||
if (typeof handler === 'function') {
|
||||
handler();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
subscriptions.push(eventEmitter.addListener('openSettings', globalActions.navigateToSettings));
|
||||
subscriptions.push(eventEmitter.addListener('addWalletMenuAction', globalActions.navigateToAddWallet));
|
||||
subscriptions.push(eventEmitter.addListener('importWalletMenuAction', globalActions.navigateToImportWallet));
|
||||
subscriptions.push(eventEmitter.addListener('reloadTransactionsMenuAction', globalActions.executeReloadTransactions));
|
||||
} catch (error) {
|
||||
console.error('[MenuElements] Error setting up event listeners:', error);
|
||||
}
|
||||
|
||||
listenersInitialized = true;
|
||||
}
|
||||
|
||||
interface MenuElementsHook {
|
||||
registerTransactionsHandler: (handler: MenuActionHandler, screenKey?: string) => boolean;
|
||||
unregisterTransactionsHandler: (screenKey: string) => void;
|
||||
isMenuElementsSupported: boolean;
|
||||
}
|
||||
|
||||
const mountedComponents = new Set<string>();
|
||||
|
||||
const useMenuElements = (): MenuElementsHook => {
|
||||
useEffect(() => {
|
||||
if (!walletsInitialized || !eventEmitter) return;
|
||||
initializeListeners();
|
||||
|
||||
console.debug('Setting up menu event listeners');
|
||||
|
||||
// Add permanent listeners only once
|
||||
eventEmitter.removeAllListeners('openSettings');
|
||||
eventEmitter.removeAllListeners('addWalletMenuAction');
|
||||
eventEmitter.removeAllListeners('importWalletMenuAction');
|
||||
|
||||
eventEmitter.addListener('openSettings', eventActions.openSettings);
|
||||
eventEmitter.addListener('addWalletMenuAction', eventActions.addWallet);
|
||||
eventEmitter.addListener('importWalletMenuAction', eventActions.importWallet);
|
||||
|
||||
const reloadTransactionsListener = eventEmitter.addListener('reloadTransactionsMenuAction', eventActions.reloadTransactions);
|
||||
const unsubscribe = navigationRef.addListener('state', () => {});
|
||||
|
||||
return () => {
|
||||
console.debug('Removing reloadTransactionsMenuAction listener');
|
||||
reloadTransactionsListener.remove();
|
||||
unsubscribe();
|
||||
};
|
||||
}, [walletsInitialized, eventActions]);
|
||||
}, []);
|
||||
|
||||
const registerTransactionsHandler = useCallback((handler: MenuActionHandler, screenKey?: string): boolean => {
|
||||
if (typeof handler !== 'function') return false;
|
||||
|
||||
const key = screenKey || navigationRef.current?.getCurrentRoute()?.name;
|
||||
if (!key) return false;
|
||||
|
||||
mountedComponents.add(key);
|
||||
|
||||
handlerRegistry.set(key, handler);
|
||||
|
||||
return true;
|
||||
}, []);
|
||||
|
||||
const unregisterTransactionsHandler = useCallback((screenKey: string): void => {
|
||||
if (!screenKey) return;
|
||||
|
||||
handlerRegistry.delete(screenKey);
|
||||
mountedComponents.delete(screenKey);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
setReloadTransactionsMenuActionFunction,
|
||||
registerTransactionsHandler,
|
||||
unregisterTransactionsHandler,
|
||||
isMenuElementsSupported: !!eventEmitter,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -1,8 +1,28 @@
|
|||
const useMenuElements = () => {
|
||||
const setReloadTransactionsMenuActionFunction = (_: () => void) => {};
|
||||
import { useCallback } from 'react';
|
||||
|
||||
type MenuActionHandler = () => void;
|
||||
|
||||
interface MenuElementsHook {
|
||||
registerTransactionsHandler: (handler: MenuActionHandler, screenKey?: string) => boolean;
|
||||
unregisterTransactionsHandler: (screenKey: string) => void;
|
||||
isMenuElementsSupported: boolean;
|
||||
}
|
||||
|
||||
// Default implementation for platforms other than iOS
|
||||
const useMenuElements = (): MenuElementsHook => {
|
||||
const registerTransactionsHandler = useCallback((_handler: MenuActionHandler, _screenKey?: string): boolean => {
|
||||
// Non-functional stub for non-iOS platforms
|
||||
return false;
|
||||
}, []);
|
||||
|
||||
const unregisterTransactionsHandler = useCallback((_screenKey: string): void => {
|
||||
// No-op for non-supported platforms
|
||||
}, []);
|
||||
|
||||
return {
|
||||
setReloadTransactionsMenuActionFunction,
|
||||
registerTransactionsHandler,
|
||||
unregisterTransactionsHandler,
|
||||
isMenuElementsSupported: false, // Not supported on platforms other than iOS
|
||||
};
|
||||
};
|
||||
|
||||
|
|
Before Width: | Height: | Size: 35 KiB |
Before Width: | Height: | Size: 90 KiB |
Before Width: | Height: | Size: 155 KiB |
Before Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 74 KiB |
BIN
img/flash-on.png
Before Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 7.2 KiB |
8
index.js
|
@ -1,3 +1,4 @@
|
|||
import './gesture-handler';
|
||||
import './shim.js';
|
||||
|
||||
import React, { useEffect } from 'react';
|
||||
|
@ -12,7 +13,12 @@ if (!Error.captureStackTrace) {
|
|||
Error.captureStackTrace = () => {};
|
||||
}
|
||||
|
||||
LogBox.ignoreLogs(['Require cycle:', 'Battery state `unknown` and monitoring disabled, this is normal for simulators and tvOS.']);
|
||||
LogBox.ignoreLogs([
|
||||
'Require cycle:',
|
||||
'Battery state `unknown` and monitoring disabled, this is normal for simulators and tvOS.',
|
||||
'Open debugger to view warnings.',
|
||||
'Non-serializable values were found in the navigation state',
|
||||
]);
|
||||
|
||||
const BlueAppComponent = () => {
|
||||
useEffect(() => {
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 60;
|
||||
objectVersion = 63;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
|
@ -14,7 +14,6 @@
|
|||
32F0A29A2311DBB20095C559 /* ComplicationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32F0A2992311DBB20095C559 /* ComplicationController.swift */; };
|
||||
6D2A6464258BA92D0092292B /* Stickers.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6D2A6463258BA92D0092292B /* Stickers.xcassets */; };
|
||||
6D2A6468258BA92D0092292B /* Stickers.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 6D2A6461258BA92C0092292B /* Stickers.appex */; platformFilter = ios; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
6D32C5C62596CE3A008C077C /* EventEmitter.m in Sources */ = {isa = PBXBuildFile; fileRef = 6D32C5C52596CE3A008C077C /* EventEmitter.m */; };
|
||||
6D4AF15925D21172009DD853 /* MarketAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D9A2E6A254BAB1B007B5B82 /* MarketAPI.swift */; };
|
||||
6D4AF16D25D21192009DD853 /* Placeholders.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DEB4BFA254FBA0E00E9F9AA /* Placeholders.swift */; };
|
||||
6D4AF17825D211A3009DD853 /* FiatUnit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6D2AA8072568B8F40090B089 /* FiatUnit.swift */; };
|
||||
|
@ -46,6 +45,8 @@
|
|||
782F075B5DD048449E2DECE9 /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = B9D9B3A7B2CB4255876B67AF /* libz.tbd */; };
|
||||
849047CA2702A32A008EE567 /* Handoff.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849047C92702A32A008EE567 /* Handoff.swift */; };
|
||||
84E05A842721191B001A0D3A /* Settings.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 84E05A832721191B001A0D3A /* Settings.bundle */; };
|
||||
B409AB042D71DFAA00BA06F8 /* MenuElementsEmitter.m in Sources */ = {isa = PBXBuildFile; fileRef = B409AB032D71DFAA00BA06F8 /* MenuElementsEmitter.m */; };
|
||||
B409AB062D71E07500BA06F8 /* MenuElementsEmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B409AB052D71E07500BA06F8 /* MenuElementsEmitter.swift */; };
|
||||
B40D4E34225841EC00428FCC /* Interface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B40D4E32225841EC00428FCC /* Interface.storyboard */; };
|
||||
B40D4E36225841ED00428FCC /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B40D4E35225841ED00428FCC /* Assets.xcassets */; };
|
||||
B40D4E3D225841ED00428FCC /* BlueWalletWatch Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = B40D4E3C225841ED00428FCC /* BlueWalletWatch Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
|
@ -113,12 +114,11 @@
|
|||
B440340F2BCC40A400162242 /* fiatUnits.json in Resources */ = {isa = PBXBuildFile; fileRef = B440340E2BCC40A400162242 /* fiatUnits.json */; };
|
||||
B44034102BCC40A400162242 /* fiatUnits.json in Resources */ = {isa = PBXBuildFile; fileRef = B440340E2BCC40A400162242 /* fiatUnits.json */; };
|
||||
B44034112BCC40A400162242 /* fiatUnits.json in Resources */ = {isa = PBXBuildFile; fileRef = B440340E2BCC40A400162242 /* fiatUnits.json */; };
|
||||
B44305BC2D6A04B2004675CC /* CustomSegmentedControl.m in Sources */ = {isa = PBXBuildFile; fileRef = B44305BB2D6A04B2004675CC /* CustomSegmentedControl.m */; };
|
||||
B450109C2C0FCD8A00619044 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = B450109B2C0FCD8A00619044 /* Utilities.swift */; };
|
||||
B450109D2C0FCD9F00619044 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = B450109B2C0FCD8A00619044 /* Utilities.swift */; };
|
||||
B450109F2C0FCDA500619044 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = B450109B2C0FCD8A00619044 /* Utilities.swift */; };
|
||||
B45010A62C1507DE00619044 /* CustomSegmentedControlManager.m in Sources */ = {isa = PBXBuildFile; fileRef = B45010A52C1507DE00619044 /* CustomSegmentedControlManager.m */; };
|
||||
B4549F362B82B10D002E3153 /* ci_post_clone.sh in Resources */ = {isa = PBXBuildFile; fileRef = B4549F352B82B10D002E3153 /* ci_post_clone.sh */; };
|
||||
B45942C42CDECF2400B3DC2E /* MenuElementsEmitter.m in Sources */ = {isa = PBXBuildFile; fileRef = B4C075292CDDB3C500322A84 /* MenuElementsEmitter.m */; };
|
||||
B461B852299599F800E431AA /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = B461B851299599F800E431AA /* AppDelegate.mm */; };
|
||||
B4742E972CCDBE8300380EEE /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B4742E962CCDBE8300380EEE /* Localizable.xcstrings */; };
|
||||
B4742E982CCDBE8300380EEE /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B4742E962CCDBE8300380EEE /* Localizable.xcstrings */; };
|
||||
|
@ -156,6 +156,8 @@
|
|||
B4AB225E2B02AD12001F4328 /* XMLParserDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4AB225C2B02AD12001F4328 /* XMLParserDelegate.swift */; };
|
||||
B4B1A4622BFA73110072E3BB /* WidgetHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4B1A4612BFA73110072E3BB /* WidgetHelper.swift */; };
|
||||
B4B1A4642BFA73110072E3BB /* WidgetHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4B1A4612BFA73110072E3BB /* WidgetHelper.swift */; };
|
||||
B4B3EC222D69FF6C00327F3D /* CustomSegmentedControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4B3EC202D69FF6C00327F3D /* CustomSegmentedControl.swift */; };
|
||||
B4B3EC252D69FF8700327F3D /* EventEmitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4B3EC232D69FF8700327F3D /* EventEmitter.swift */; };
|
||||
B4D0B2622C1DEA11006B6B1B /* ReceivePageInterfaceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4D0B2612C1DEA11006B6B1B /* ReceivePageInterfaceController.swift */; };
|
||||
B4D0B2642C1DEA99006B6B1B /* ReceiveType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4D0B2632C1DEA99006B6B1B /* ReceiveType.swift */; };
|
||||
B4D0B2662C1DEB7F006B6B1B /* ReceiveInterfaceMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4D0B2652C1DEB7F006B6B1B /* ReceiveInterfaceMode.swift */; };
|
||||
|
@ -294,8 +296,6 @@
|
|||
6D2A6463258BA92D0092292B /* Stickers.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Stickers.xcassets; sourceTree = "<group>"; };
|
||||
6D2A6465258BA92D0092292B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
6D2AA8072568B8F40090B089 /* FiatUnit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FiatUnit.swift; sourceTree = "<group>"; };
|
||||
6D32C5C42596CE2F008C077C /* EventEmitter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = EventEmitter.h; sourceTree = "<group>"; };
|
||||
6D32C5C52596CE3A008C077C /* EventEmitter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = EventEmitter.m; sourceTree = "<group>"; };
|
||||
6D333B3A252FE1A3004D72DF /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
|
||||
6D333B3C252FE1A3004D72DF /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
|
||||
6D4AF18225D215D0009DD853 /* BlueWalletWatch-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "BlueWalletWatch-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
|
@ -336,6 +336,8 @@
|
|||
9F1F51A83D044F3BB26A35FC /* libRNSVG-tvOS.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = "libRNSVG-tvOS.a"; sourceTree = "<group>"; };
|
||||
A7C4B1FDAD264618BAF8C335 /* libRNCWebView.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNCWebView.a; sourceTree = "<group>"; };
|
||||
AB2325650CE04F018697ACFE /* libRNReactNativeHapticFeedback.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNReactNativeHapticFeedback.a; sourceTree = "<group>"; };
|
||||
B409AB032D71DFAA00BA06F8 /* MenuElementsEmitter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = MenuElementsEmitter.m; path = MenuElementsEmitter/MenuElementsEmitter.m; sourceTree = SOURCE_ROOT; };
|
||||
B409AB052D71E07500BA06F8 /* MenuElementsEmitter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = MenuElementsEmitter.swift; path = MenuElementsEmitter/MenuElementsEmitter.swift; sourceTree = SOURCE_ROOT; };
|
||||
B40D4E30225841EC00428FCC /* BlueWalletWatch.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = BlueWalletWatch.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
B40D4E33225841EC00428FCC /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Interface.storyboard; sourceTree = "<group>"; };
|
||||
B40D4E35225841ED00428FCC /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
|
@ -372,9 +374,8 @@
|
|||
B44033F82BCC379200162242 /* WidgetDataStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetDataStore.swift; sourceTree = "<group>"; };
|
||||
B44033FF2BCC37F800162242 /* Bundle+decode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+decode.swift"; sourceTree = "<group>"; };
|
||||
B440340E2BCC40A400162242 /* fiatUnits.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; name = fiatUnits.json; path = ../../../models/fiatUnits.json; sourceTree = "<group>"; };
|
||||
B44305BB2D6A04B2004675CC /* CustomSegmentedControl.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CustomSegmentedControl.m; sourceTree = "<group>"; };
|
||||
B450109B2C0FCD8A00619044 /* Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utilities.swift; sourceTree = "<group>"; };
|
||||
B45010A52C1507DE00619044 /* CustomSegmentedControlManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CustomSegmentedControlManager.m; sourceTree = "<group>"; };
|
||||
B45010A92C15080500619044 /* CustomSegmentedControlManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CustomSegmentedControlManager.h; sourceTree = "<group>"; };
|
||||
B4549F352B82B10D002E3153 /* ci_post_clone.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; path = ci_post_clone.sh; sourceTree = "<group>"; };
|
||||
B461B850299599F800E431AA /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = BlueWallet/AppDelegate.h; sourceTree = "<group>"; };
|
||||
B461B851299599F800E431AA /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = BlueWallet/AppDelegate.mm; sourceTree = "<group>"; };
|
||||
|
@ -396,8 +397,8 @@
|
|||
B4AB225C2B02AD12001F4328 /* XMLParserDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XMLParserDelegate.swift; sourceTree = "<group>"; };
|
||||
B4B1A4612BFA73110072E3BB /* WidgetHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetHelper.swift; sourceTree = "<group>"; };
|
||||
B4B31A352C77BBA000663334 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = nb.lproj/Interface.strings; sourceTree = "<group>"; };
|
||||
B4C075282CDDB3BE00322A84 /* MenuElementsEmitter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MenuElementsEmitter.h; sourceTree = "<group>"; };
|
||||
B4C075292CDDB3C500322A84 /* MenuElementsEmitter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MenuElementsEmitter.m; sourceTree = "<group>"; };
|
||||
B4B3EC202D69FF6C00327F3D /* CustomSegmentedControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomSegmentedControl.swift; sourceTree = "<group>"; };
|
||||
B4B3EC232D69FF8700327F3D /* EventEmitter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventEmitter.swift; sourceTree = "<group>"; };
|
||||
B4D0B2612C1DEA11006B6B1B /* ReceivePageInterfaceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceivePageInterfaceController.swift; sourceTree = "<group>"; };
|
||||
B4D0B2632C1DEA99006B6B1B /* ReceiveType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiveType.swift; sourceTree = "<group>"; };
|
||||
B4D0B2652C1DEB7F006B6B1B /* ReceiveInterfaceMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceiveInterfaceMode.swift; sourceTree = "<group>"; };
|
||||
|
@ -489,7 +490,6 @@
|
|||
13B07FAE1A68108700A75B9A /* BlueWallet */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B4C0752B2CDDB3CC00322A84 /* MenuElementsEmitter */,
|
||||
B461B850299599F800E431AA /* AppDelegate.h */,
|
||||
B461B851299599F800E431AA /* AppDelegate.mm */,
|
||||
32C7944323B8879D00BE2AFA /* BlueWalletRelease.entitlements */,
|
||||
|
@ -501,8 +501,6 @@
|
|||
32B5A3292334450100F8D608 /* Bridge.swift */,
|
||||
32B5A3282334450100F8D608 /* BlueWallet-Bridging-Header.h */,
|
||||
6DF25A9E249DB97E001D06F5 /* LaunchScreen.storyboard */,
|
||||
6D32C5C42596CE2F008C077C /* EventEmitter.h */,
|
||||
6D32C5C52596CE3A008C077C /* EventEmitter.m */,
|
||||
84E05A832721191B001A0D3A /* Settings.bundle */,
|
||||
B4742E962CCDBE8300380EEE /* Localizable.xcstrings */,
|
||||
);
|
||||
|
@ -677,6 +675,15 @@
|
|||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B409AB072D71E07C00BA06F8 /* MenuElementsEmitter */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B409AB052D71E07500BA06F8 /* MenuElementsEmitter.swift */,
|
||||
B409AB032D71DFAA00BA06F8 /* MenuElementsEmitter.m */,
|
||||
);
|
||||
path = MenuElementsEmitter;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B40D4E31225841EC00428FCC /* BlueWalletWatch */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -786,6 +793,15 @@
|
|||
path = Shared;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B44305BD2D6A04B9004675CC /* SegmentedControl */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B44305BB2D6A04B2004675CC /* CustomSegmentedControl.m */,
|
||||
B4B3EC202D69FF6C00327F3D /* CustomSegmentedControl.swift */,
|
||||
);
|
||||
path = SegmentedControl;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B450109A2C0FCD7E00619044 /* Utilities */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -798,21 +814,14 @@
|
|||
B45010A12C1504E900619044 /* Components */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B409AB072D71E07C00BA06F8 /* MenuElementsEmitter */,
|
||||
B44305BD2D6A04B9004675CC /* SegmentedControl */,
|
||||
B4B3EC232D69FF8700327F3D /* EventEmitter.swift */,
|
||||
B4B1A4612BFA73110072E3BB /* WidgetHelper.swift */,
|
||||
B45010A82C1507F000619044 /* SegmentedControl */,
|
||||
);
|
||||
path = Components;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B45010A82C1507F000619044 /* SegmentedControl */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B45010A52C1507DE00619044 /* CustomSegmentedControlManager.m */,
|
||||
B45010A92C15080500619044 /* CustomSegmentedControlManager.h */,
|
||||
);
|
||||
path = SegmentedControl;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B4549F2E2B80FEA1002E3153 /* ci_scripts */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -829,15 +838,6 @@
|
|||
path = BlueWalletUITests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B4C0752B2CDDB3CC00322A84 /* MenuElementsEmitter */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B4C075292CDDB3C500322A84 /* MenuElementsEmitter.m */,
|
||||
B4C075282CDDB3BE00322A84 /* MenuElementsEmitter.h */,
|
||||
);
|
||||
path = MenuElementsEmitter;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FAA856B639C61E61D2CF90A8 /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -993,7 +993,7 @@
|
|||
};
|
||||
};
|
||||
buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "BlueWallet" */;
|
||||
compatibilityVersion = "Xcode 15.0";
|
||||
compatibilityVersion = "Xcode 15.3";
|
||||
developmentRegion = en_US;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
|
@ -1239,28 +1239,30 @@
|
|||
B44033EE2BCC374500162242 /* Numeric+abbreviated.swift in Sources */,
|
||||
B48630E82CCEE92400A8425C /* PriceWidget.swift in Sources */,
|
||||
B44033DD2BCC36C300162242 /* LatestTransaction.swift in Sources */,
|
||||
6D32C5C62596CE3A008C077C /* EventEmitter.m in Sources */,
|
||||
B49A28C12CD199FC006B08E4 /* SwiftTCPClient.swift in Sources */,
|
||||
B44033FE2BCC37D700162242 /* MarketAPI.swift in Sources */,
|
||||
B450109C2C0FCD8A00619044 /* Utilities.swift in Sources */,
|
||||
B409AB042D71DFAA00BA06F8 /* MenuElementsEmitter.m in Sources */,
|
||||
B48630E52CCEE8B800A8425C /* PriceView.swift in Sources */,
|
||||
B48630E72CCEE91900A8425C /* PriceWidgetProvider.swift in Sources */,
|
||||
B4B3EC252D69FF8700327F3D /* EventEmitter.swift in Sources */,
|
||||
B49A28C02CD199C7006B08E4 /* MarketAPI+Electrum.swift in Sources */,
|
||||
B48630ED2CCEEEB000A8425C /* WalletAppShortcuts.swift in Sources */,
|
||||
B45010A62C1507DE00619044 /* CustomSegmentedControlManager.m in Sources */,
|
||||
B409AB062D71E07500BA06F8 /* MenuElementsEmitter.swift in Sources */,
|
||||
B44033CE2BCC352900162242 /* UserDefaultsGroup.swift in Sources */,
|
||||
13B07FC11A68108700A75B9A /* main.m in Sources */,
|
||||
B45942C42CDECF2400B3DC2E /* MenuElementsEmitter.m in Sources */,
|
||||
B461B852299599F800E431AA /* AppDelegate.mm in Sources */,
|
||||
B44033F42BCC377F00162242 /* WidgetData.swift in Sources */,
|
||||
B49A28C52CD1A894006B08E4 /* MarketData.swift in Sources */,
|
||||
B49A28BF2CD18A9A006B08E4 /* FiatUnitEnum.swift in Sources */,
|
||||
B44305BC2D6A04B2004675CC /* CustomSegmentedControl.m in Sources */,
|
||||
B44033C42BCC332400162242 /* Balance.swift in Sources */,
|
||||
B48630EE2CCEEEE900A8425C /* PriceIntent.swift in Sources */,
|
||||
B44034072BCC38A000162242 /* FiatUnit.swift in Sources */,
|
||||
B44034002BCC37F800162242 /* Bundle+decode.swift in Sources */,
|
||||
B44033E22BCC36CB00162242 /* Placeholders.swift in Sources */,
|
||||
B4793DBB2CEDACBD00C92C2E /* Chain.swift in Sources */,
|
||||
B4B3EC222D69FF6C00327F3D /* CustomSegmentedControl.swift in Sources */,
|
||||
B4B1A4622BFA73110072E3BB /* WidgetHelper.swift in Sources */,
|
||||
B48630E12CCEE7C800A8425C /* PriceWidgetEntryView.swift in Sources */,
|
||||
B44033DA2BCC369A00162242 /* Colors.swift in Sources */,
|
||||
|
@ -1453,7 +1455,7 @@
|
|||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 1703136799;
|
||||
CURRENT_PROJECT_VERSION = 1703169999;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = A7W54YZ4WU;
|
||||
|
@ -1471,7 +1473,7 @@
|
|||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.finance";
|
||||
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = io.bluewallet.bluewallet;
|
||||
INFOPLIST_KEY_WKExtensionDelegateClassName = "$(PRODUCT_BUNDLE_IDENTIFIER).ExtensionDelegate";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
|
@ -1481,7 +1483,7 @@
|
|||
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
||||
"$(inherited)",
|
||||
);
|
||||
MARKETING_VERSION = 7.0.6;
|
||||
MARKETING_VERSION = 7.1.5;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
|
@ -1516,7 +1518,7 @@
|
|||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 1703136799;
|
||||
CURRENT_PROJECT_VERSION = 1703169999;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = A7W54YZ4WU;
|
||||
|
@ -1529,7 +1531,7 @@
|
|||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.finance";
|
||||
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = io.bluewallet.bluewallet;
|
||||
INFOPLIST_KEY_WKExtensionDelegateClassName = "$(PRODUCT_BUNDLE_IDENTIFIER).ExtensionDelegate";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.4;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
|
@ -1539,7 +1541,7 @@
|
|||
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
||||
"$(inherited)",
|
||||
);
|
||||
MARKETING_VERSION = 7.0.6;
|
||||
MARKETING_VERSION = 7.1.5;
|
||||
OTHER_LDFLAGS = (
|
||||
"$(inherited)",
|
||||
"-ObjC",
|
||||
|
@ -1575,20 +1577,20 @@
|
|||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 1703136799;
|
||||
CURRENT_PROJECT_VERSION = 1703169999;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = A7W54YZ4WU;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
INFOPLIST_FILE = Stickers/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(SDKROOT)/usr/lib/swift",
|
||||
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
||||
"$(inherited)",
|
||||
);
|
||||
MARKETING_VERSION = 7.0.6;
|
||||
MARKETING_VERSION = 7.1.5;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
|
||||
|
@ -1618,20 +1620,20 @@
|
|||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1703136799;
|
||||
CURRENT_PROJECT_VERSION = 1703169999;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = A7W54YZ4WU;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
INFOPLIST_FILE = Stickers/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.1;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.6;
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(SDKROOT)/usr/lib/swift",
|
||||
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
||||
"$(inherited)",
|
||||
);
|
||||
MARKETING_VERSION = 7.0.6;
|
||||
MARKETING_VERSION = 7.1.5;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.Stickers;
|
||||
|
@ -1662,7 +1664,7 @@
|
|||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 1703136799;
|
||||
CURRENT_PROJECT_VERSION = 1703169999;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
|
@ -1670,7 +1672,7 @@
|
|||
"DEVELOPMENT_TEAM[sdk=macosx*]" = A7W54YZ4WU;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
INFOPLIST_FILE = Widgets/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
|
@ -1681,7 +1683,7 @@
|
|||
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
||||
"$(inherited)",
|
||||
);
|
||||
MARKETING_VERSION = 7.0.6;
|
||||
MARKETING_VERSION = 7.1.5;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
|
||||
|
@ -1718,7 +1720,7 @@
|
|||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1703136799;
|
||||
CURRENT_PROJECT_VERSION = 1703169999;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
|
@ -1726,7 +1728,7 @@
|
|||
"DEVELOPMENT_TEAM[sdk=macosx*]" = A7W54YZ4WU;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
INFOPLIST_FILE = Widgets/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 16.6;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.6;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
|
@ -1737,7 +1739,7 @@
|
|||
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
||||
"$(inherited)",
|
||||
);
|
||||
MARKETING_VERSION = 7.0.6;
|
||||
MARKETING_VERSION = 7.1.5;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.MarketWidget;
|
||||
|
@ -1905,7 +1907,7 @@
|
|||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 1703136799;
|
||||
CURRENT_PROJECT_VERSION = 1703169999;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
|
@ -1925,7 +1927,7 @@
|
|||
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
||||
"$(inherited)",
|
||||
);
|
||||
MARKETING_VERSION = 7.0.6;
|
||||
MARKETING_VERSION = 7.1.5;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
|
||||
|
@ -1958,7 +1960,7 @@
|
|||
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1703136799;
|
||||
CURRENT_PROJECT_VERSION = 1703169999;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
|
@ -1978,7 +1980,7 @@
|
|||
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
||||
"$(inherited)",
|
||||
);
|
||||
MARKETING_VERSION = 7.0.6;
|
||||
MARKETING_VERSION = 7.1.5;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.watch.extension;
|
||||
|
@ -2010,7 +2012,7 @@
|
|||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Developer";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 1703136799;
|
||||
CURRENT_PROJECT_VERSION = 1703169999;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = "";
|
||||
|
@ -2024,7 +2026,7 @@
|
|||
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
||||
"$(inherited)",
|
||||
);
|
||||
MARKETING_VERSION = 7.0.6;
|
||||
MARKETING_VERSION = 7.1.5;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
|
||||
|
@ -2059,7 +2061,7 @@
|
|||
"CODE_SIGN_IDENTITY[sdk=watchos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 1703136799;
|
||||
CURRENT_PROJECT_VERSION = 1703169999;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = "";
|
||||
|
@ -2073,7 +2075,7 @@
|
|||
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
||||
"$(inherited)",
|
||||
);
|
||||
MARKETING_VERSION = 7.0.6;
|
||||
MARKETING_VERSION = 7.1.5;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRESERVE_DEAD_CODE_INITS_AND_TERMS = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = io.bluewallet.bluewallet.watch;
|
||||
|
|
|
@ -6,12 +6,9 @@
|
|||
#import "RNQuickActionManager.h"
|
||||
#import <UserNotifications/UserNotifications.h>
|
||||
#import <RNCPushNotificationIOS.h>
|
||||
#import "EventEmitter.h"
|
||||
#import "MenuElementsEmitter.h"
|
||||
#import <React/RCTRootView.h>
|
||||
#import <Bugsnag/Bugsnag.h>
|
||||
#import "BlueWallet-Swift.h"
|
||||
#import "CustomSegmentedControlManager.h"
|
||||
|
||||
@interface AppDelegate() <UNUserNotificationCenterDelegate>
|
||||
|
||||
|
@ -23,8 +20,6 @@
|
|||
|
||||
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
|
||||
{
|
||||
[MenuElementsEmitter sharedInstance];
|
||||
[CustomSegmentedControlManager registerIfNecessary];
|
||||
[self clearFilesIfNeeded];
|
||||
self.userDefaultsGroup = [[NSUserDefaults alloc] initWithSuiteName:@"group.io.bluewallet.bluewallet"];
|
||||
|
||||
|
@ -154,27 +149,42 @@
|
|||
- (BOOL)application:(UIApplication *)application continueUserActivity:(nonnull NSUserActivity *)userActivity
|
||||
restorationHandler:(nonnull void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler
|
||||
{
|
||||
NSDictionary *userActivityData = @{@"activityType": userActivity.activityType, @"userInfo": userActivity.userInfo};
|
||||
// Validate userActivity and its type
|
||||
if (!userActivity || !userActivity.activityType) {
|
||||
NSLog(@"[Handoff] Invalid or missing userActivity");
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSDictionary *userActivityData = @{@"activityType": userActivity.activityType ?: @"",
|
||||
@"userInfo": userActivity.userInfo ?: @{}};
|
||||
|
||||
// Save activity data to userDefaults for potential later use
|
||||
[self.userDefaultsGroup setValue:userActivityData forKey:@"onUserActivityOpen"];
|
||||
|
||||
// Check if the activity type matches the allowed types
|
||||
// Check if the activity type matches one of the allowed types
|
||||
if ([userActivity.activityType isEqualToString:@"io.bluewallet.bluewallet.receiveonchain"] ||
|
||||
[userActivity.activityType isEqualToString:@"io.bluewallet.bluewallet.xpub"] ||
|
||||
[userActivity.activityType isEqualToString:@"io.bluewallet.bluewallet.blockexplorer"]) {
|
||||
|
||||
[EventEmitter.sharedInstance sendUserActivity:userActivityData];
|
||||
if ([EventEmitter.shared respondsToSelector:@selector(sendUserActivity:)]) {
|
||||
[EventEmitter.shared sendUserActivity:userActivityData];
|
||||
} else {
|
||||
NSLog(@"[Handoff] EventEmitter does not implement sendUserActivity:");
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
if (userActivity.activityType == NSUserActivityTypeBrowsingWeb) {
|
||||
// Forward web browsing activities to LinkingManager
|
||||
if ([userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) {
|
||||
return [RCTLinkingManager application:application
|
||||
continueUserActivity:userActivity
|
||||
restorationHandler:restorationHandler];
|
||||
}
|
||||
|
||||
// If activity type does not match any of the specified types, do nothing
|
||||
NSLog(@"[Handoff] Unhandled user activity type: %@", userActivity.activityType);
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
|
||||
return [RCTLinkingManager application:app openURL:url options:options];
|
||||
}
|
||||
|
@ -195,7 +205,7 @@
|
|||
-(void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions options))completionHandler
|
||||
{
|
||||
NSDictionary *userInfo = notification.request.content.userInfo;
|
||||
completionHandler(UNNotificationPresentationOptionSound | UNNotificationPresentationOptionAlert | UNNotificationPresentationOptionBadge);
|
||||
completionHandler(UNNotificationPresentationOptionSound | UNNotificationPresentationOptionList | UNNotificationPresentationOptionBanner | UNNotificationPresentationOptionBadge);
|
||||
}
|
||||
|
||||
- (void)buildMenuWithBuilder:(id<UIMenuBuilder>)builder {
|
||||
|
@ -244,25 +254,59 @@
|
|||
}
|
||||
|
||||
- (void)openSettings:(UIKeyCommand *)keyCommand {
|
||||
[MenuElementsEmitter.sharedInstance openSettings];
|
||||
// Safely access the MenuElementsEmitter
|
||||
MenuElementsEmitter *emitter = [MenuElementsEmitter shared];
|
||||
if (emitter) {
|
||||
NSLog(@"[MenuElements] AppDelegate: openSettings called, calling emitter");
|
||||
// Force on main thread for consistency
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[emitter openSettings];
|
||||
});
|
||||
} else {
|
||||
NSLog(@"[MenuElements] AppDelegate: MenuElementsEmitter not available for openSettings");
|
||||
}
|
||||
}
|
||||
|
||||
- (void)addWalletAction:(UIKeyCommand *)keyCommand {
|
||||
// Implement the functionality for adding a wallet
|
||||
[MenuElementsEmitter.sharedInstance addWalletMenuAction];
|
||||
NSLog(@"Add Wallet action performed");
|
||||
// Safely access the MenuElementsEmitter
|
||||
MenuElementsEmitter *emitter = [MenuElementsEmitter shared];
|
||||
if (emitter) {
|
||||
NSLog(@"[MenuElements] AppDelegate: addWalletAction called, calling emitter");
|
||||
// Force on main thread for consistency
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[emitter addWalletMenuAction];
|
||||
});
|
||||
} else {
|
||||
NSLog(@"[MenuElements] AppDelegate: MenuElementsEmitter not available for addWalletAction");
|
||||
}
|
||||
}
|
||||
|
||||
- (void)importWalletAction:(UIKeyCommand *)keyCommand {
|
||||
// Implement the functionality for adding a wallet
|
||||
[MenuElementsEmitter.sharedInstance importWalletMenuAction];
|
||||
NSLog(@"Import Wallet action performed");
|
||||
// Safely access the MenuElementsEmitter
|
||||
MenuElementsEmitter *emitter = [MenuElementsEmitter shared];
|
||||
if (emitter) {
|
||||
NSLog(@"[MenuElements] AppDelegate: importWalletAction called, calling emitter");
|
||||
// Force on main thread for consistency
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[emitter importWalletMenuAction];
|
||||
});
|
||||
} else {
|
||||
NSLog(@"[MenuElements] AppDelegate: MenuElementsEmitter not available for importWalletAction");
|
||||
}
|
||||
}
|
||||
|
||||
- (void)reloadTransactionsAction:(UIKeyCommand *)keyCommand {
|
||||
// Implement the functionality for adding a wallet
|
||||
[MenuElementsEmitter.sharedInstance reloadTransactionsMenuAction];
|
||||
NSLog(@"Reload Transactions action performed");
|
||||
// Safely access the MenuElementsEmitter
|
||||
MenuElementsEmitter *emitter = [MenuElementsEmitter shared];
|
||||
if (emitter) {
|
||||
NSLog(@"[MenuElements] AppDelegate: reloadTransactionsAction called, calling emitter");
|
||||
// Force on main thread for consistency
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[emitter reloadTransactionsMenuAction];
|
||||
});
|
||||
} else {
|
||||
NSLog(@"[MenuElements] AppDelegate: MenuElementsEmitter not available for reloadTransactionsAction");
|
||||
}
|
||||
}
|
||||
|
||||
- (void)showHelp:(id)sender {
|
||||
|
|
Before Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 333 B |
Before Width: | Height: | Size: 614 B |
Before Width: | Height: | Size: 5.3 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 614 B |
Before Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 26 KiB |
|
@ -31,61 +31,61 @@
|
|||
"size" : "1024x1024"
|
||||
},
|
||||
{
|
||||
"filename" : "16pt@1x.png",
|
||||
"filename" : "icon_16x16.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"filename" : "16pt@2x.png",
|
||||
"filename" : "icon_16x16@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "16x16"
|
||||
},
|
||||
{
|
||||
"filename" : "32pt@1x.png",
|
||||
"filename" : "icon_32x32.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"filename" : "32pt@2x.png",
|
||||
"filename" : "icon_32x32@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "32x32"
|
||||
},
|
||||
{
|
||||
"filename" : "128pt@1x.png",
|
||||
"filename" : "icon_128x128.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"filename" : "128pt@2x.png",
|
||||
"filename" : "icon_128x128@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "128x128"
|
||||
},
|
||||
{
|
||||
"filename" : "256pt@1x.png",
|
||||
"filename" : "icon_256x256.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"filename" : "256pt@2x.png",
|
||||
"filename" : "icon_256x256@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "256x256"
|
||||
},
|
||||
{
|
||||
"filename" : "512pt@1x.png",
|
||||
"filename" : "icon_512x512.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "1x",
|
||||
"size" : "512x512"
|
||||
},
|
||||
{
|
||||
"filename" : "512pt@2x.png",
|
||||
"filename" : "icon_512x512@2x.png",
|
||||
"idiom" : "mac",
|
||||
"scale" : "2x",
|
||||
"size" : "512x512"
|
||||
|
|
After Width: | Height: | Size: 7.6 KiB |