mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-04-04 14:53:33 -04:00
Compare commits
923 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7e924c2741 | ||
|
|
241b851c02 | ||
|
|
2ba9949035 | ||
|
|
3d4b76ffc3 | ||
|
|
f7fb0c6c82 | ||
|
|
e5fc734b90 | ||
|
|
10fa659e52 | ||
|
|
0ac315b017 | ||
|
|
bbfbd2daae | ||
|
|
cd2c4d3314 | ||
|
|
37ee7752c2 | ||
|
|
6b11e6b060 | ||
|
|
f650ab7394 | ||
|
|
fead7459d4 | ||
|
|
bbf3970121 | ||
|
|
26fb810840 | ||
|
|
af9074af6e | ||
|
|
4229c2a4fa | ||
|
|
fd58602e6d | ||
|
|
af26397ad7 | ||
|
|
859d01594a | ||
|
|
09a37058e6 | ||
|
|
edc7366b1d | ||
|
|
7306cb335b | ||
|
|
1e13cc4294 | ||
|
|
a88e5a9ec8 | ||
|
|
09d743c603 | ||
|
|
36c1ec528a | ||
|
|
aec9600036 | ||
|
|
6e1306bdd6 | ||
|
|
37f5c95716 | ||
|
|
0d48c541a0 | ||
|
|
8928cd1667 | ||
|
|
780f935fea | ||
|
|
5bf095178d | ||
|
|
e135293b43 | ||
|
|
be7e009909 | ||
|
|
ada8a713c1 | ||
|
|
eb7391dae0 | ||
|
|
8bdbd5e4fe | ||
|
|
137d15ae71 | ||
|
|
8a2ed6f8ff | ||
|
|
7766b43187 | ||
|
|
b9a637fdac | ||
|
|
3096dbc922 | ||
|
|
d6b0bb4378 | ||
|
|
43ef43ba72 | ||
|
|
314037dd06 | ||
|
|
7c4d66bbf5 | ||
|
|
49d28961ef | ||
|
|
9d649daee5 | ||
|
|
7598a47283 | ||
|
|
c9f7230d27 | ||
|
|
716633c6df | ||
|
|
8a215a765f | ||
|
|
e2ff41a0b1 | ||
|
|
d790fae74a | ||
|
|
1a17862f45 | ||
|
|
acd4c36531 | ||
|
|
f623450f08 | ||
|
|
ce681cfb67 | ||
|
|
a0a6523a25 | ||
|
|
8e2fa36d0e | ||
|
|
c953a84c1f | ||
|
|
2b403f0761 | ||
|
|
f954e3b2d7 | ||
|
|
24ee2a206e | ||
|
|
02d165829f | ||
|
|
b4d996bd14 | ||
|
|
60989faa03 | ||
|
|
4aeda8a1a7 | ||
|
|
023a20f263 | ||
|
|
62821ed803 | ||
|
|
903d62ed57 | ||
|
|
adef626b34 | ||
|
|
11a933cc04 | ||
|
|
4b5ce99bb1 | ||
|
|
f8dff77cee | ||
|
|
0a9b08803e | ||
|
|
39a8568663 | ||
|
|
e1502440eb | ||
|
|
5834d032c3 | ||
|
|
0a28a2af61 | ||
|
|
259f56bd26 | ||
|
|
011ad66a3f | ||
|
|
281c72f88d | ||
|
|
1293523ebf | ||
|
|
67e5d34f39 | ||
|
|
8b3dec6116 | ||
|
|
3359f49f0a | ||
|
|
9715cae587 | ||
|
|
4b122bd907 | ||
|
|
edefa36c8a | ||
|
|
e40a352aed | ||
|
|
70267f0623 | ||
|
|
c19ed7f6d5 | ||
|
|
e2fb0f2ee8 | ||
|
|
f9e208a7e0 | ||
|
|
ee2b8a60a2 | ||
|
|
5651f17e96 | ||
|
|
9679b3722a | ||
|
|
6cb736a10d | ||
|
|
ac5c3b421d | ||
|
|
f01fad5fa6 | ||
|
|
110b727cbc | ||
|
|
1dba2debc5 | ||
|
|
8fec73341f | ||
|
|
33c76de397 | ||
|
|
d409eb1126 | ||
|
|
69d53591c8 | ||
|
|
4cf783f07f | ||
|
|
a971c121cc | ||
|
|
b4abfb6aa4 | ||
|
|
8f760081a5 | ||
|
|
c15934f210 | ||
|
|
c7e1bb4463 | ||
|
|
4ac97fc476 | ||
|
|
a5dda01ffd | ||
|
|
9db4937e54 | ||
|
|
3dc50a384e | ||
|
|
ccc48620b8 | ||
|
|
943c3960e1 | ||
|
|
9a84a67555 | ||
|
|
2241e3c7d3 | ||
|
|
03042dae96 | ||
|
|
3df9d067d6 | ||
|
|
c5f3e8b3c2 | ||
|
|
3a3e8c0e10 | ||
|
|
f010bf7eed | ||
|
|
b22d6a77b0 | ||
|
|
f91d9125e4 | ||
|
|
625eedef1e | ||
|
|
61dd33187a | ||
|
|
817640c7d0 | ||
|
|
aa15d5a6df | ||
|
|
60857007bd | ||
|
|
cfd0c5d846 | ||
|
|
71ec88d235 | ||
|
|
902ea583bd | ||
|
|
fb6b54fba0 | ||
|
|
9dfc56987c | ||
|
|
b982a02717 | ||
|
|
15a0d8ee16 | ||
|
|
9ca03132db | ||
|
|
f908ba3ea3 | ||
|
|
79ea09bd91 | ||
|
|
c743a4f1fe | ||
|
|
ccd1fc5c10 | ||
|
|
2f82a64dbe | ||
|
|
71c6eae9c4 | ||
|
|
28894a08cc | ||
|
|
41e366dcc4 | ||
|
|
6ee11a6b2d | ||
|
|
ab82c66f83 | ||
|
|
f94820f45e | ||
|
|
fe03e1ca68 | ||
|
|
44ee6951c9 | ||
|
|
40194f7204 | ||
|
|
40eefef9a2 | ||
|
|
d8ab44ebb5 | ||
|
|
f875539f2e | ||
|
|
d9c2d142cb | ||
|
|
51d5715f04 | ||
|
|
8ad81b1d50 | ||
|
|
6d4083d8a7 | ||
|
|
bb120afea2 | ||
|
|
cebcc20c26 | ||
|
|
0522e787cd | ||
|
|
81e01f1485 | ||
|
|
9b2b93d56f | ||
|
|
133b402e2b | ||
|
|
9a91eab13f | ||
|
|
5f6722b917 | ||
|
|
fdcd4d46ac | ||
|
|
27a72170b8 | ||
|
|
b3021e60ec | ||
|
|
6d8834bd87 | ||
|
|
ab8a9d95d8 | ||
|
|
563ee3f5df | ||
|
|
c3389a7fcf | ||
|
|
e824bbb533 | ||
|
|
7de0e351f3 | ||
|
|
2df5f55390 | ||
|
|
21038cc5ac | ||
|
|
d6749bcd41 | ||
|
|
005c9ccdef | ||
|
|
d47cb53e59 | ||
|
|
adfb3ca4f0 | ||
|
|
88b7e4edda | ||
|
|
e33b57f0fd | ||
|
|
777825b73f | ||
|
|
04ca9b6f9a | ||
|
|
7f3d3eeb9c | ||
|
|
75ac16d43c | ||
|
|
5f1120c718 | ||
|
|
806a0694c6 | ||
|
|
c506ae3242 | ||
|
|
444fa529fb | ||
|
|
62ced3eb04 | ||
|
|
31718f1c4d | ||
|
|
98ab8971b4 | ||
|
|
870c86f9af | ||
|
|
a4f6aabee9 | ||
|
|
5584ad0a10 | ||
|
|
6b4932b4c5 | ||
|
|
7f1cb0aaf3 | ||
|
|
d42a5a48e9 | ||
|
|
8a1b762c35 | ||
|
|
d2d1de41d2 | ||
|
|
d1366af2a0 | ||
|
|
cae4b15bbb | ||
|
|
e41bc64b0c | ||
|
|
3d03ebe487 | ||
|
|
1fcb431d1b | ||
|
|
4f321490af | ||
|
|
04c7469e68 | ||
|
|
98bc0a9309 | ||
|
|
6e537f4cdf | ||
|
|
b121d61852 | ||
|
|
6ba3c3ffbd | ||
|
|
67b3efad4c | ||
|
|
1282e468e3 | ||
|
|
67b2e78a63 | ||
|
|
213c56f945 | ||
|
|
ccde6e4f4b | ||
|
|
2c1c94d24c | ||
|
|
56edb51f36 | ||
|
|
b6ed2c7dd8 | ||
|
|
74c7c5c423 | ||
|
|
314d774bde | ||
|
|
d94091ae4e | ||
|
|
8bf3c4fcd7 | ||
|
|
4358c15432 | ||
|
|
fdf8d649fe | ||
|
|
a4e22ec4b1 | ||
|
|
783bc43547 | ||
|
|
2e6aec175a | ||
|
|
ee41aaa112 | ||
|
|
32e8a8b1b9 | ||
|
|
6b3def230b | ||
|
|
51c46db106 | ||
|
|
64c18179ac | ||
|
|
5ee0b39e07 | ||
|
|
6470319fd6 | ||
|
|
4ca3f3c8ee | ||
|
|
07e2d2d509 | ||
|
|
07d2a143a2 | ||
|
|
968845492f | ||
|
|
c2106b64f9 | ||
|
|
0d73d1d258 | ||
|
|
423d425950 | ||
|
|
7ab0093fec | ||
|
|
56d2757448 | ||
|
|
f5b6037367 | ||
|
|
2c4ce6c8d1 | ||
|
|
f9ccd3628a | ||
|
|
abef2b394e | ||
|
|
97ff6f1de9 | ||
|
|
7fad4435cb | ||
|
|
92a97209ca | ||
|
|
e7c3f083b4 | ||
|
|
7b5961f941 | ||
|
|
85d4b411b5 | ||
|
|
c9ec32aca7 | ||
|
|
13a3013a8e | ||
|
|
0dd3bac855 | ||
|
|
d7a716a5cb | ||
|
|
0bbc9cbe81 | ||
|
|
df766d8d1f | ||
|
|
20a150a228 | ||
|
|
010292a440 | ||
|
|
394dae18e9 | ||
|
|
f1676c52f0 | ||
|
|
05f6f3c79b | ||
|
|
9942b488ea | ||
|
|
5d83f4bf2d | ||
|
|
d9afd914ff | ||
|
|
990d8160f8 | ||
|
|
80c321b66f | ||
|
|
67b45f3e5c | ||
|
|
ca3cee5673 | ||
|
|
ae0d170244 | ||
|
|
9a31cfa938 | ||
|
|
cdbc9c305e | ||
|
|
cdbce13c49 | ||
|
|
0a41ebbcda | ||
|
|
476fe1602b | ||
|
|
2f482e9afc | ||
|
|
d59e6e0691 | ||
|
|
7ec6866f26 | ||
|
|
3686e7facf | ||
|
|
1aba1894ea | ||
|
|
14503c9b8f | ||
|
|
a315668d31 | ||
|
|
db9ea63210 | ||
|
|
51ecbd5b53 | ||
|
|
45ef0d9809 | ||
|
|
356702b50d | ||
|
|
e72ce5376c | ||
|
|
0d35f3fbd2 | ||
|
|
28c22c1eae | ||
|
|
7cf83f65c3 | ||
|
|
4c4f2e720d | ||
|
|
0fa562e6fd | ||
|
|
bcd0f778cf | ||
|
|
401ab9f706 | ||
|
|
8b09a81d5a | ||
|
|
86718cc406 | ||
|
|
ccb72364e1 | ||
|
|
bfd6a59d87 | ||
|
|
af4caa455a | ||
|
|
d511e4a75c | ||
|
|
8fd906223c | ||
|
|
deadb56aaa | ||
|
|
1ff867879c | ||
|
|
f3630dd868 | ||
|
|
39143525c3 | ||
|
|
e8dd1f8f8b | ||
|
|
28e5d2e3f2 | ||
|
|
21def91427 | ||
|
|
bc5d04c662 | ||
|
|
c736227448 | ||
|
|
168285cb64 | ||
|
|
3411f50d29 | ||
|
|
319c14b0e0 | ||
|
|
64c077396f | ||
|
|
65f0d448a1 | ||
|
|
2fdaa464dd | ||
|
|
f86a6a10ac | ||
|
|
08a2438e79 | ||
|
|
60cc47f7ca | ||
|
|
7e4f606492 | ||
|
|
8ff58534d9 | ||
|
|
a4bb184e95 | ||
|
|
940c1f5692 | ||
|
|
0430733b58 | ||
|
|
33136816af | ||
|
|
469a235799 | ||
|
|
2d71c827b3 | ||
|
|
17f9e72a9f | ||
|
|
120aeed56f | ||
|
|
8128c3b2d7 | ||
|
|
4581a33cae | ||
|
|
d43e2af3ae | ||
|
|
0283eb4d85 | ||
|
|
f483ae42a8 | ||
|
|
f974eba465 | ||
|
|
7d7969fe0f | ||
|
|
2a3d7e8362 | ||
|
|
a38c03c4e0 | ||
|
|
342a0c612a | ||
|
|
f1f9970407 | ||
|
|
c83a3e6315 | ||
|
|
fbc65e7e2a | ||
|
|
d9e9407cab | ||
|
|
d0881b76e0 | ||
|
|
61c867b49c | ||
|
|
87de30d257 | ||
|
|
817605417c | ||
|
|
65b018db2a | ||
|
|
addb52e3fa | ||
|
|
c3124ec7c3 | ||
|
|
b1caa6dfdc | ||
|
|
26f28d55d9 | ||
|
|
cb66688529 | ||
|
|
40c61f11be | ||
|
|
9b45c75a5b | ||
|
|
09425c1910 | ||
|
|
591798e98c | ||
|
|
acdd82a680 | ||
|
|
5719ac3209 | ||
|
|
2ac687b0c2 | ||
|
|
a86a249f05 | ||
|
|
e49a7b0bb4 | ||
|
|
e904a7949c | ||
|
|
080d8110df | ||
|
|
d90826e851 | ||
|
|
e495da692b | ||
|
|
52970ef93e | ||
|
|
8f641d117a | ||
|
|
d164ebc7da | ||
|
|
632cc5810d | ||
|
|
e565a6c77f | ||
|
|
c1fe700d7a | ||
|
|
06ee267804 | ||
|
|
aad722c723 | ||
|
|
aefc58b636 | ||
|
|
fdd96507b8 | ||
|
|
2ad87a5ec5 | ||
|
|
b94cdba5af | ||
|
|
725261335c | ||
|
|
5fb0051fc6 | ||
|
|
1247847739 | ||
|
|
18cb4e74d6 | ||
|
|
e07cb7fca9 | ||
|
|
dc54ed46f8 | ||
|
|
0415d86d71 | ||
|
|
b8b95be5ce | ||
|
|
46820f0986 | ||
|
|
dcc022ac7f | ||
|
|
9142f0d633 | ||
|
|
181c72befe | ||
|
|
99f3459978 | ||
|
|
75fbc9679c | ||
|
|
700b7774b1 | ||
|
|
d9f0a9b1ca | ||
|
|
70644ff26d | ||
|
|
bbefcc3bc8 | ||
|
|
09767dbae3 | ||
|
|
57eafa95ba | ||
|
|
f4f28a411e | ||
|
|
f6059ef5c7 | ||
|
|
e3fa4efa95 | ||
|
|
6884a91eb8 | ||
|
|
71ba018a42 | ||
|
|
10f5232ac3 | ||
|
|
78d707484d | ||
|
|
69db66fbbb | ||
|
|
99691cd7ee | ||
|
|
47cef359ca | ||
|
|
046105498f | ||
|
|
4d3ef5dd2a | ||
|
|
8bcd5623bf | ||
|
|
a29b4a3a8e | ||
|
|
dee0fb396b | ||
|
|
b5c707e07f | ||
|
|
8fe35bd1d7 | ||
|
|
6c864c35cd | ||
|
|
f00acf6af1 | ||
|
|
49e1599bc4 | ||
|
|
7311d4b724 | ||
|
|
fa44e348a2 | ||
|
|
8cba02741c | ||
|
|
48d04d5e72 | ||
|
|
7cac25c0e2 | ||
|
|
88b0fa0163 | ||
|
|
3fcef24cc9 | ||
|
|
d9fba6ce6b | ||
|
|
8bc2f0c40e | ||
|
|
21254695d5 | ||
|
|
f063f0a6f4 | ||
|
|
2d847cbcdb | ||
|
|
547e99f526 | ||
|
|
a9794cd2ee | ||
|
|
c651abd8ae | ||
|
|
15781475b6 | ||
|
|
26a28175fd | ||
|
|
aa3680934b | ||
|
|
0b36586ddf | ||
|
|
7b58acac0d | ||
|
|
27bf4eadf9 | ||
|
|
c8d4353888 | ||
|
|
4876ca2643 | ||
|
|
e06a382c94 | ||
|
|
d1a421ca15 | ||
|
|
cd3c8d89d0 | ||
|
|
1f943ccead | ||
|
|
753776fa9a | ||
|
|
9787a2446e | ||
|
|
4cb29d274b | ||
|
|
df55d63f99 | ||
|
|
236e2d48c5 | ||
|
|
30d45d834f | ||
|
|
edf30897f2 | ||
|
|
3d31ebb576 | ||
|
|
d3bac8bcc0 | ||
|
|
a360f80cdf | ||
|
|
0cc7549546 | ||
|
|
283d2743e0 | ||
|
|
b431fa11fa | ||
|
|
648e00867f | ||
|
|
552af7bb6b | ||
|
|
92980f7c79 | ||
|
|
09a563bf73 | ||
|
|
141fa12a20 | ||
|
|
6e0035d4f3 | ||
|
|
97bac4132c | ||
|
|
b23b0280cd | ||
|
|
7ac093a8d0 | ||
|
|
dfc524b957 | ||
|
|
65ba0d348b | ||
|
|
ed07031539 | ||
|
|
93f3690344 | ||
|
|
1341d1356a | ||
|
|
38dcf16c03 | ||
|
|
8696a42959 | ||
|
|
c6fc7db1e9 | ||
|
|
58540aca57 | ||
|
|
b7b75279c2 | ||
|
|
204a35d026 | ||
|
|
fb2841f198 | ||
|
|
5de055c977 | ||
|
|
084659ea3d | ||
|
|
c1a414afab | ||
|
|
a5747034d6 | ||
|
|
fda52fec97 | ||
|
|
e38ec79618 | ||
|
|
1ef125db12 | ||
|
|
b580b640bd | ||
|
|
214bddaca4 | ||
|
|
065d489869 | ||
|
|
46ffefbbb9 | ||
|
|
a19db3bca9 | ||
|
|
2c8d8d9989 | ||
|
|
d52943e31e | ||
|
|
3eababb742 | ||
|
|
8a954d3c20 | ||
|
|
8516901032 | ||
|
|
3f2d246fec | ||
|
|
58fdaa26ca | ||
|
|
7dc1a8790d | ||
|
|
70c9ec1d73 | ||
|
|
2bcbbc96ad | ||
|
|
527d36a159 | ||
|
|
2ce21247ee | ||
|
|
8ea6c406e0 | ||
|
|
e22f50ecd3 | ||
|
|
20dcd98fdf | ||
|
|
bc5708857a | ||
|
|
b9c045ebfb | ||
|
|
c69bd7018e | ||
|
|
078d149175 | ||
|
|
be9f0bd061 | ||
|
|
a4723563f5 | ||
|
|
1fdcd24f28 | ||
|
|
a43480db92 | ||
|
|
e85a072f1c | ||
|
|
bbfa2a4eab | ||
|
|
2f2db4ded8 | ||
|
|
7296a0d2cd | ||
|
|
08e02b6ac0 | ||
|
|
715811d7fd | ||
|
|
c7d6ae6995 | ||
|
|
b1d1396944 | ||
|
|
25a319710e | ||
|
|
796b13dd62 | ||
|
|
8197863ac5 | ||
|
|
89bd164d43 | ||
|
|
80d7061e5f | ||
|
|
c49bac3a09 | ||
|
|
06d53fe801 | ||
|
|
15ba529938 | ||
|
|
83054d0cd1 | ||
|
|
8da486adf2 | ||
|
|
32bc3847fa | ||
|
|
5d763c18c8 | ||
|
|
bd3920cfff | ||
|
|
06d94332b6 | ||
|
|
50614484d8 | ||
|
|
c29d3d8c92 | ||
|
|
26f46af375 | ||
|
|
32b1491dd0 | ||
|
|
51b8a6c80a | ||
|
|
0f63d6d3a0 | ||
|
|
4771b08773 | ||
|
|
9b880101fd | ||
|
|
594806d6e8 | ||
|
|
e9afd4db2f | ||
|
|
b23efe4089 | ||
|
|
e33be41a93 | ||
|
|
33b09df872 | ||
|
|
e9050d0aa0 | ||
|
|
baeb2a33fe | ||
|
|
4ad89acdc7 | ||
|
|
7d87af8f5c | ||
|
|
65c0e84e2a | ||
|
|
7b15d85871 | ||
|
|
ad8ec0f4fd | ||
|
|
2d05d83dd0 | ||
|
|
bd45066b13 | ||
|
|
8ee4274054 | ||
|
|
83a7ed4d6b | ||
|
|
07dbd86ac6 | ||
|
|
0e671d2cc0 | ||
|
|
2d6d3c04ce | ||
|
|
b0148963c7 | ||
|
|
13356950f3 | ||
|
|
629bcb30a7 | ||
|
|
03721fff1c | ||
|
|
2a6911ae3d | ||
|
|
164eddecab | ||
|
|
9eacb38eb9 | ||
|
|
20f5cfb9a7 | ||
|
|
6c6c1cc90a | ||
|
|
a32c099cc1 | ||
|
|
fe2f832e83 | ||
|
|
868746cc23 | ||
|
|
3be7a54284 | ||
|
|
635e1ec8e2 | ||
|
|
a638a35a76 | ||
|
|
8cc33d3418 | ||
|
|
9947f7b967 | ||
|
|
daf5350f41 | ||
|
|
020b9ddb8d | ||
|
|
23aff9497a | ||
|
|
3c119396f3 | ||
|
|
f7c7c47ac0 | ||
|
|
dbe2369bbe | ||
|
|
4e8033d221 | ||
|
|
97a0f87cbd | ||
|
|
bfa2713d43 | ||
|
|
fe5e109751 | ||
|
|
8cc96030b1 | ||
|
|
a2b172ad58 | ||
|
|
e756225d8b | ||
|
|
dd803b604f | ||
|
|
b5c961c8ee | ||
|
|
47cd9d227e | ||
|
|
e2be3aafcd | ||
|
|
015fe76c44 | ||
|
|
44666aec03 | ||
|
|
6a265e4f35 | ||
|
|
12c7316524 | ||
|
|
dcf9741d69 | ||
|
|
63dd1fdd50 | ||
|
|
5aa166bbfd | ||
|
|
34cbf7093e | ||
|
|
159d58949e | ||
|
|
fcf802b7e3 | ||
|
|
92ff6dadb0 | ||
|
|
05fa2f9883 | ||
|
|
71bb8fd784 | ||
|
|
16ffd6dfab | ||
|
|
2661d15910 | ||
|
|
394102bb93 | ||
|
|
3585b12dfd | ||
|
|
423d87d5f1 | ||
|
|
13b13b1104 | ||
|
|
a77e7b96b7 | ||
|
|
d7213c255c | ||
|
|
ddeb1dcdb7 | ||
|
|
221cfa3528 | ||
|
|
d6f6348ff1 | ||
|
|
0c6afdc98e | ||
|
|
02a2148b3f | ||
|
|
36a02268d8 | ||
|
|
450f07f505 | ||
|
|
777eba9fed | ||
|
|
eaa8fa57d1 | ||
|
|
200bf479e1 | ||
|
|
331f409af9 | ||
|
|
ce875a5e63 | ||
|
|
638013f835 | ||
|
|
1de87cbfec | ||
|
|
7f3428b36a | ||
|
|
35595ded47 | ||
|
|
35e9264017 | ||
|
|
02d33c8f83 | ||
|
|
f229ebc3a8 | ||
|
|
0062351f6d | ||
|
|
e86f6798ec | ||
|
|
4f53f7136b | ||
|
|
d80b982dde | ||
|
|
24788aa9af | ||
|
|
9ffae658df | ||
|
|
82ad573cac | ||
|
|
36bf7ad65b | ||
|
|
b30af128c7 | ||
|
|
72c31ae097 | ||
|
|
d2c608021d | ||
|
|
1f36fb2413 | ||
|
|
16a0cbcecf | ||
|
|
e068e246aa | ||
|
|
ec7c77fcf9 | ||
|
|
46a338b874 | ||
|
|
bfee7ff09d | ||
|
|
ce1305d8ae | ||
|
|
aaebf88438 | ||
|
|
dde2c99e36 | ||
|
|
4dc2f3b9b9 | ||
|
|
f30cfffb86 | ||
|
|
ca3eb62ba7 | ||
|
|
c8e55ca4ce | ||
|
|
e4acb25a40 | ||
|
|
c741e10139 | ||
|
|
28d0b35f8e | ||
|
|
f7f09cd9e5 | ||
|
|
501c92c350 | ||
|
|
f021101322 | ||
|
|
369265bc2c | ||
|
|
b1f1e5db1f | ||
|
|
51d32e5afb | ||
|
|
f396e8e482 | ||
|
|
077321731e | ||
|
|
60eb0c6978 | ||
|
|
475f0af78a | ||
|
|
206fa07035 | ||
|
|
aff949714c | ||
|
|
7e834b9ff6 | ||
|
|
19bad26a98 | ||
|
|
7cc7c8d27b | ||
|
|
ae5a8c7cfa | ||
|
|
5004b73210 | ||
|
|
02f613d269 | ||
|
|
439ac0310b | ||
|
|
3e95467819 | ||
|
|
90522cb88b | ||
|
|
af39b01d4a | ||
|
|
73a0a5ff0b | ||
|
|
e157f500bc | ||
|
|
274ee5ed5f | ||
|
|
4cb11ba8c0 | ||
|
|
7b8e775139 | ||
|
|
86a7d26bfd | ||
|
|
84a437772d | ||
|
|
d7c95e2ae0 | ||
|
|
b4f0ef8b43 | ||
|
|
6d30cd7ae4 | ||
|
|
f631236ee7 | ||
|
|
1a58ff5c4c | ||
|
|
73aca913a1 | ||
|
|
24dee0cad6 | ||
|
|
2d2de75372 | ||
|
|
d98982e6fd | ||
|
|
14c12ffb08 | ||
|
|
f260afca11 | ||
|
|
5bcbe25d97 | ||
|
|
2eee366fbd | ||
|
|
85d57ec5e6 | ||
|
|
502c878f82 | ||
|
|
1136c3f767 | ||
|
|
42b496cd77 | ||
|
|
4acb5ee020 | ||
|
|
ea18781cc6 | ||
|
|
593617c0ff | ||
|
|
c6a139d88d | ||
|
|
b7357a4546 | ||
|
|
5eac959d15 | ||
|
|
74c86ecfbe | ||
|
|
f353e590e1 | ||
|
|
a4cc3e10c2 | ||
|
|
7321f56ee2 | ||
|
|
8800d9adc6 | ||
|
|
22cd535527 | ||
|
|
1d0e9592df | ||
|
|
2ef4af0ff2 | ||
|
|
542a6b1592 | ||
|
|
613ef94dba | ||
|
|
1dc2a1fadf | ||
|
|
41a606f5c1 | ||
|
|
7b2b9855f9 | ||
|
|
b2b519ba2e | ||
|
|
5cf89392ff | ||
|
|
0f05304ec3 | ||
|
|
87bc962c88 | ||
|
|
546ce6e229 | ||
|
|
2163d5aaf6 | ||
|
|
905ea160f2 | ||
|
|
675f4a372b | ||
|
|
7ff42db0c6 | ||
|
|
a01283a446 | ||
|
|
fefa261e7d | ||
|
|
0447b22dd2 | ||
|
|
cf125c1b48 | ||
|
|
81a9d8257c | ||
|
|
ee3f471300 | ||
|
|
5c2e5f626d | ||
|
|
a0f4b62361 | ||
|
|
786166b448 | ||
|
|
66e198b4ef | ||
|
|
4919240242 | ||
|
|
d7e6a41e3f | ||
|
|
202ef737dd | ||
|
|
04993224dc | ||
|
|
bebe7c28f8 | ||
|
|
639991dde4 | ||
|
|
31404cb89a | ||
|
|
f6205ca1dd | ||
|
|
6e86fc0593 | ||
|
|
f39a9845a3 | ||
|
|
ba17582945 | ||
|
|
02a1cbd467 | ||
|
|
2cd102ef0b | ||
|
|
240361b55b | ||
|
|
9beabc93cd | ||
|
|
8f4c6b911a | ||
|
|
083ef3010d | ||
|
|
e6c2253219 | ||
|
|
d802eb3f28 | ||
|
|
a342d5d5ad | ||
|
|
99adb77fcb | ||
|
|
2ea4eae9d6 | ||
|
|
9b079b2c3a | ||
|
|
8648e8569e | ||
|
|
1be0ab8bcb | ||
|
|
718f76c1f2 | ||
|
|
155d1f4c06 | ||
|
|
cb79e27d5a | ||
|
|
26991f8dd8 | ||
|
|
2375330d76 | ||
|
|
94e9b6d99b | ||
|
|
b516d24101 | ||
|
|
1b131d9371 | ||
|
|
3f45ef192d | ||
|
|
c6684af521 | ||
|
|
52f12b81ff | ||
|
|
6630f787bf | ||
|
|
2d7b2da3e2 | ||
|
|
d3b008fcd9 | ||
|
|
8a62fd0e6a | ||
|
|
b044860f05 | ||
|
|
1c5786dfb6 | ||
|
|
6bc9e3d695 | ||
|
|
b74fe59f12 | ||
|
|
6b57aa7f14 | ||
|
|
227125b35c | ||
|
|
c4012d8dfc | ||
|
|
cf3fa9ffbc | ||
|
|
40640d029a | ||
|
|
01eb7038dc | ||
|
|
58115bfd11 | ||
|
|
f1ea5031fb | ||
|
|
26d15a9fb3 | ||
|
|
54ba8e6047 | ||
|
|
eca063ab75 | ||
|
|
8892f4144e | ||
|
|
d2c25f9d6c | ||
|
|
b57457dc2f | ||
|
|
2861b0cfa2 | ||
|
|
0c45dbb884 | ||
|
|
a9f9261fb7 | ||
|
|
7e5f54a4f1 | ||
|
|
1228e8759c | ||
|
|
1daf771218 | ||
|
|
880cb08c3d | ||
|
|
e2cbae3089 | ||
|
|
42dcc3318c | ||
|
|
b32a85ae7e | ||
|
|
af85edddca | ||
|
|
eccd88e3c2 | ||
|
|
e0e11629a1 | ||
|
|
968095c183 | ||
|
|
d32b5115c5 | ||
|
|
d3001ec887 | ||
|
|
fef6a52008 | ||
|
|
048e6affbc | ||
|
|
c653d49691 | ||
|
|
6f5c9bd054 | ||
|
|
9e5576244d | ||
|
|
ef91317232 | ||
|
|
10c44d050f | ||
|
|
1845ea7170 | ||
|
|
d453294622 | ||
|
|
d11f9e4971 | ||
|
|
08272dd04e | ||
|
|
42441b9b42 | ||
|
|
e4a293c046 | ||
|
|
0cc5a39d63 | ||
|
|
942ea3f125 | ||
|
|
a8a70bb71c | ||
|
|
0d7c3fb4b2 | ||
|
|
77c682454e | ||
|
|
dd3473f5d8 | ||
|
|
cceadc5e04 | ||
|
|
e48c3a3f9c | ||
|
|
14981ef077 | ||
|
|
a7858d44bd | ||
|
|
9ae5f27c04 | ||
|
|
d691129842 | ||
|
|
e26d551263 | ||
|
|
277c6a444f | ||
|
|
f344800fd6 | ||
|
|
39a6fba33f | ||
|
|
8e11657bd2 | ||
|
|
dfbeaa4edf | ||
|
|
e90dc3b7f4 | ||
|
|
dba89e611a | ||
|
|
1a3fecc89e | ||
|
|
407e6a3d06 | ||
|
|
6ee19d5359 | ||
|
|
2df424dbac | ||
|
|
9874be6bf1 | ||
|
|
a3d4199d1d | ||
|
|
247fa146a9 | ||
|
|
f2b2c02cd6 | ||
|
|
a915f27f00 | ||
|
|
e14a488934 | ||
|
|
e82a8d9bc3 | ||
|
|
4527a0d12b | ||
|
|
01be202484 | ||
|
|
d1fe99edc3 | ||
|
|
fa629591e9 | ||
|
|
4ab3edc97b | ||
|
|
f1bfc6bf55 | ||
|
|
3283843ef3 | ||
|
|
4cb14ec8cc | ||
|
|
41535a68be | ||
|
|
d62447a12a | ||
|
|
802367c914 | ||
|
|
ff9b2c6ee8 | ||
|
|
a0e25c941a | ||
|
|
091c99e784 | ||
|
|
e264bb407b | ||
|
|
16625210fc | ||
|
|
2550453ee4 | ||
|
|
d1c480f23f | ||
|
|
b4b0397589 | ||
|
|
ab6b34e84c | ||
|
|
87af9d5078 | ||
|
|
95fab7c395 | ||
|
|
90825925ff | ||
|
|
7036cf9e49 | ||
|
|
53123eb0ee | ||
|
|
3c5407dd51 | ||
|
|
1ffe81f740 | ||
|
|
6bb35d61e1 | ||
|
|
f36ccf7bdc | ||
|
|
4632e68a00 | ||
|
|
09858d0783 | ||
|
|
9d1423c41b | ||
|
|
1a4b7786dd | ||
|
|
77c0a21ad0 | ||
|
|
7cedf14121 | ||
|
|
235346f3dd | ||
|
|
34c36b7c3a | ||
|
|
3e0f788ec3 | ||
|
|
867bb8a072 | ||
|
|
31a400158a | ||
|
|
8106ff6489 | ||
|
|
de3508993c | ||
|
|
fd3e7a6f8a | ||
|
|
4cf97a6054 | ||
|
|
75036e3ec7 |
420
.cursor/rules/dev-rules.mdc
Normal file
420
.cursor/rules/dev-rules.mdc
Normal file
@@ -0,0 +1,420 @@
|
|||||||
|
---
|
||||||
|
alwaysApply: true
|
||||||
|
---
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
- next.js react tailwind frontend `/web`
|
||||||
|
- broken down into pages, components, hooks, lib
|
||||||
|
- express node api server `/backend/api`
|
||||||
|
- one off scripts, like migrations `/backend/scripts`
|
||||||
|
- supabase postgres. schema in `/backend/supabase`
|
||||||
|
- supabase-generated types in `/backend/supabase/schema.ts`
|
||||||
|
- files shared between backend directories `/backend/shared`
|
||||||
|
- anything in `/backend` can import from `shared`, but not vice versa
|
||||||
|
- files shared between the frontend and backend in `/common`
|
||||||
|
- `/common` has lots of type definitions for our data structures, like User. It also contains many useful utility functions. We try not to add package dependencies to common. `/web` and `/backend` are allowed to import from `/common`, but not vice versa.
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
- The project has both dev and prod environments.
|
||||||
|
- Backend is on GCP (Google Cloud Platform). Deployment handled by terraform.
|
||||||
|
- Project ID is `compass-130ba`.
|
||||||
|
|
||||||
|
## Code Guidelines
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Here's an example component from web in our style:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import clsx from 'clsx'
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
import { isAdminId, isModId } from 'common/envs/constants'
|
||||||
|
import { type Headline } from 'common/news'
|
||||||
|
import { EditNewsButton } from 'web/components/news/edit-news-button'
|
||||||
|
import { Carousel } from 'web/components/widgets/carousel'
|
||||||
|
import { useUser } from 'web/hooks/use-user'
|
||||||
|
import { track } from 'web/lib/service/analytics'
|
||||||
|
import { DashboardEndpoints } from 'web/components/dashboard/dashboard-page'
|
||||||
|
import { removeEmojis } from 'common/util/string'
|
||||||
|
|
||||||
|
export function HeadlineTabs(props: {
|
||||||
|
headlines: Headline[]
|
||||||
|
currentSlug: string
|
||||||
|
endpoint: DashboardEndpoints
|
||||||
|
hideEmoji?: boolean
|
||||||
|
notSticky?: boolean
|
||||||
|
className?: string
|
||||||
|
}) {
|
||||||
|
const { headlines, endpoint, currentSlug, hideEmoji, notSticky, className } =
|
||||||
|
props
|
||||||
|
const user = useUser()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
className,
|
||||||
|
'bg-canvas-50 w-full',
|
||||||
|
!notSticky && 'sticky top-0 z-50'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Carousel labelsParentClassName="gap-px">
|
||||||
|
{headlines.map(({ id, slug, title }) => (
|
||||||
|
<Tab
|
||||||
|
key={id}
|
||||||
|
label={hideEmoji ? removeEmojis(title) : title}
|
||||||
|
href={`/${endpoint}/${slug}`}
|
||||||
|
active={slug === currentSlug}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{user && <Tab label="More" href="/dashboard" />}
|
||||||
|
{user && (isAdminId(user.id) || isModId(user.id)) && (
|
||||||
|
<EditNewsButton endpoint={endpoint} defaultDashboards={headlines} />
|
||||||
|
)}
|
||||||
|
</Carousel>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
We prefer to have many smaller components that each represent one logical unit, rather than one very large component that does everything. Then we compose and reuse the components.
|
||||||
|
|
||||||
|
It's best to export the main component at the top of the file. We also try to name the component the same as the file name (headline-tabs.tsx) so that it's easy to find.
|
||||||
|
|
||||||
|
Here's another example in `home.tsx` that calls our api. We have an endpoint called 'headlines', which is being cached by NextJS:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { api } from 'web/lib/api/api'
|
||||||
|
// More imports...
|
||||||
|
|
||||||
|
export async function getStaticProps() {
|
||||||
|
try {
|
||||||
|
const headlines = await api('headlines', {})
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
headlines,
|
||||||
|
revalidate: 30 * 60, // 30 minutes
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
return { props: { headlines: [] }, revalidate: 60 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Home(props: { headlines: Headline[] }) { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
If we are calling the API on the client, prefer using the `useAPIGetter` hook:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export const YourTopicsSection = (props: {
|
||||||
|
user: User
|
||||||
|
className?: string
|
||||||
|
}) => {
|
||||||
|
const { user, className } = props
|
||||||
|
const { data, refresh } = useAPIGetter('get-followed-groups', {
|
||||||
|
userId: user.id,
|
||||||
|
})
|
||||||
|
const followedGroups = data?.groups ?? []
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
This stores the result in memory, and allows you to call refresh() to get an updated version.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
We frequently use `usePersistentInMemoryState` or `usePersistentLocalState` as an alternative to `useState`. These cache data. Most of the time you want in-memory caching so that navigating back to a page will preserve the same state and appear to load instantly.
|
||||||
|
|
||||||
|
Here's the definition of usePersistentInMemoryState:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export const usePersistentInMemoryState = <T>(initialValue: T, key: string) => {
|
||||||
|
const [state, setState] = useStateCheckEquality<T>(
|
||||||
|
safeJsonParse(store[key]) ?? initialValue
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const storedValue = safeJsonParse(store[key]) ?? initialValue
|
||||||
|
setState(storedValue as T)
|
||||||
|
}, [key])
|
||||||
|
|
||||||
|
const saveState = useEvent((newState: T | ((prevState: T) => T)) => {
|
||||||
|
setState((prevState) => {
|
||||||
|
const updatedState = isFunction(newState) ? newState(prevState) : newState
|
||||||
|
store[key] = JSON.stringify(updatedState)
|
||||||
|
return updatedState
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return [state, saveState] as const
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
For live updates, we use websockets. In `use-api-subscription.ts`, we have this hook:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export function useApiSubscription(opts: SubscriptionOptions) {
|
||||||
|
useEffect(() => {
|
||||||
|
const ws = client
|
||||||
|
if (ws != null) {
|
||||||
|
if (opts.enabled ?? true) {
|
||||||
|
ws.subscribe(opts.topics, opts.onBroadcast).catch(opts.onError)
|
||||||
|
return () => {
|
||||||
|
ws.unsubscribe(opts.topics, opts.onBroadcast).catch(opts.onError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [opts.enabled, JSON.stringify(opts.topics)])
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In `use-bets`, we have this hook to get live updates with useApiSubscription:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export const useContractBets = (
|
||||||
|
contractId: string,
|
||||||
|
opts?: APIParams<'bets'> & { enabled?: boolean }
|
||||||
|
) => {
|
||||||
|
const { enabled = true, ...apiOptions } = {
|
||||||
|
contractId,
|
||||||
|
...opts,
|
||||||
|
}
|
||||||
|
const optionsKey = JSON.stringify(apiOptions)
|
||||||
|
|
||||||
|
const [newBets, setNewBets] = usePersistentInMemoryState<Bet[]>(
|
||||||
|
[],
|
||||||
|
`${optionsKey}-bets`
|
||||||
|
)
|
||||||
|
|
||||||
|
const addBets = (bets: Bet[]) => {
|
||||||
|
setNewBets((currentBets) => {
|
||||||
|
const uniqueBets = sortBy(
|
||||||
|
uniqBy([...currentBets, ...bets], 'id'),
|
||||||
|
'createdTime'
|
||||||
|
)
|
||||||
|
return uniqueBets.filter((b) => !betShouldBeFiltered(b, apiOptions))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPageVisible = useIsPageVisible()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isPageVisible && enabled) {
|
||||||
|
api('bets', apiOptions).then(addBets)
|
||||||
|
}
|
||||||
|
}, [optionsKey, enabled, isPageVisible])
|
||||||
|
|
||||||
|
useApiSubscription({
|
||||||
|
topics: [`contract/${contractId}/new-bet`],
|
||||||
|
onBroadcast: (msg) => {
|
||||||
|
addBets(msg.data.bets as Bet[])
|
||||||
|
},
|
||||||
|
enabled,
|
||||||
|
})
|
||||||
|
|
||||||
|
return newBets
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Here are all the topics we broadcast, from `backend/shared/src/websockets/helpers.ts`
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export function broadcastUpdatedPrivateUser(userId: string) {
|
||||||
|
// don't send private user info because it's private and anyone can listen
|
||||||
|
broadcast(`private-user/${userId}`, {})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function broadcastUpdatedUser(user: Partial<User> & { id: string }) {
|
||||||
|
broadcast(`user/${user.id}`, { user })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function broadcastUpdatedComment(comment: Comment) {
|
||||||
|
broadcast(`user/${comment.onUserId}/comment`, { comment })
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
We have our scripts in the directory `/backend/scripts`.
|
||||||
|
|
||||||
|
To write a script, run it inside the helper function called `runScript` that automatically fetches any secret keys and loads them into process.env.
|
||||||
|
|
||||||
|
Example from `/backend/scripts/manicode.ts`
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { runScript } from 'run-script'
|
||||||
|
|
||||||
|
runScript(async ({ pg }) => {
|
||||||
|
const userPrompt = process.argv[2]
|
||||||
|
await pg.none(...)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Generally scripts should be run by me, especially if they modify backend state or schema.
|
||||||
|
But if you need to run a script, you can use `bun`. For example:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
bun run manicode.ts "Generate a page called cowp, which has cows that make noises!"
|
||||||
|
```
|
||||||
|
|
||||||
|
if that doesn't work, try
|
||||||
|
|
||||||
|
```sh
|
||||||
|
bun x ts-node manicode.ts "Generate a page called cowp, which has cows that make noises!"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Our backend is mostly a set of endpoints. We create new endpoints by adding to the schema in `/common/src/api/schema.ts`.
|
||||||
|
|
||||||
|
E.g. Here is a hypothetical bet schema:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
bet: {
|
||||||
|
method: 'POST',
|
||||||
|
authed: true,
|
||||||
|
returns: {} as CandidateBet & { betId: string },
|
||||||
|
props: z
|
||||||
|
.object({
|
||||||
|
contractId: z.string(),
|
||||||
|
amount: z.number().gte(1),
|
||||||
|
replyToCommentId: z.string().optional(),
|
||||||
|
limitProb: z.number().gte(0.01).lte(0.99).optional(),
|
||||||
|
expiresAt: z.number().optional(),
|
||||||
|
// Used for binary and new multiple choice contracts (cpmm-multi-1).
|
||||||
|
outcome: z.enum(['YES', 'NO']).default('YES'),
|
||||||
|
//Multi
|
||||||
|
answerId: z.string().optional(),
|
||||||
|
dryRun: z.boolean().optional(),
|
||||||
|
})
|
||||||
|
.strict(),
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, we define the bet endpoint in `backend/api/src/place-bet.ts`
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export const placeBet: APIHandler<'bet'> = async (props, auth) => {
|
||||||
|
const isApi = auth.creds.kind === 'key'
|
||||||
|
return await betsQueue.enqueueFn(
|
||||||
|
() => placeBetMain(props, auth.uid, isApi),
|
||||||
|
[props.contractId, auth.uid]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
And finally, you need to register the handler in `backend/api/src/routes.ts`
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { placeBet } from './place-bet'
|
||||||
|
...
|
||||||
|
|
||||||
|
const handlers = {
|
||||||
|
bet: placeBet,
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
We have two ways to access our postgres database.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { db } from 'web/lib/supabase/db'
|
||||||
|
|
||||||
|
db.from('profiles').select('*').eq('user_id', userId)
|
||||||
|
```
|
||||||
|
|
||||||
|
and
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { createSupabaseDirectClient } from 'shared/supabase/init'
|
||||||
|
|
||||||
|
const pg = createSupabaseDirectClient()
|
||||||
|
pg.oneOrNone<Row<'profiles'>>('select * from profiles where user_id = $1', [userId])
|
||||||
|
```
|
||||||
|
|
||||||
|
The supabase client just uses the supabase client library, which is a wrapper around postgREST. It allows us to query and update the database directly from the frontend.
|
||||||
|
|
||||||
|
`createSupabaseDirectClient` is used on the backend. it lets us specify sql strings to run directly on our database, using the pg-promise library. The client (code in web) does not have permission to do this.
|
||||||
|
|
||||||
|
Another example using the direct client:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export const getUniqueBettorIds = async (
|
||||||
|
contractId: string,
|
||||||
|
pg: SupabaseDirectClient
|
||||||
|
) => {
|
||||||
|
const res = await pg.manyOrNone(
|
||||||
|
'select distinct user_id from contract_bets where contract_id = $1',
|
||||||
|
[contractId]
|
||||||
|
)
|
||||||
|
return res.map((r) => r.user_id as string)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
(you may notice we write sql in lowercase)
|
||||||
|
|
||||||
|
We have a few helper functions for updating and inserting data into the database.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import {
|
||||||
|
buikInsert,
|
||||||
|
bulkUpdate,
|
||||||
|
bulkUpdateData,
|
||||||
|
bulkUpsert,
|
||||||
|
insert,
|
||||||
|
update,
|
||||||
|
updateData,
|
||||||
|
} from 'shared/supabase/utils'
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
const pg = createSupabaseDirectClient()
|
||||||
|
|
||||||
|
// you are encouraged to use tryCatch for these
|
||||||
|
const { data, error } = await tryCatch(
|
||||||
|
insert(pg, 'profiles', { user_id: auth.uid, ...body })
|
||||||
|
)
|
||||||
|
|
||||||
|
if (error) throw APIError(500, 'Error creating profile: ' + error.message)
|
||||||
|
|
||||||
|
await update(pg, 'profiles', 'user_id', { user_id: auth.uid, age: 99 })
|
||||||
|
|
||||||
|
await updateData(pg, 'private_users', { id: userId, notifications: { ... } })
|
||||||
|
```
|
||||||
|
|
||||||
|
The sqlBuilder from `shared/supabase/sql-builder.ts` can be used to construct SQL queries with re-useable parts. All it does is sanitize and output sql query strings. It has several helper functions including:
|
||||||
|
|
||||||
|
- `select`: Specifies the columns to select
|
||||||
|
- `from`: Specifies the table to query
|
||||||
|
- `where`: Adds WHERE clauses
|
||||||
|
- `orderBy`: Specifies the order of results
|
||||||
|
- `limit`: Limits the number of results
|
||||||
|
- `renderSql`: Combines all parts into a final SQL string
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const query = renderSql(
|
||||||
|
select('distinct user_id'),
|
||||||
|
from('contract_bets'),
|
||||||
|
where('contract_id = ${id}', { id }),
|
||||||
|
orderBy('created_time desc'),
|
||||||
|
limitValue != null && limit(limitValue)
|
||||||
|
)
|
||||||
|
|
||||||
|
const res = await pg.manyOrNone(query)
|
||||||
|
```
|
||||||
|
|
||||||
|
Use these functions instead of string concatenation.
|
||||||
22
.env.example
22
.env.example
@@ -1,17 +1,7 @@
|
|||||||
# Rename this file to `.env` and fill in the values.
|
# You already have access to basic local functionality (UI, authentication, database read access).
|
||||||
|
|
||||||
# Required variables for basic local functionality
|
# openssl enc -aes-256-cbc -salt -pbkdf2 -iter 100000 -in backend/shared/src/googleApplicationCredentials-dev.json -out secrets/googleApplicationCredentials-dev.json.enc
|
||||||
|
GOOGLE_CREDENTIALS_ENC_PWD=nP7s3274uzOG4c2t
|
||||||
# For database connection. A 16-character password with digits and letters.
|
|
||||||
SUPABASE_DB_PASSWORD=
|
|
||||||
|
|
||||||
# For authentication.
|
|
||||||
# Ask the project admin. Should start with "AIza".
|
|
||||||
NEXT_PUBLIC_FIREBASE_API_KEY=
|
|
||||||
|
|
||||||
# The URL where your local backend server is running.
|
|
||||||
# You can change the port if needed.
|
|
||||||
NEXT_PUBLIC_API_URL=localhost:8088
|
|
||||||
|
|
||||||
|
|
||||||
# Optional variables for full local functionality
|
# Optional variables for full local functionality
|
||||||
@@ -20,10 +10,6 @@ NEXT_PUBLIC_API_URL=localhost:8088
|
|||||||
# Create a free account at https://rapidapi.com/wirefreethought/api/geodb-cities and get an API key.
|
# Create a free account at https://rapidapi.com/wirefreethought/api/geodb-cities and get an API key.
|
||||||
GEODB_API_KEY=
|
GEODB_API_KEY=
|
||||||
|
|
||||||
# For analytics like page views, user actions, feature usage, etc.
|
|
||||||
# Create a free account at https://posthog.com and get a project API key. Should start with "phc_".
|
|
||||||
POSTHOG_KEY=
|
|
||||||
|
|
||||||
# For sending emails (e.g. for user sign up, password reset, notifications, etc.).
|
# For sending emails (e.g. for user sign up, password reset, notifications, etc.).
|
||||||
# Create a free account at https://resend.com and get an API key. Should start with "re_".
|
# Create a free account at https://resend.com and get an API key. Should start with "re_".
|
||||||
RESEND_API_KEY=
|
RESEND_KEY=
|
||||||
|
|||||||
15
.github/FUNDING.yml
vendored
Normal file
15
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# These are supported funding model platforms
|
||||||
|
|
||||||
|
github: [CompassConnections] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||||
|
patreon: CompassMeet # Replace with a single Patreon username
|
||||||
|
open_collective: compass-connection # Replace with a single Open Collective username
|
||||||
|
ko_fi: compassconnections # Replace with a single Ko-fi username
|
||||||
|
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||||
|
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||||
|
liberapay: CompassConnections # Replace with a single Liberapay username
|
||||||
|
issuehunt: # Replace with a single IssueHunt username
|
||||||
|
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||||
|
polar: # Replace with a single Polar username
|
||||||
|
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
|
||||||
|
thanks_dev: # Replace with a single thanks.dev username
|
||||||
|
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
||||||
56
.github/workflows/ci.yml
vendored
56
.github/workflows/ci.yml
vendored
@@ -32,27 +32,63 @@ jobs:
|
|||||||
run: npm run lint
|
run: npm run lint
|
||||||
|
|
||||||
- name: Run Jest tests
|
- name: Run Jest tests
|
||||||
run: npm run test tests/jest
|
env:
|
||||||
|
NEXT_PUBLIC_FIREBASE_ENV: DEV
|
||||||
|
run: |
|
||||||
|
yarn test:coverage
|
||||||
|
# npm install -g lcov-result-merger
|
||||||
|
# mkdir coverage
|
||||||
|
# lcov-result-merger \
|
||||||
|
# "backend/api/coverage/lcov.info" \
|
||||||
|
# "backend/shared/coverage/lcov.info" \
|
||||||
|
# "backend/email/coverage/lcov.info" \
|
||||||
|
# "common/coverage/lcov.info" \
|
||||||
|
# "web/coverage/lcov.info" \
|
||||||
|
# > coverage/lcov.info
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# - name: Build app
|
# - name: Build app
|
||||||
# env:
|
# env:
|
||||||
# DATABASE_URL: ${{ secrets.DATABASE_URL }}
|
# DATABASE_URL: ${{ secrets.DATABASE_URL }}
|
||||||
# run: npm run build
|
# run: npm run build
|
||||||
|
|
||||||
# Optional: Playwright E2E tests
|
# Optional: Playwright E2E tests
|
||||||
- name: Install Playwright deps
|
- name: Install Playwright deps
|
||||||
run: npx playwright install --with-deps
|
run: |
|
||||||
# npm install @playwright/test
|
npx playwright install --with-deps
|
||||||
# npx playwright install
|
# npm install @playwright/test
|
||||||
|
|
||||||
- name: Run E2E tests
|
- name: Run E2E tests
|
||||||
|
env:
|
||||||
|
NEXT_PUBLIC_API_URL: localhost:8088
|
||||||
|
NEXT_PUBLIC_FIREBASE_ENV: DEV
|
||||||
|
NEXT_PUBLIC_FIREBASE_API_KEY: ${{ secrets.NEXT_PUBLIC_FIREBASE_API_KEY }}
|
||||||
|
NEXT_PUBLIC_SUPABASE_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_KEY }}
|
||||||
run: |
|
run: |
|
||||||
NEXT_PUBLIC_API_URL=localhost:8088 \
|
npx nyc --reporter=lcov yarn --cwd=web serve &
|
||||||
NEXT_PUBLIC_FIREBASE_ENV=PROD \
|
npx nyc --reporter=lcov yarn --cwd=backend/api dev &
|
||||||
NEXT_PUBLIC_FIREBASE_API_KEY=${{ secrets.NEXT_PUBLIC_FIREBASE_API_KEY }} \
|
|
||||||
yarn --cwd=web serve &
|
|
||||||
npx wait-on http://localhost:3000
|
npx wait-on http://localhost:3000
|
||||||
npx playwright test tests/playwright
|
npx playwright test tests/e2e
|
||||||
SERVER_PID=$(fuser -k 3000/tcp)
|
SERVER_PID=$(fuser -k 3000/tcp)
|
||||||
echo $SERVER_PID
|
echo $SERVER_PID
|
||||||
kill $SERVER_PID
|
kill $SERVER_PID
|
||||||
|
SERVER_PID=$(fuser -k 8088/tcp)
|
||||||
|
echo $SERVER_PID
|
||||||
|
kill $SERVER_PID
|
||||||
|
|
||||||
|
- name: Upload coverage to Codecov
|
||||||
|
uses: codecov/codecov-action@v5
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
files: |
|
||||||
|
backend/api/coverage/lcov.info
|
||||||
|
backend/shared/coverage/lcov.info
|
||||||
|
backend/email/coverage/lcov.info
|
||||||
|
common/coverage/lcov.info
|
||||||
|
web/coverage/lcov.info
|
||||||
|
flags: unit
|
||||||
|
fail_ci_if_error: true
|
||||||
|
slug: CompassConnections/Compass
|
||||||
|
env:
|
||||||
|
CI: true
|
||||||
|
|||||||
42
.gitignore
vendored
42
.gitignore
vendored
@@ -13,6 +13,10 @@
|
|||||||
# testing
|
# testing
|
||||||
/coverage
|
/coverage
|
||||||
|
|
||||||
|
# Playwright
|
||||||
|
/tests/reports/playwright-report
|
||||||
|
/tests/e2e/web/.auth/
|
||||||
|
|
||||||
# next.js
|
# next.js
|
||||||
/.next/
|
/.next/
|
||||||
/out/
|
/out/
|
||||||
@@ -55,9 +59,45 @@ tsconfig.tsbuildinfo
|
|||||||
|
|
||||||
*prisma/migrations
|
*prisma/migrations
|
||||||
martin
|
martin
|
||||||
|
email-preview
|
||||||
.obsidian
|
.obsidian
|
||||||
.idea
|
.idea
|
||||||
*.last-run.json
|
*.last-run.json
|
||||||
|
|
||||||
*lock.hcl
|
*lock.hcl
|
||||||
/web/pages/test.tsx
|
/web/pages/_test.tsx
|
||||||
|
|
||||||
|
*.png
|
||||||
|
*.jpg
|
||||||
|
*.jpeg
|
||||||
|
*.gif
|
||||||
|
*.svg
|
||||||
|
*.ico
|
||||||
|
*.mp4
|
||||||
|
*.mov
|
||||||
|
*.webp
|
||||||
|
*.avi
|
||||||
|
*.wmv
|
||||||
|
*.mp3
|
||||||
|
*.wav
|
||||||
|
*.flac
|
||||||
|
*.aac
|
||||||
|
*.zip
|
||||||
|
*.tar.gz
|
||||||
|
*.rar
|
||||||
|
/favicon_color.ico
|
||||||
|
/backend/shared/src/googleApplicationCredentials-dev.json
|
||||||
|
*.tfstate
|
||||||
|
*.tfstate.backup
|
||||||
|
*.terraform
|
||||||
|
/backups/firebase/auth/data/
|
||||||
|
/backups/firebase/storage/data/
|
||||||
|
|
||||||
|
android/app/release*
|
||||||
|
icons/
|
||||||
|
*.bak
|
||||||
|
|
||||||
|
test-results
|
||||||
|
/.nyc_output/
|
||||||
|
|
||||||
|
**/coverage
|
||||||
|
|||||||
422
.windsurf/rules/compass.md
Normal file
422
.windsurf/rules/compass.md
Normal file
@@ -0,0 +1,422 @@
|
|||||||
|
---
|
||||||
|
trigger: always_on
|
||||||
|
description:
|
||||||
|
globs:
|
||||||
|
---
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
- next.js react tailwind frontend `/web`
|
||||||
|
- broken down into pages, components, hooks, lib
|
||||||
|
- express node api server `/backend/api`
|
||||||
|
- one off scripts, like migrations `/backend/scripts`
|
||||||
|
- supabase postgres. schema in `/backend/supabase`
|
||||||
|
- supabase-generated types in `/backend/supabase/schema.ts`
|
||||||
|
- files shared between backend directories `/backend/shared`
|
||||||
|
- anything in `/backend` can import from `shared`, but not vice versa
|
||||||
|
- files shared between the frontend and backend in `/common`
|
||||||
|
- `/common` has lots of type definitions for our data structures, like User. It also contains many useful utility functions. We try not to add package dependencies to common. `/web` and `/backend` are allowed to import from `/common`, but not vice versa.
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
- The project has both dev and prod environments.
|
||||||
|
- Backend is on GCP (Google Cloud Platform). Deployment handled by terraform.
|
||||||
|
- Project ID is `compass-130ba`.
|
||||||
|
|
||||||
|
## Code Guidelines
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Here's an example component from web in our style:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import clsx from 'clsx'
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
import { isAdminId, isModId } from 'common/envs/constants'
|
||||||
|
import { type Headline } from 'common/news'
|
||||||
|
import { EditNewsButton } from 'web/components/news/edit-news-button'
|
||||||
|
import { Carousel } from 'web/components/widgets/carousel'
|
||||||
|
import { useUser } from 'web/hooks/use-user'
|
||||||
|
import { track } from 'web/lib/service/analytics'
|
||||||
|
import { DashboardEndpoints } from 'web/components/dashboard/dashboard-page'
|
||||||
|
import { removeEmojis } from 'common/util/string'
|
||||||
|
|
||||||
|
export function HeadlineTabs(props: {
|
||||||
|
headlines: Headline[]
|
||||||
|
currentSlug: string
|
||||||
|
endpoint: DashboardEndpoints
|
||||||
|
hideEmoji?: boolean
|
||||||
|
notSticky?: boolean
|
||||||
|
className?: string
|
||||||
|
}) {
|
||||||
|
const { headlines, endpoint, currentSlug, hideEmoji, notSticky, className } =
|
||||||
|
props
|
||||||
|
const user = useUser()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
className,
|
||||||
|
'bg-canvas-50 w-full',
|
||||||
|
!notSticky && 'sticky top-0 z-50'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Carousel labelsParentClassName="gap-px">
|
||||||
|
{headlines.map(({ id, slug, title }) => (
|
||||||
|
<Tab
|
||||||
|
key={id}
|
||||||
|
label={hideEmoji ? removeEmojis(title) : title}
|
||||||
|
href={`/${endpoint}/${slug}`}
|
||||||
|
active={slug === currentSlug}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
{user && <Tab label="More" href="/dashboard" />}
|
||||||
|
{user && (isAdminId(user.id) || isModId(user.id)) && (
|
||||||
|
<EditNewsButton endpoint={endpoint} defaultDashboards={headlines} />
|
||||||
|
)}
|
||||||
|
</Carousel>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
We prefer to have many smaller components that each represent one logical unit, rather than one very large component that does everything. Then we compose and reuse the components.
|
||||||
|
|
||||||
|
It's best to export the main component at the top of the file. We also try to name the component the same as the file name (headline-tabs.tsx) so that it's easy to find.
|
||||||
|
|
||||||
|
Here's another example in `home.tsx` that calls our api. We have an endpoint called 'headlines', which is being cached by NextJS:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { api } from 'web/lib/api/api'
|
||||||
|
// More imports...
|
||||||
|
|
||||||
|
export async function getStaticProps() {
|
||||||
|
try {
|
||||||
|
const headlines = await api('headlines', {})
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
headlines,
|
||||||
|
revalidate: 30 * 60, // 30 minutes
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
return { props: { headlines: [] }, revalidate: 60 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Home(props: { headlines: Headline[] }) { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
If we are calling the API on the client, prefer using the `useAPIGetter` hook:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export const YourTopicsSection = (props: {
|
||||||
|
user: User
|
||||||
|
className?: string
|
||||||
|
}) => {
|
||||||
|
const { user, className } = props
|
||||||
|
const { data, refresh } = useAPIGetter('get-followed-groups', {
|
||||||
|
userId: user.id,
|
||||||
|
})
|
||||||
|
const followedGroups = data?.groups ?? []
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
This stores the result in memory, and allows you to call refresh() to get an updated version.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
We frequently use `usePersistentInMemoryState` or `usePersistentLocalState` as an alternative to `useState`. These cache data. Most of the time you want in-memory caching so that navigating back to a page will preserve the same state and appear to load instantly.
|
||||||
|
|
||||||
|
Here's the definition of usePersistentInMemoryState:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export const usePersistentInMemoryState = <T>(initialValue: T, key: string) => {
|
||||||
|
const [state, setState] = useStateCheckEquality<T>(
|
||||||
|
safeJsonParse(store[key]) ?? initialValue
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const storedValue = safeJsonParse(store[key]) ?? initialValue
|
||||||
|
setState(storedValue as T)
|
||||||
|
}, [key])
|
||||||
|
|
||||||
|
const saveState = useEvent((newState: T | ((prevState: T) => T)) => {
|
||||||
|
setState((prevState) => {
|
||||||
|
const updatedState = isFunction(newState) ? newState(prevState) : newState
|
||||||
|
store[key] = JSON.stringify(updatedState)
|
||||||
|
return updatedState
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return [state, saveState] as const
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
For live updates, we use websockets. In `use-api-subscription.ts`, we have this hook:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export function useApiSubscription(opts: SubscriptionOptions) {
|
||||||
|
useEffect(() => {
|
||||||
|
const ws = client
|
||||||
|
if (ws != null) {
|
||||||
|
if (opts.enabled ?? true) {
|
||||||
|
ws.subscribe(opts.topics, opts.onBroadcast).catch(opts.onError)
|
||||||
|
return () => {
|
||||||
|
ws.unsubscribe(opts.topics, opts.onBroadcast).catch(opts.onError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [opts.enabled, JSON.stringify(opts.topics)])
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In `use-bets`, we have this hook to get live updates with useApiSubscription:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export const useContractBets = (
|
||||||
|
contractId: string,
|
||||||
|
opts?: APIParams<'bets'> & { enabled?: boolean }
|
||||||
|
) => {
|
||||||
|
const { enabled = true, ...apiOptions } = {
|
||||||
|
contractId,
|
||||||
|
...opts,
|
||||||
|
}
|
||||||
|
const optionsKey = JSON.stringify(apiOptions)
|
||||||
|
|
||||||
|
const [newBets, setNewBets] = usePersistentInMemoryState<Bet[]>(
|
||||||
|
[],
|
||||||
|
`${optionsKey}-bets`
|
||||||
|
)
|
||||||
|
|
||||||
|
const addBets = (bets: Bet[]) => {
|
||||||
|
setNewBets((currentBets) => {
|
||||||
|
const uniqueBets = sortBy(
|
||||||
|
uniqBy([...currentBets, ...bets], 'id'),
|
||||||
|
'createdTime'
|
||||||
|
)
|
||||||
|
return uniqueBets.filter((b) => !betShouldBeFiltered(b, apiOptions))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const isPageVisible = useIsPageVisible()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isPageVisible && enabled) {
|
||||||
|
api('bets', apiOptions).then(addBets)
|
||||||
|
}
|
||||||
|
}, [optionsKey, enabled, isPageVisible])
|
||||||
|
|
||||||
|
useApiSubscription({
|
||||||
|
topics: [`contract/${contractId}/new-bet`],
|
||||||
|
onBroadcast: (msg) => {
|
||||||
|
addBets(msg.data.bets as Bet[])
|
||||||
|
},
|
||||||
|
enabled,
|
||||||
|
})
|
||||||
|
|
||||||
|
return newBets
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Here are all the topics we broadcast, from `backend/shared/src/websockets/helpers.ts`
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export function broadcastUpdatedPrivateUser(userId: string) {
|
||||||
|
// don't send private user info because it's private and anyone can listen
|
||||||
|
broadcast(`private-user/${userId}`, {})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function broadcastUpdatedUser(user: Partial<User> & { id: string }) {
|
||||||
|
broadcast(`user/${user.id}`, { user })
|
||||||
|
}
|
||||||
|
|
||||||
|
export function broadcastUpdatedComment(comment: Comment) {
|
||||||
|
broadcast(`user/${comment.onUserId}/comment`, { comment })
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
We have our scripts in the directory `/backend/scripts`.
|
||||||
|
|
||||||
|
To write a script, run it inside the helper function called `runScript` that automatically fetches any secret keys and loads them into process.env.
|
||||||
|
|
||||||
|
Example from `/backend/scripts/manicode.ts`
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { runScript } from 'run-script'
|
||||||
|
|
||||||
|
runScript(async ({ pg }) => {
|
||||||
|
const userPrompt = process.argv[2]
|
||||||
|
await pg.none(...)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Generally scripts should be run by me, especially if they modify backend state or schema.
|
||||||
|
But if you need to run a script, you can use `bun`. For example:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
bun run manicode.ts "Generate a page called cowp, which has cows that make noises!"
|
||||||
|
```
|
||||||
|
|
||||||
|
if that doesn't work, try
|
||||||
|
|
||||||
|
```sh
|
||||||
|
bun x ts-node manicode.ts "Generate a page called cowp, which has cows that make noises!"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Our backend is mostly a set of endpoints. We create new endpoints by adding to the schema in `/common/src/api/schema.ts`.
|
||||||
|
|
||||||
|
E.g. Here is a hypothetical bet schema:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
bet: {
|
||||||
|
method: 'POST',
|
||||||
|
authed: true,
|
||||||
|
returns: {} as CandidateBet & { betId: string },
|
||||||
|
props: z
|
||||||
|
.object({
|
||||||
|
contractId: z.string(),
|
||||||
|
amount: z.number().gte(1),
|
||||||
|
replyToCommentId: z.string().optional(),
|
||||||
|
limitProb: z.number().gte(0.01).lte(0.99).optional(),
|
||||||
|
expiresAt: z.number().optional(),
|
||||||
|
// Used for binary and new multiple choice contracts (cpmm-multi-1).
|
||||||
|
outcome: z.enum(['YES', 'NO']).default('YES'),
|
||||||
|
//Multi
|
||||||
|
answerId: z.string().optional(),
|
||||||
|
dryRun: z.boolean().optional(),
|
||||||
|
})
|
||||||
|
.strict(),
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, we define the bet endpoint in `backend/api/src/place-bet.ts`
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export const placeBet: APIHandler<'bet'> = async (props, auth) => {
|
||||||
|
const isApi = auth.creds.kind === 'key'
|
||||||
|
return await betsQueue.enqueueFn(
|
||||||
|
() => placeBetMain(props, auth.uid, isApi),
|
||||||
|
[props.contractId, auth.uid]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
And finally, you need to register the handler in `backend/api/src/routes.ts`
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { placeBet } from './place-bet'
|
||||||
|
...
|
||||||
|
|
||||||
|
const handlers = {
|
||||||
|
bet: placeBet,
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
We have two ways to access our postgres database.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { db } from 'web/lib/supabase/db'
|
||||||
|
|
||||||
|
db.from('profiles').select('*').eq('user_id', userId)
|
||||||
|
```
|
||||||
|
|
||||||
|
and
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { createSupabaseDirectClient } from 'shared/supabase/init'
|
||||||
|
|
||||||
|
const pg = createSupabaseDirectClient()
|
||||||
|
pg.oneOrNone<Row<'profiles'>>('select * from profiles where user_id = $1', [userId])
|
||||||
|
```
|
||||||
|
|
||||||
|
The supabase client just uses the supabase client library, which is a wrapper around postgREST. It allows us to query and update the database directly from the frontend.
|
||||||
|
|
||||||
|
`createSupabaseDirectClient` is used on the backend. it lets us specify sql strings to run directly on our database, using the pg-promise library. The client (code in web) does not have permission to do this.
|
||||||
|
|
||||||
|
Another example using the direct client:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export const getUniqueBettorIds = async (
|
||||||
|
contractId: string,
|
||||||
|
pg: SupabaseDirectClient
|
||||||
|
) => {
|
||||||
|
const res = await pg.manyOrNone(
|
||||||
|
'select distinct user_id from contract_bets where contract_id = $1',
|
||||||
|
[contractId]
|
||||||
|
)
|
||||||
|
return res.map((r) => r.user_id as string)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
(you may notice we write sql in lowercase)
|
||||||
|
|
||||||
|
We have a few helper functions for updating and inserting data into the database.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import {
|
||||||
|
buikInsert,
|
||||||
|
bulkUpdate,
|
||||||
|
bulkUpdateData,
|
||||||
|
bulkUpsert,
|
||||||
|
insert,
|
||||||
|
update,
|
||||||
|
updateData,
|
||||||
|
} from 'shared/supabase/utils'
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
const pg = createSupabaseDirectClient()
|
||||||
|
|
||||||
|
// you are encouraged to use tryCatch for these
|
||||||
|
const { data, error } = await tryCatch(
|
||||||
|
insert(pg, 'profiles', { user_id: auth.uid, ...body })
|
||||||
|
)
|
||||||
|
|
||||||
|
if (error) throw APIError(500, 'Error creating profile: ' + error.message)
|
||||||
|
|
||||||
|
await update(pg, 'profiles', 'user_id', { user_id: auth.uid, age: 99 })
|
||||||
|
|
||||||
|
await updateData(pg, 'private_users', { id: userId, notifications: { ... } })
|
||||||
|
```
|
||||||
|
|
||||||
|
The sqlBuilder from `shared/supabase/sql-builder.ts` can be used to construct SQL queries with re-useable parts. All it does is sanitize and output sql query strings. It has several helper functions including:
|
||||||
|
|
||||||
|
- `select`: Specifies the columns to select
|
||||||
|
- `from`: Specifies the table to query
|
||||||
|
- `where`: Adds WHERE clauses
|
||||||
|
- `orderBy`: Specifies the order of results
|
||||||
|
- `limit`: Limits the number of results
|
||||||
|
- `renderSql`: Combines all parts into a final SQL string
|
||||||
|
|
||||||
|
Example usage:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const query = renderSql(
|
||||||
|
select('distinct user_id'),
|
||||||
|
from('contract_bets'),
|
||||||
|
where('contract_id = ${id}', { id }),
|
||||||
|
orderBy('created_time desc'),
|
||||||
|
limitValue != null && limit(limitValue)
|
||||||
|
)
|
||||||
|
|
||||||
|
const res = await pg.manyOrNone(query)
|
||||||
|
```
|
||||||
|
|
||||||
|
Use these functions instead of string concatenation.
|
||||||
@@ -19,7 +19,7 @@ We welcome pull requests, but only if they meet the project's quality and design
|
|||||||
3. **Add the upstream remote**:
|
3. **Add the upstream remote**:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git remote add upstream https://github.com/CompassMeet/Compass.git
|
git remote add upstream https://github.com/CompassConnections/Compass.git
|
||||||
```
|
```
|
||||||
|
|
||||||
## Create a New Branch
|
## Create a New Branch
|
||||||
|
|||||||
200
README.md
200
README.md
@@ -1,13 +1,13 @@
|
|||||||
|
|
||||||
[](https://github.com/CompassMeet/Compass/actions/workflows/ci.yml)
|

|
||||||
[](https://github.com/CompassMeet/Compass/actions/workflows/cd.yml)
|
[](https://github.com/CompassConnections/Compass/actions/workflows/cd.yml)
|
||||||

|
[](https://github.com/CompassConnections/Compass/actions/workflows/ci.yml)
|
||||||
|
[](https://codecov.io/gh/CompassConnections/Compass)
|
||||||
|
[](https://www.compassmeet.com/stats)
|
||||||
|
|
||||||
# Compass
|
# Compass
|
||||||
|
|
||||||
This repository provides the source code for [Compass](https://compassmeet.com), a web application for people to form deep 1-on-1 relationships in a fully transparent and efficient way. And it just got released!
|
This repository contains the source code for [Compass](https://compassmeet.com) — a transparent platform for forming deep, authentic 1-on-1 connections with clarity and efficiency.
|
||||||
|
|
||||||
**We can’t do this alone.** Whatever your skills—coding, design, writing, moderation, marketing, or even small donations—you can make a real difference. [Contribute](https://www.compassmeet.com/about) in any way you can and help our community thrive!
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@@ -18,10 +18,32 @@ This repository provides the source code for [Compass](https://compassmeet.com),
|
|||||||
- Open source
|
- Open source
|
||||||
- Democratically governed
|
- Democratically governed
|
||||||
|
|
||||||
A detailed description of the vision is available [here](https://martinbraquet.com/meeting-rational).
|
You can find a lot of interesting info in the [About page](https://www.compassmeet.com/about) and the [FAQ](https://www.compassmeet.com/faq) as well.
|
||||||
|
A detailed description of the early vision is also available in this [blog post](https://martinbraquet.com/meeting-rational) (you can disregard the parts about rationality, as Compass shifted to a more general audience).
|
||||||
|
|
||||||
|
**We can’t do this alone.** Whatever your skills—coding, design, writing, moderation, marketing, or even small donations—you can make a real difference. [Contribute](https://www.compassmeet.com/support) in any way you can and help our community thrive!
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## To Do
|
## To Do
|
||||||
|
|
||||||
|
No contribution is too small—whether it’s changing a color, resizing a button, tweaking a font, or improving wording. Bigger contributions like adding new profile fields, building modules, or improving onboarding are equally welcome. The goal is to make the platform better step by step, and every improvement counts. If you see something that could be clearer, smoother, or more engaging, **please jump in**!
|
||||||
|
|
||||||
|
The complete, official list of tasks is available [here on ClickUp](https://sharing.clickup.com/90181043445/l/h/6-901810339879-1/bbfd32f4f4bf64b). If you are working on one task, just assign it to yourself and move its status to "in progress". If there is also a GitHub issue for that task, assign it to yourself as well.
|
||||||
|
|
||||||
|
To have edit access to the ClickUp workspace, you need an admin to manually give you permission (one time thing). To do so, use your preferred option:
|
||||||
|
- Ask or DM an admin on [Discord](https://discord.gg/8Vd7jzqjun)
|
||||||
|
- Email hello@compassmeet.com
|
||||||
|
- Raise an issue on GitHub
|
||||||
|
|
||||||
|
If you want to add tasks without creating an account, you can simply email
|
||||||
|
```
|
||||||
|
a.t.901810339879.u-276866260.b847aba1-2709-4f17-b4dc-565a6967c234@tasks.clickup.com
|
||||||
|
```
|
||||||
|
Put the task title in the email subject and the task description in the email content.
|
||||||
|
|
||||||
|
Here is a tailored selection of things that would be very useful. If you want to help but don’t know where to start, just ask us on [Discord](https://discord.gg/8Vd7jzqjun).
|
||||||
|
|
||||||
- [x] Authentication (user/password and Google Sign In)
|
- [x] Authentication (user/password and Google Sign In)
|
||||||
- [x] Set up PostgreSQL in Production with supabase
|
- [x] Set up PostgreSQL in Production with supabase
|
||||||
- [x] Set up web hosting (vercel)
|
- [x] Set up web hosting (vercel)
|
||||||
@@ -29,20 +51,38 @@ A detailed description of the vision is available [here](https://martinbraquet.c
|
|||||||
- [x] Ask for detailed info upon registration (location, desired type of connection, prompt answers, gender, etc.)
|
- [x] Ask for detailed info upon registration (location, desired type of connection, prompt answers, gender, etc.)
|
||||||
- [x] Set up page listing all the profiles
|
- [x] Set up page listing all the profiles
|
||||||
- [x] Search through most profile variables
|
- [x] Search through most profile variables
|
||||||
- [x] (Set up chat / direct messaging)
|
- [x] Set up chat / direct messaging
|
||||||
- [x] Set up domain name (https://compassmeet.com)
|
- [x] Set up domain name (compassmeet.com)
|
||||||
|
- [ ] Cover with tests (unit, integration, e2e)
|
||||||
|
- [x] Add Android mobile app
|
||||||
|
- [ ] Add iOS mobile app
|
||||||
|
- [ ] Add better onboarding (tooltips, modals, etc.)
|
||||||
|
- [ ] Add modules to learn more about each other (personality test, conflict style, love languages, etc.)
|
||||||
|
- [ ] Add modules to improve interpersonal skills (active listening, nonviolent communication, etc.)
|
||||||
|
- [ ] Add calendar integration and scheduling
|
||||||
|
- [ ] Add events (group calls, in-person meetups, etc.)
|
||||||
|
|
||||||
#### Secondary To Do
|
#### Secondary To Do
|
||||||
|
|
||||||
Any action item is open to anyone for collaboration, but the following ones are particularly easy to do for first-time contributors.
|
Everything is open to anyone for collaboration, but the following ones are particularly easy to do for first-time contributors.
|
||||||
|
|
||||||
- [ ] Add profile features (intellectual interests, cause areas, personality type, conflict style, etc.)
|
|
||||||
- [ ] Add filters to search through remaining profile features (politics, religion, education level, etc.)
|
|
||||||
- [ ] Cover with tests (very important, just the test template and framework are ready)
|
|
||||||
- [ ] Clean up terms and conditions
|
|
||||||
- [ ] Clean up privacy notice
|
|
||||||
- [x] Clean up learn more page
|
- [x] Clean up learn more page
|
||||||
- [x] Add dark theme
|
- [x] Add dark theme
|
||||||
|
- [ ] Add profile fields (intellectual interests, cause areas, personality type, conflict style, timezone, etc.)
|
||||||
|
- [x] Add filters to search through remaining profile fields (politics, religion, education level, etc.)
|
||||||
|
- [ ] Make the app more user-friendly and appealing (UI/UX)
|
||||||
|
- [ ] Clean up terms and conditions (convert to Markdown)
|
||||||
|
- [ ] Clean up privacy notice (convert to Markdown)
|
||||||
|
- [ ] Add other authentication methods (GitHub, Facebook, Apple, phone, etc.)
|
||||||
|
- [x] Add email verification
|
||||||
|
- [x] Add password reset
|
||||||
|
- [x] Add automated welcome email
|
||||||
|
- [ ] Security audit and penetration testing
|
||||||
|
- [ ] Make `deploy-api.sh` run automatically on push to `main` branch
|
||||||
|
- [x] Create settings page (change email, password, delete account, etc.)
|
||||||
|
- [ ] Improve [financials](web/public/md/financials.md) page (donor / acknowledgments, etc.)
|
||||||
|
- [x] Improve loading sign (e.g., animation of a compass moving around)
|
||||||
|
- [ ] Show compatibility score in profile page
|
||||||
|
|
||||||
## Implementation
|
## Implementation
|
||||||
|
|
||||||
@@ -55,82 +95,33 @@ The web app is coded in Typescript using React as front-end. It includes:
|
|||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
Below are all the steps to contribute. If you have any trouble or questions, please don't hesitate to open an issue or contact us on [Discord](https://discord.gg/8Vd7jzqjun)! We're responsive and happy to help.
|
Below are the steps to contribute. If you have any trouble or questions, please don't hesitate to open an issue or contact us on [Discord](https://discord.gg/8Vd7jzqjun)! We're responsive and happy to help.
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
Clone the repo and navigating into it:
|
Fork the [repo](https://github.com/CompassConnections/Compass) on GitHub (button in top right). Then, clone your repo and navigating into it:
|
||||||
```bash
|
```bash
|
||||||
git clone git@github.com:CompassMeet/Compass.git
|
git clone https://github.com/<your-username>/Compass.git
|
||||||
cd Compass
|
cd Compass
|
||||||
```
|
```
|
||||||
|
|
||||||
Install `opentofu`, `docker`, and `yarn`. Try running this on Linux or macOS for a faster install:
|
Install `yarn` (if not already installed):
|
||||||
```bash
|
```bash
|
||||||
./setup.sh
|
npm install --global yarn
|
||||||
```
|
```
|
||||||
If it doesn't work, you can install them manually (Google how to install `opentofu`, `docker`, and `yarn` for your OS).
|
|
||||||
|
|
||||||
Then, install the dependencies for this project:
|
Then, install the dependencies for this project:
|
||||||
```bash
|
```bash
|
||||||
yarn install
|
yarn install
|
||||||
```
|
```
|
||||||
|
|
||||||
### Environment Variables
|
|
||||||
|
|
||||||
We can't make the following information public, for security and privacy reasons:
|
|
||||||
- Database, otherwise anyone could access all the user data (including private messages)
|
|
||||||
- Firebase, otherwise anyone could remove users or modify the media files
|
|
||||||
- Email, analytics, and location services, otherwise anyone could use our paid plan
|
|
||||||
|
|
||||||
So, for your development, we will give you user-specific access when possible (e.g., Firebase) and for the rest you will need to set up cloned services (email, locations, etc.) and store your secrets as environment variables.
|
|
||||||
|
|
||||||
To do so, simply create an `.env` file as a copy of `.env.example`, open it, and fill in the variables according to the instructions in the file:
|
|
||||||
```bash
|
|
||||||
cp .env.example .env
|
|
||||||
```
|
|
||||||
|
|
||||||
### Installing PostgreSQL
|
|
||||||
|
|
||||||
Run the following commands to set up your local development database. Run only the section that corresponds to your operating system.
|
|
||||||
|
|
||||||
On macOS:
|
|
||||||
```bash
|
|
||||||
brew install postgresql
|
|
||||||
brew services start postgresql
|
|
||||||
```
|
|
||||||
|
|
||||||
On Linux:
|
|
||||||
```bash
|
|
||||||
sudo apt update
|
|
||||||
sudo apt install postgresql postgresql-contrib
|
|
||||||
sudo systemctl start postgresql
|
|
||||||
````
|
|
||||||
|
|
||||||
On Windows, you can download PostgreSQL from the [official website](https://www.postgresql.org/download/windows/).
|
|
||||||
|
|
||||||
### Database Initialization
|
|
||||||
|
|
||||||
Create a database named `compass` and set the password for the `postgres` user:
|
|
||||||
```bash
|
|
||||||
sudo -u postgres psql
|
|
||||||
ALTER USER postgres WITH PASSWORD 'password';
|
|
||||||
\q
|
|
||||||
```
|
|
||||||
|
|
||||||
Create the database
|
|
||||||
```bash
|
|
||||||
...
|
|
||||||
```
|
|
||||||
Note that your local database will be made of synthetic data, not real users. This is fine for development and testing.
|
|
||||||
|
|
||||||
### Tests
|
### Tests
|
||||||
|
|
||||||
Make sure the tests pass:
|
Make sure the tests pass:
|
||||||
```bash
|
```bash
|
||||||
yarn test
|
yarn test
|
||||||
```
|
```
|
||||||
TODO: fix tests
|
If they don't and you can't find out why, simply raise an issue! Sometimes it's something on our end that we overlooked.
|
||||||
|
|
||||||
### Running the Development Server
|
### Running the Development Server
|
||||||
|
|
||||||
@@ -139,11 +130,74 @@ Start the development server:
|
|||||||
yarn dev
|
yarn dev
|
||||||
```
|
```
|
||||||
|
|
||||||
Once the server is running, visit http://localhost:3000 to start using the app. You can sign up and visit the profiles; you should see 5 synthetic profiles.
|
Once the server is running, visit http://localhost:3000 to start using the app. You can sign up and visit the profiles; you should see a few synthetic profiles.
|
||||||
|
|
||||||
|
Note: it's normal if page loading locally is much slower than the deployed version. It can take up to 10 seconds, it would be great to improve that though!
|
||||||
|
|
||||||
|
### Contributing
|
||||||
|
|
||||||
Now you can start contributing by making changes and submitting pull requests!
|
Now you can start contributing by making changes and submitting pull requests!
|
||||||
|
|
||||||
See [development.md](docs/development.md) for additional instructions, such as adding new profile features.
|
We recommend using a good code editor (VSCode, WebStorm, Cursor, etc.) with Typescript support and a good AI assistant (GitHub Copilot, etc.) to make your life easier. To debug, you can use the browser developer tools (F12), specifically:
|
||||||
|
- Components tab to see the React component tree and props (you need to install the [React Developer Tools](https://react.dev/learn/react-developer-tools) extension)
|
||||||
|
- Console tab for errors and logs
|
||||||
|
- Network tab to see the requests and responses
|
||||||
|
- Storage tab to see cookies and local storage
|
||||||
|
|
||||||
|
You can also add `console.log()` statements in the code.
|
||||||
|
|
||||||
|
If you are new to Typescript or the open-source space, you could start with small changes, such as tweaking some web components or improving wording in some pages. You can find those files in `web/public/md/`.
|
||||||
|
|
||||||
|
##### Resources
|
||||||
|
|
||||||
|
There is a lof of documentation in the [docs](docs) folder and across the repo, namely:
|
||||||
|
- [Next.js.md](docs/Next.js.md) for core fundamentals about our web / page-rendering framework.
|
||||||
|
- [knowledge.md](docs/knowledge.md) for general information about the project structure.
|
||||||
|
- [development.md](docs/development.md) for additional instructions, such as adding new profile fields.
|
||||||
|
- [web](web) for the web.
|
||||||
|
- [backend/api](backend/api) for the backend API.
|
||||||
|
- [android](android) for the Android app.
|
||||||
|
|
||||||
|
There are a lot of useful scripts you can use in the [scripts](scripts) folder.
|
||||||
|
|
||||||
|
### Submission
|
||||||
|
|
||||||
|
Add the original repo as upstream for syncing:
|
||||||
|
```bash
|
||||||
|
git remote add upstream https://github.com/CompassConnections/Compass.git
|
||||||
|
```
|
||||||
|
|
||||||
|
Create a new branch for your changes:
|
||||||
|
```bash
|
||||||
|
git checkout -b <branch-name>
|
||||||
|
```
|
||||||
|
|
||||||
|
Make changes, then stage and commit:
|
||||||
|
```bash
|
||||||
|
git add .
|
||||||
|
git commit -m "Describe your changes"
|
||||||
|
```
|
||||||
|
|
||||||
|
Push branch to your fork:
|
||||||
|
```bash
|
||||||
|
git push origin <branch-name>
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally, open a Pull Request on GitHub from your `fork/<branch-name>` → `CompassConnections/Compass` main branch.
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
|
||||||
|
Almost all the features will work out of the box, so you can skip this step and come back later if you need to test the following services: email, geolocation.
|
||||||
|
|
||||||
|
We can't make the following information public, for security and privacy reasons:
|
||||||
|
- Database, otherwise anyone could access all the user data (including private messages)
|
||||||
|
- Firebase, otherwise anyone could remove users or modify the media files
|
||||||
|
- Email, analytics, and location services, otherwise anyone could use the service plans Compass paid for and run up the bill.
|
||||||
|
|
||||||
|
That's why we separate all those services between production and development environments, so that you can code freely without impacting the functioning of the deployed platform.
|
||||||
|
Contributors should use the default keys for local development. Production uses a separate environment with stricter rules and private keys that are not shared.
|
||||||
|
|
||||||
|
If you do need one of the few remaining services, you need to set them up and store your own secrets as environment variables. To do so, simply open `.env` and fill in the variables according to the instructions in the file.
|
||||||
|
|
||||||
## Acknowledgements
|
## Acknowledgements
|
||||||
This project is built on top of [manifold.love](https://github.com/sipec/polylove), an open-source dating platform licensed under the MIT License. We greatly appreciate their work and contributions to open-source, which have significantly aided in the development of some core features such as direct messaging, prompts, and email notifications. We invite the community to explore and contribute to other open-source projects like manifold.love as well, especially if you're interested in functionalities that deviate from Compass' ideals of deep, intentional connections.
|
This project is built on top of [manifold.love](https://github.com/sipec/polylove), an open-source dating platform licensed under the MIT License. We greatly appreciate their work and contributions to open-source, which have significantly aided in the development of some core features such as direct messaging, prompts, and email notifications. We invite the community to explore and contribute to other open-source projects like manifold.love as well, especially if you're interested in functionalities that deviate from Compass' ideals of deep, intentional connections.
|
||||||
|
|||||||
@@ -8,5 +8,5 @@
|
|||||||
|
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
Contact the development team to report a vulnerability. You should receive updates within a week.
|
Contact the development team at hello@compassmeet.com to report a vulnerability. You should receive updates within a week.
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export default function RootLayout(
|
|||||||
<footer className="p-6 text-center text-gray-500">
|
<footer className="p-6 text-center text-gray-500">
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<a
|
<a
|
||||||
href="https://github.com/CompassMeet/Compass"
|
href="https://github.com/CompassConnections/Compass"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="inline-flex items-center text-gray-500 hover:text-gray-700 transition"
|
className="inline-flex items-center text-gray-500 hover:text-gray-700 transition"
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import {aColor, supportEmail} from "@/lib/client/constants";
|
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
|
||||||
export default function PrivacyPage() {
|
export default function PrivacyPage() {
|
||||||
@@ -49,7 +49,7 @@ export default function HomePage() {
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
{/*<header className="flex justify-between items-center p-2 max-w-6xl mx-auto w-full">*/}
|
{/*<header className="flex justify-between items-center p-2 max-w-6xl mx-auto w-full">*/}
|
||||||
{/* <a */}
|
{/* <a */}
|
||||||
{/* href="https://github.com/CompassMeet/Compass" */}
|
{/* href="https://github.com/CompassConnections/Compass" */}
|
||||||
{/* target="_blank" */}
|
{/* target="_blank" */}
|
||||||
{/* rel="noopener noreferrer"*/}
|
{/* rel="noopener noreferrer"*/}
|
||||||
{/* className="text-gray-700 hover: transition"*/}
|
{/* className="text-gray-700 hover: transition"*/}
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
import {useEffect, useState} from 'react';
|
import {useEffect, useState} from 'react';
|
||||||
import {Textarea} from '@/components/ui/textarea';
|
import {Textarea} from '@/components/ui/textarea';
|
||||||
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from '@/components/ui/select';
|
import {Select, SelectContent, SelectItem, SelectTrigger, SelectValue,} from '@/components/ui/select';
|
||||||
import {cons} from "effect/List";
|
// import {cons} from "effect/List";
|
||||||
|
|
||||||
type Prompt = {
|
type Prompt = {
|
||||||
id: string;
|
id: string;
|
||||||
2
_old/lib/client/constants.tsx
Normal file
2
_old/lib/client/constants.tsx
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
0
android/.aiexclude
Normal file
0
android/.aiexclude
Normal file
102
android/.gitignore
vendored
Normal file
102
android/.gitignore
vendored
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
# Using Android gitignore template: https://github.com/github/gitignore/blob/HEAD/Android.gitignore
|
||||||
|
|
||||||
|
# Built application files
|
||||||
|
*.apk
|
||||||
|
*.aar
|
||||||
|
*.ap_
|
||||||
|
*.aab
|
||||||
|
|
||||||
|
# Files for the ART/Dalvik VM
|
||||||
|
*.dex
|
||||||
|
|
||||||
|
# Java class files
|
||||||
|
*.class
|
||||||
|
|
||||||
|
# Generated files
|
||||||
|
bin/
|
||||||
|
gen/
|
||||||
|
out/
|
||||||
|
# Uncomment the following line in case you need and you don't have the release build type files in your app
|
||||||
|
# release/
|
||||||
|
|
||||||
|
# Gradle files
|
||||||
|
.gradle/
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Local configuration file (sdk path, etc)
|
||||||
|
local.properties
|
||||||
|
|
||||||
|
# Proguard folder generated by Eclipse
|
||||||
|
proguard/
|
||||||
|
|
||||||
|
# Log Files
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Android Studio Navigation editor temp files
|
||||||
|
.navigation/
|
||||||
|
|
||||||
|
# Android Studio captures folder
|
||||||
|
captures/
|
||||||
|
|
||||||
|
# IntelliJ
|
||||||
|
*.iml
|
||||||
|
.idea/workspace.xml
|
||||||
|
.idea/tasks.xml
|
||||||
|
.idea/gradle.xml
|
||||||
|
.idea/assetWizardSettings.xml
|
||||||
|
.idea/dictionaries
|
||||||
|
.idea/libraries
|
||||||
|
# Android Studio 3 in .gitignore file.
|
||||||
|
.idea/caches
|
||||||
|
.idea/modules.xml
|
||||||
|
# Comment next line if keeping position of elements in Navigation Editor is relevant for you
|
||||||
|
.idea/navEditor.xml
|
||||||
|
|
||||||
|
# Keystore files
|
||||||
|
# Uncomment the following lines if you do not want to check your keystore files in.
|
||||||
|
#*.jks
|
||||||
|
#*.keystore
|
||||||
|
|
||||||
|
# External native build folder generated in Android Studio 2.2 and later
|
||||||
|
.externalNativeBuild
|
||||||
|
.cxx/
|
||||||
|
|
||||||
|
# Google Services (e.g. APIs or Firebase)
|
||||||
|
# google-services.json
|
||||||
|
|
||||||
|
# Freeline
|
||||||
|
freeline.py
|
||||||
|
freeline/
|
||||||
|
freeline_project_description.json
|
||||||
|
|
||||||
|
# fastlane
|
||||||
|
fastlane/report.xml
|
||||||
|
fastlane/Preview.html
|
||||||
|
fastlane/screenshots
|
||||||
|
fastlane/test_output
|
||||||
|
fastlane/readme.md
|
||||||
|
|
||||||
|
# Version control
|
||||||
|
vcs.xml
|
||||||
|
|
||||||
|
# lint
|
||||||
|
lint/intermediates/
|
||||||
|
lint/generated/
|
||||||
|
lint/outputs/
|
||||||
|
lint/tmp/
|
||||||
|
# lint/reports/
|
||||||
|
|
||||||
|
# Android Profiling
|
||||||
|
*.hprof
|
||||||
|
|
||||||
|
# Cordova plugins for Capacitor
|
||||||
|
capacitor-cordova-android-plugins
|
||||||
|
|
||||||
|
# Copied web assets
|
||||||
|
app/src/main/assets/public
|
||||||
|
|
||||||
|
# Generated Config files
|
||||||
|
app/src/main/assets/capacitor.config.json
|
||||||
|
app/src/main/assets/capacitor.plugins.json
|
||||||
|
app/src/main/res/xml/config.xml
|
||||||
|
/app/release/
|
||||||
413
android/README.md
Normal file
413
android/README.md
Normal file
@@ -0,0 +1,413 @@
|
|||||||
|
# Compass Android WebView App
|
||||||
|
|
||||||
|
This folder contains the source code for the Android application of Compass.
|
||||||
|
A hybrid mobile app built with **Next.js (TypeScript)** frontend, **Firebase backend**, and wrapped as a **Capacitor WebView** for Android. In the future it may contain native code as well.
|
||||||
|
|
||||||
|
This document describes how to:
|
||||||
|
1. Build and run the web frontend and backend locally
|
||||||
|
2. Sync and build the Android WebView wrapper
|
||||||
|
3. Debug, sign, and publish the APK
|
||||||
|
4. Enable Google Sign-In and push notifications
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Project Overview
|
||||||
|
|
||||||
|
The app is a Capacitor Android project that loads the local Next.js assests inside a WebView.
|
||||||
|
|
||||||
|
During development, it can instead load the local frontend (`http://10.0.2.2:3000`) and backend (`http://10.0.2.2:8088`).
|
||||||
|
|
||||||
|
Firebase handles authentication and push notifications.
|
||||||
|
Google Sign-In is supported natively in the WebView via the Capacitor Social Login plugin.
|
||||||
|
|
||||||
|
Project Structure
|
||||||
|
|
||||||
|
- `app/src/main/java/com/compass/app`: Contains the Java/Kotlin source code for the Android application.
|
||||||
|
- `app/src/main/res`: Contains the resources for the application, such as layouts, strings, and images.
|
||||||
|
- `app/build.gradle`: The Gradle build file for the Android application module.
|
||||||
|
- `build.gradle`: The top-level Gradle build file for the project.
|
||||||
|
- `AndroidManifest.xml`: The manifest file that describes essential information about the application.
|
||||||
|
|
||||||
|
### **Why Local Is the Default**
|
||||||
|
- **Performance:** Local assets load instantly, without network latency.
|
||||||
|
- **Reliability:** Works offline or in poor connectivity environments.
|
||||||
|
- **App Store policy compliance:** Apple and Google generally prefer that the main experience doesn’t depend on a remote site (for security, review, and performance reasons).
|
||||||
|
- **Version consistency:** The web bundle is versioned with the app, ensuring no breaking updates outside your control.
|
||||||
|
|
||||||
|
When Remote (No Local Assets) Is sometimes Used
|
||||||
|
Loading from a **remote URL** (e.g. `https://compassmeet.com`) is **less common**, but seen in a few cases:
|
||||||
|
- **Internal enterprise apps** where the WebView just wraps an existing web portal.
|
||||||
|
- **Dynamic content** or **frequent updates** where pushing a new web build every time through app stores would be too slow.
|
||||||
|
- To leverage the low latency of ISR and SSR.
|
||||||
|
However, this approach requires:
|
||||||
|
- Careful handling of **CORS**, **SSL**, and **login/session** persistence.
|
||||||
|
- Compliance with **Google Play policies** (they may reject apps that are “just a webview of a website” unless there’s meaningful native integration).
|
||||||
|
|
||||||
|
**A middle ground we use:**
|
||||||
|
- The app ships with **local assets** for core functionality.
|
||||||
|
- The app **fetches remote content or updates** (e.g., via Capacitor Live Updates, Ionic Appflow).
|
||||||
|
|
||||||
|
## 2. Prerequisites
|
||||||
|
|
||||||
|
### Required Software
|
||||||
|
| Tool | Version | Purpose |
|
||||||
|
| -------------- | ------- | ---------------------------------- |
|
||||||
|
| Node.js | 22+ | For building frontend/backend |
|
||||||
|
| yarn | latest | Package manager |
|
||||||
|
| Java | 21 | Required for Android Gradle plugin |
|
||||||
|
| Android Studio | latest | For building and signing APKs |
|
||||||
|
| Capacitor CLI | latest | Android bridge |
|
||||||
|
| OpenJDK | 21 | JDK for Gradle |
|
||||||
|
|
||||||
|
### Environment Setup
|
||||||
|
```bash
|
||||||
|
sudo apt install openjdk-21-jdk
|
||||||
|
sudo update-alternatives --config java
|
||||||
|
# Select Java 21
|
||||||
|
|
||||||
|
export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64
|
||||||
|
java -version
|
||||||
|
javac -version
|
||||||
|
````
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Build and Run the Web App
|
||||||
|
|
||||||
|
```
|
||||||
|
yarn install
|
||||||
|
yarn build-web-view
|
||||||
|
```
|
||||||
|
|
||||||
|
### Local mode
|
||||||
|
|
||||||
|
If you want the webview to load from your local web version of Compass, run the web app.
|
||||||
|
|
||||||
|
In root directory:
|
||||||
|
```bash
|
||||||
|
export NEXT_PUBLIC_LOCAL_ANDROID=1
|
||||||
|
yarn dev # or prod
|
||||||
|
```
|
||||||
|
|
||||||
|
* Runs Next.js frontend at `http://localhost:3000`
|
||||||
|
* Runs backend at `http://10.0.2.2:8088`
|
||||||
|
|
||||||
|
### Deployed mode
|
||||||
|
|
||||||
|
If you want the webview to load from the deployed web version of Compass (like at www.compassmeet.com), no web app to run.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Android WebView App Setup
|
||||||
|
|
||||||
|
### Install dependencies
|
||||||
|
|
||||||
|
```
|
||||||
|
cd android
|
||||||
|
./gradlew clean
|
||||||
|
```
|
||||||
|
|
||||||
|
Sync web files and native plugins with Android, for offline fallback. In root:
|
||||||
|
```
|
||||||
|
export NEXT_PUBLIC_LOCAL_ANDROID=1 # if running your local web Compass
|
||||||
|
yarn build-web-view # if you made changes to web app
|
||||||
|
npx cap sync android
|
||||||
|
```
|
||||||
|
|
||||||
|
### Load from site
|
||||||
|
|
||||||
|
During local development, open Android Studio project and run the app on an emulator or your physical device.
|
||||||
|
|
||||||
|
To use an emulator:
|
||||||
|
```
|
||||||
|
npx cap open android
|
||||||
|
```
|
||||||
|
|
||||||
|
To use a physical device for the local web version, you need your mobile and computer to be on the same network / Wi-Fi and point the URL (`LOCAL_BACKEND_DOMAIN` in the code) to your computer IP address (for example, `192.168.1.3:3000`). You also need to set
|
||||||
|
```
|
||||||
|
export NEXT_PUBLIC_WEBVIEW_DEV_PHONE=1
|
||||||
|
```
|
||||||
|
Then adb install the app your phone (or simply run it from Android Studio on your phone) and the app should be loading content directly from the local code on your computer. You can make changes in the code and it will refresh instantly on the phone.
|
||||||
|
|
||||||
|
Building the Application:
|
||||||
|
1. Open Android Studio.
|
||||||
|
2. Click on "Open an existing Android Studio project".
|
||||||
|
3. Navigate to the `android` folder in this repository and select it.
|
||||||
|
4. Wait for Android Studio to index the project and download any necessary dependencies.
|
||||||
|
5. Connect your Android device via USB or set up an Android emulator.
|
||||||
|
6. Click on the "Run" button (green play button) in Android Studio to build and run the application.
|
||||||
|
7. Select your device or emulator and click "OK".
|
||||||
|
8. The application should now build and launch on your device or emulator.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Building the APK
|
||||||
|
|
||||||
|
### From Android Studio
|
||||||
|
|
||||||
|
- If you want to generate a signed APK for release, go to "Build" > "Generate Signed Bundle / APK..." and follow the prompts.
|
||||||
|
- Make sure to test the application thoroughly on different devices and Android versions to ensure compatibility.
|
||||||
|
|
||||||
|
### Debug build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd android
|
||||||
|
./gradlew assembleDebug
|
||||||
|
```
|
||||||
|
|
||||||
|
Outputs:
|
||||||
|
|
||||||
|
```
|
||||||
|
android/app/build/outputs/apk/debug/app-debug.apk
|
||||||
|
```
|
||||||
|
|
||||||
|
### Install on emulator
|
||||||
|
|
||||||
|
```bash
|
||||||
|
adb install -r app/build/outputs/apk/debug/app-debug.apk
|
||||||
|
```
|
||||||
|
|
||||||
|
### Release build (signed)
|
||||||
|
|
||||||
|
1. Generate a release keystore:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
keytool -genkey -v -keystore release-key.keystore -alias compass \
|
||||||
|
-keyalg RSA -keysize 2048 -validity 10000
|
||||||
|
```
|
||||||
|
2. Add signing config to `android/app/build.gradle`
|
||||||
|
3. Build:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew assembleRelease
|
||||||
|
```
|
||||||
|
|
||||||
|
### Release on App Stores
|
||||||
|
|
||||||
|
To release on the app stores, you need to submit the .aab files, which are not signed, instead of APK. Google or Apple will then sign it with their own key.
|
||||||
|
|
||||||
|
---
|
||||||
|
## 9. Debugging
|
||||||
|
|
||||||
|
Client logs from the emulator on Chrome can be accessed at:
|
||||||
|
```
|
||||||
|
chrome://inspect/#devices
|
||||||
|
```
|
||||||
|
|
||||||
|
Backend logs can be accessed from the output of `yarn prod / dev` like in the web application.
|
||||||
|
|
||||||
|
Java/Kotlin logs can be accessed via Android Studio's Logcat.
|
||||||
|
```
|
||||||
|
adb logcat | grep CompassApp
|
||||||
|
adb logcat | grep com.compassconnections.app
|
||||||
|
adb logcat | grep Capacitor
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also add this inside `MainActivity.java`:
|
||||||
|
|
||||||
|
```java
|
||||||
|
webView.setWebChromeClient(new WebChromeClient() {
|
||||||
|
@Override
|
||||||
|
public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
|
||||||
|
Log.d("WebView", consoleMessage.message());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Deploy to Play Store
|
||||||
|
|
||||||
|
1. Sign the release APK or AAB.
|
||||||
|
2. Verify package name matches Firebase settings (`com.compassconnections.app`).
|
||||||
|
3. Upload to Google Play Console.
|
||||||
|
4. Add Privacy Policy and content rating.
|
||||||
|
5. Submit for review.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Common Issues
|
||||||
|
|
||||||
|
| Problem | Cause | Fix |
|
||||||
|
| -------------------------------------- | -------------------------------------- | ------------------------------------------------------------------- |
|
||||||
|
| `INSTALL_FAILED_UPDATE_INCOMPATIBLE` | Old APK signed with different key | Uninstall old app first |
|
||||||
|
| `Account reauth failed [16]` | Missing or incorrect SHA-1 in Firebase | Re-add SHA-1 of keystore |
|
||||||
|
| App opens in Firefox | Missing `WebViewClient` override | Fix `shouldOverrideUrlLoading` |
|
||||||
|
| APK > 1 GB | Cached webpack artifacts included | Add `.next/` and `/public/cache` to `.gitignore` and build excludes |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Local Development Workflow
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Terminal 1
|
||||||
|
export NEXT_PUBLIC_LOCAL_ANDROID=1
|
||||||
|
yarn dev # or prod
|
||||||
|
|
||||||
|
# Terminal 2: start frontend
|
||||||
|
export NEXT_PUBLIC_LOCAL_ANDROID=1
|
||||||
|
yarn build-web-view # if you made changes to web app
|
||||||
|
npx cap sync android
|
||||||
|
# Run on emulator or device
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. Deployment Workflow
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build web app for production and Sync assets to Android
|
||||||
|
yarn build-sync-android
|
||||||
|
|
||||||
|
# Build signed release APK in Android Studio
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Live Updates
|
||||||
|
|
||||||
|
To avoid releasing to the app stores after every code update in the web pages, we build the new bundle and store it in Capawesome Cloud (an alternative to Ionic).
|
||||||
|
|
||||||
|
First, you need to do this one-time setup:
|
||||||
|
```
|
||||||
|
npm install -g @capawesome/cli@latest
|
||||||
|
npx @capawesome/cli login
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, run this to build your local assets and push them to Capawesome. Once done, each mobile app user will receive a notice that there is a new update available, which they can approve to download.
|
||||||
|
```
|
||||||
|
yarn build-web-view
|
||||||
|
npx @capawesome/cli apps:bundles:create --path web/out
|
||||||
|
```
|
||||||
|
|
||||||
|
That's all. So you should run the lines above every time you want your web updates pushed to main (which essentially updates the web app) to update the mobile app as well.
|
||||||
|
Maybe we should add it to our CD. For example we set a file with `{liveUpdateVersion: 1}` and run the live update each time a push to main increments that counter.
|
||||||
|
There is a limit of 100 monthly active user per month, though. So we may need to pay or create our custom limit as we scale. Next plan is $9 / month and allows 1000 MAUs.
|
||||||
|
|
||||||
|
- ∞ Live Updates
|
||||||
|
- 100 Monthly Active Users
|
||||||
|
- 500 MB of Storage (around 10 MB per update, but we just delete the previous ones)
|
||||||
|
- 5 GB of Bandwidth
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 15. Resources
|
||||||
|
|
||||||
|
* [Capacitor Docs](https://capacitorjs.com/docs)
|
||||||
|
* [Firebase Android Setup](https://firebase.google.com/docs/android/setup)
|
||||||
|
* [FCM HTTP API](https://firebase.google.com/docs/cloud-messaging/send-message)
|
||||||
|
* [Next.js Deployment](https://nextjs.org/docs/deployment)
|
||||||
|
|
||||||
|
|
||||||
|
# Useful Commands
|
||||||
|
|
||||||
|
- To build the project: `./gradlew assembleDebug`
|
||||||
|
- To run unit tests: `./gradlew test`
|
||||||
|
- To run instrumentation tests: `./gradlew connectedAndroidTest`
|
||||||
|
- To clean the project: `./gradlew clean`
|
||||||
|
- To install dependencies: Open Android Studio and it will handle dependencies automatically.
|
||||||
|
- To update dependencies: Modify the `build.gradle` files and sync the project in Android Studio.
|
||||||
|
- To generate a signed APK: Use the "Generate Signed Bundle / APK..." option in Android Studio.
|
||||||
|
- To lint the project: `./gradlew lint`
|
||||||
|
- To check for updates to the Android Gradle Plugin: `./gradlew dependencyUpdates`
|
||||||
|
- To run the application on a connected device or emulator: `./gradlew installDebug`
|
||||||
|
- To view the project structure: Use the "Project" view in Android Studio.
|
||||||
|
- To analyze the APK: `./gradlew analyzeRelease`
|
||||||
|
- To run ProGuard/R8: `./gradlew minifyRelease`
|
||||||
|
- To generate documentation: `./gradlew javadoc`
|
||||||
|
|
||||||
|
# One time setups
|
||||||
|
|
||||||
|
Was already done for Compass, so you only need to do the steps below if you create a project separated from Compass.
|
||||||
|
## Configure Firebase
|
||||||
|
|
||||||
|
### In Firebase Console
|
||||||
|
|
||||||
|
1. Add a **Web app** → obtain `firebaseConfig`
|
||||||
|
2. Add an **Android app**
|
||||||
|
|
||||||
|
* Package name: `com.compassconnections.app`
|
||||||
|
* Add your SHA-1 and SHA-256 fingerprints (see below)
|
||||||
|
* Download `google-services.json` and put it in:
|
||||||
|
|
||||||
|
```
|
||||||
|
android/app/google-services.json
|
||||||
|
```
|
||||||
|
|
||||||
|
### To get SHA-1 for debug keystore
|
||||||
|
|
||||||
|
```bash
|
||||||
|
keytool -list -v \
|
||||||
|
-keystore ~/.android/debug.keystore \
|
||||||
|
-alias androiddebugkey \
|
||||||
|
-storepass android \
|
||||||
|
-keypass android
|
||||||
|
```
|
||||||
|
|
||||||
|
Add both SHA-1 and SHA-256 to Firebase.
|
||||||
|
|
||||||
|
|
||||||
|
## 7. Google Sign-In (Web + Native)
|
||||||
|
|
||||||
|
In Firebase Console:
|
||||||
|
|
||||||
|
* Enable **Google** provider under *Authentication → Sign-in method*
|
||||||
|
* Add your **Android SHA-1**
|
||||||
|
* Add your **Web OAuth client ID**
|
||||||
|
|
||||||
|
In your code:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { googleNativeLogin } from 'web/lib/service/android-push'
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## 8. Push Notifications (FCM)
|
||||||
|
|
||||||
|
### Setup FCM
|
||||||
|
|
||||||
|
* Add Firebase Cloud Messaging to your project
|
||||||
|
* Include `google-services.json` under `android/app/`
|
||||||
|
* Add in `android/build.gradle`:
|
||||||
|
|
||||||
|
```gradle
|
||||||
|
classpath 'com.google.gms:google-services:4.3.15'
|
||||||
|
```
|
||||||
|
* Add at the bottom of `android/app/build.gradle`:
|
||||||
|
|
||||||
|
```gradle
|
||||||
|
apply plugin: 'com.google.gms.google-services'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test notification
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const message = {
|
||||||
|
notification: {
|
||||||
|
title: "Test Notification",
|
||||||
|
body: "Hello from Firebase Admin SDK"
|
||||||
|
},
|
||||||
|
token: "..."
|
||||||
|
};
|
||||||
|
initAdmin()
|
||||||
|
await admin.messaging().send(message)
|
||||||
|
.then(response => console.log("Successfully sent message:", response))
|
||||||
|
.catch(error => console.error("Error sending message:", error));
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Deep link / custom scheme
|
||||||
|
|
||||||
|
A **custom scheme** is a URL protocol that your app owns.
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```
|
||||||
|
com.compassconnections.app://auth
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
When Android (or iOS) sees a redirect to one of these URLs, it **launches your app** and passes it the URL data. It's useful to open links in the app instead of the browser. For example, if there's a link to Compass on Discord and we click on it on a mobile device that has the app, we want the link to open in the app instead of the browser.
|
||||||
|
|
||||||
|
You register this scheme in your `AndroidManifest.xml` so Android knows which app handles it.
|
||||||
3
android/app/.gitignore
vendored
Normal file
3
android/app/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
/build/*
|
||||||
|
!/build/.npmkeep
|
||||||
|
/google-services.json
|
||||||
66
android/app/build.gradle
Normal file
66
android/app/build.gradle
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
apply plugin: 'com.android.application'
|
||||||
|
apply plugin: 'com.google.gms.google-services'
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace "com.compassconnections.app"
|
||||||
|
compileSdk 36
|
||||||
|
defaultConfig {
|
||||||
|
applicationId "com.compassconnections.app"
|
||||||
|
minSdkVersion rootProject.ext.minSdkVersion
|
||||||
|
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||||
|
versionCode 14
|
||||||
|
versionName "1.1.3"
|
||||||
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
aaptOptions {
|
||||||
|
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||||
|
// Default: https://android.googlesource.com/platform/frameworks/base/+/282e181b58cf72b6ca770dc7ca5f91f135444502/tools/aapt/AaptAssets.cpp#61
|
||||||
|
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled false
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
flatDir{
|
||||||
|
dirs '../capacitor-cordova-android-plugins/src/main/libs', 'libs'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation fileTree(include: ['*.jar'], dir: 'libs')
|
||||||
|
implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
|
||||||
|
implementation "androidx.coordinatorlayout:coordinatorlayout:$androidxCoordinatorLayoutVersion"
|
||||||
|
implementation "androidx.core:core-splashscreen:$coreSplashScreenVersion"
|
||||||
|
implementation project(':capacitor-android')
|
||||||
|
testImplementation "junit:junit:$junitVersion"
|
||||||
|
androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
|
||||||
|
androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
|
||||||
|
implementation project(':capacitor-cordova-android-plugins')
|
||||||
|
|
||||||
|
// Import the Firebase BoM
|
||||||
|
implementation platform('com.google.firebase:firebase-bom:34.4.0')
|
||||||
|
// TODO: Add the dependencies for Firebase products you want to use
|
||||||
|
// When using the BoM, don't specify versions in Firebase dependencies
|
||||||
|
implementation 'com.google.firebase:firebase-analytics'
|
||||||
|
// Add the dependencies for any other desired Firebase products
|
||||||
|
// https://firebase.google.com/docs/android/setup#available-libraries
|
||||||
|
|
||||||
|
implementation 'com.google.android.gms:play-services-auth:21.4.0'
|
||||||
|
implementation 'com.google.firebase:firebase-auth:24.0.1'
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: 'capacitor.build.gradle'
|
||||||
|
|
||||||
|
try {
|
||||||
|
def servicesJSON = file('google-services.json')
|
||||||
|
if (servicesJSON.text) {
|
||||||
|
apply plugin: 'com.google.gms.google-services'
|
||||||
|
}
|
||||||
|
} catch(Exception e) {
|
||||||
|
logger.info("google-services.json not found, google-services plugin not applied. Push Notifications won't work")
|
||||||
|
}
|
||||||
24
android/app/capacitor.build.gradle
Normal file
24
android/app/capacitor.build.gradle
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_21
|
||||||
|
targetCompatibility JavaVersion.VERSION_21
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
||||||
|
dependencies {
|
||||||
|
implementation project(':capacitor-app')
|
||||||
|
implementation project(':capacitor-keyboard')
|
||||||
|
implementation project(':capacitor-push-notifications')
|
||||||
|
implementation project(':capacitor-status-bar')
|
||||||
|
implementation project(':capawesome-capacitor-live-update')
|
||||||
|
implementation project(':capgo-capacitor-social-login')
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (hasProperty('postBuildExtras')) {
|
||||||
|
postBuildExtras()
|
||||||
|
}
|
||||||
21
android/app/proguard-rules.pro
vendored
Normal file
21
android/app/proguard-rules.pro
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Add project specific ProGuard rules here.
|
||||||
|
# You can control the set of applied configuration files using the
|
||||||
|
# proguardFiles setting in build.gradle.
|
||||||
|
#
|
||||||
|
# For more details, see
|
||||||
|
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||||
|
|
||||||
|
# If your project uses WebView with JS, uncomment the following
|
||||||
|
# and specify the fully qualified class name to the JavaScript interface
|
||||||
|
# class:
|
||||||
|
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||||
|
# public *;
|
||||||
|
#}
|
||||||
|
|
||||||
|
# Uncomment this to preserve the line number information for
|
||||||
|
# debugging stack traces.
|
||||||
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
|
|
||||||
|
# If you keep the line number information, uncomment this to
|
||||||
|
# hide the original source file name.
|
||||||
|
#-renamesourcefileattribute SourceFile
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.getcapacitor.myapp;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instrumented test, which will execute on an Android device.
|
||||||
|
*
|
||||||
|
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||||
|
*/
|
||||||
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
public class ExampleInstrumentedTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void useAppContext() throws Exception {
|
||||||
|
// Context of the app under test.
|
||||||
|
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
|
||||||
|
|
||||||
|
assertEquals("com.getcapacitor.app", appContext.getPackageName());
|
||||||
|
}
|
||||||
|
}
|
||||||
76
android/app/src/main/AndroidManifest.xml
Normal file
76
android/app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
<?xml version="1.1" encoding="utf-8" ?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
<application
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:usesCleartextTraffic="true"
|
||||||
|
android:fitsSystemWindows="true"
|
||||||
|
android:theme="@style/AppTheme">
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation"
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:label="@string/title_activity_main"
|
||||||
|
android:theme="@style/AppTheme.NoActionBarLaunch"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:exported="true">
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<!-- <intent-filter>-->
|
||||||
|
<!-- <action android:name="openapp" />-->
|
||||||
|
<!-- <category android:name="android.intent.category.DEFAULT" />-->
|
||||||
|
<!-- <category android:name="android.intent.category.BROWSABLE" />-->
|
||||||
|
|
||||||
|
<!-- <data android:scheme="com.compassconnections.app" android:host="details"/>-->
|
||||||
|
<!-- </intent-filter>-->
|
||||||
|
|
||||||
|
<!-- <intent-filter android:autoVerify="true">-->
|
||||||
|
<!-- <action android:name="android.intent.action.VIEW" />-->
|
||||||
|
<!-- <category android:name="android.intent.category.DEFAULT" />-->
|
||||||
|
<!-- <category android:name="android.intent.category.BROWSABLE" />-->
|
||||||
|
<!-- <data android:scheme="com.compassconnections.app" android:host="auth" />-->
|
||||||
|
<!-- </intent-filter>-->
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<!-- <service-->
|
||||||
|
<!-- android:name=".MyMessagingService"-->
|
||||||
|
<!-- android:exported="false">-->
|
||||||
|
<!-- <intent-filter>-->
|
||||||
|
<!-- <action android:name="com.google.firebase.MESSAGING_EVENT" />-->
|
||||||
|
<!-- </intent-filter>-->
|
||||||
|
<!--<!– <meta-data–>-->
|
||||||
|
<!--<!– android:name="com.google.firebase.messaging.default_notification_channel_id"–>-->
|
||||||
|
<!--<!– android:value="@string/default_notification_channel_id" />–>-->
|
||||||
|
<!-- </service>-->
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="${applicationId}.fileprovider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" />
|
||||||
|
</provider>
|
||||||
|
</application>
|
||||||
|
|
||||||
|
<!-- Permissions -->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
<uses-permission android:name="android.permission.VIBRATE" />
|
||||||
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
|
|
||||||
|
<uses-permission android:name="com.google.android.gms.permission.AD_ID" tools:node="remove" />
|
||||||
|
|
||||||
|
<!-- Firebase Cloud Messaging -->
|
||||||
|
<permission android:name="${applicationId}.permission.C2D_MESSAGE" android:protectionLevel="signature" />
|
||||||
|
<uses-permission android:name="${applicationId}.permission.C2D_MESSAGE" />
|
||||||
|
<!-- Old, can be removed ?-->
|
||||||
|
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
|
||||||
|
</manifest>
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
package com.compassconnections.app;
|
||||||
|
|
||||||
|
import android.Manifest;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.webkit.JavascriptInterface;
|
||||||
|
import android.webkit.WebSettings;
|
||||||
|
import android.webkit.WebView;
|
||||||
|
|
||||||
|
import androidx.activity.result.ActivityResultLauncher;
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
|
||||||
|
import com.capacitorjs.plugins.pushnotifications.PushNotificationsPlugin;
|
||||||
|
import com.getcapacitor.BridgeActivity;
|
||||||
|
import com.getcapacitor.BridgeWebViewClient;
|
||||||
|
import com.getcapacitor.Plugin;
|
||||||
|
import com.getcapacitor.PluginHandle;
|
||||||
|
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import ee.forgr.capacitor.social.login.GoogleProvider;
|
||||||
|
import ee.forgr.capacitor.social.login.ModifiedMainActivityForSocialLoginPlugin;
|
||||||
|
import ee.forgr.capacitor.social.login.SocialLoginPlugin;
|
||||||
|
|
||||||
|
|
||||||
|
//import android.app.NotificationChannel;
|
||||||
|
//import android.app.NotificationManager;
|
||||||
|
//import android.os.Build;
|
||||||
|
//import com.google.firebase.messaging.RemoteMessage;
|
||||||
|
//import com.capacitorjs.plugins.pushnotifications.MessagingService;
|
||||||
|
|
||||||
|
//public class MyMessagingService extends MessagingService {
|
||||||
|
//
|
||||||
|
// @Override
|
||||||
|
// public void onMessageReceived(RemoteMessage remoteMessage) {
|
||||||
|
// // TODO(developer): Handle FCM messages here.
|
||||||
|
// // Not getting messages here? See why this may be: https://goo.gl/39bRNJ
|
||||||
|
// Log.d(TAG, "From: " + remoteMessage.getFrom());
|
||||||
|
//
|
||||||
|
// // Check if message contains a data payload.
|
||||||
|
// if (remoteMessage.getData().size() > 0) {
|
||||||
|
// Log.d(TAG, "Message data payload: " + remoteMessage.getData());
|
||||||
|
//
|
||||||
|
// if (/* Check if data needs to be processed by long running job */ true) {
|
||||||
|
// // For long-running tasks (10 seconds or more) use WorkManager.
|
||||||
|
// scheduleJob();
|
||||||
|
// } else {
|
||||||
|
// // Handle message within 10 seconds
|
||||||
|
// handleNow();
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Check if message contains a notification payload.
|
||||||
|
// if (remoteMessage.getNotification() != null) {
|
||||||
|
// Log.d(TAG, "Message Notification Body: " + remoteMessage.getNotification().getBody());
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Also if you intend on generating your own notifications as a result of a received FCM
|
||||||
|
// // message, here is where that should be initiated. See sendNotification method below.
|
||||||
|
// }
|
||||||
|
//}
|
||||||
|
|
||||||
|
|
||||||
|
public class MainActivity extends BridgeActivity implements ModifiedMainActivityForSocialLoginPlugin {
|
||||||
|
|
||||||
|
// Declare this at class level
|
||||||
|
private final ActivityResultLauncher<String> requestPermissionLauncher =
|
||||||
|
registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> {
|
||||||
|
if (isGranted) {
|
||||||
|
Log.i("CompassApp", "Permission granted");
|
||||||
|
// Permission granted – you can show notifications
|
||||||
|
} else {
|
||||||
|
Log.i("CompassApp", "Permission denied");
|
||||||
|
// Permission denied – handle gracefully
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
private void askNotificationPermission() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { // API 33
|
||||||
|
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
|
||||||
|
!= PackageManager.PERMISSION_GRANTED) {
|
||||||
|
// Permission not yet granted; request it
|
||||||
|
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class NativeBridge {
|
||||||
|
@JavascriptInterface
|
||||||
|
public boolean isNativeApp() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onNewIntent(Intent intent) {
|
||||||
|
super.onNewIntent(intent);
|
||||||
|
|
||||||
|
// String data = intent.getDataString();
|
||||||
|
String endpoint = intent.getStringExtra("endpoint");
|
||||||
|
Log.i("CompassApp", "onNewIntent called with endpoint: " + endpoint);
|
||||||
|
if (endpoint != null) {
|
||||||
|
Log.i("CompassApp", "redirecting to endpoint: " + endpoint);
|
||||||
|
try {
|
||||||
|
String payload = new JSONObject().put("endpoint", endpoint).toString();
|
||||||
|
Log.i("CompassApp", "Payload: " + payload);
|
||||||
|
bridge.getWebView().post(() -> bridge.getWebView().evaluateJavascript("bridgeRedirect(" + payload + ");", null));
|
||||||
|
} catch (JSONException e) {
|
||||||
|
Log.i("CompassApp", "Failed to encode JSON payload", e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.i("CompassApp", "No relevant data");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
|
Log.i("CompassApp", "onCreate called");
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
WebView webView = this.bridge.getWebView();
|
||||||
|
webView.setWebViewClient(new BridgeWebViewClient(this.bridge));
|
||||||
|
|
||||||
|
WebView.setWebContentsDebuggingEnabled(true);
|
||||||
|
|
||||||
|
// Set a recognizable User-Agent (always reliable)
|
||||||
|
WebSettings settings = webView.getSettings();
|
||||||
|
settings.setUserAgentString(settings.getUserAgentString() + " CompassAppWebView");
|
||||||
|
|
||||||
|
settings.setJavaScriptEnabled(true);
|
||||||
|
webView.addJavascriptInterface(new NativeBridge(), "AndroidBridge");
|
||||||
|
|
||||||
|
registerPlugin(PushNotificationsPlugin.class);
|
||||||
|
// Initialize the Bridge with Push Notifications plugin
|
||||||
|
// this.init(savedInstanceState, new ArrayList<Class<? extends Plugin>>() {{
|
||||||
|
// add(com.getcapacitor.plugin.PushNotifications.class);
|
||||||
|
// }});
|
||||||
|
|
||||||
|
askNotificationPermission();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, data);
|
||||||
|
|
||||||
|
if (requestCode >= GoogleProvider.REQUEST_AUTHORIZE_GOOGLE_MIN && requestCode < GoogleProvider.REQUEST_AUTHORIZE_GOOGLE_MAX) {
|
||||||
|
PluginHandle pluginHandle = getBridge().getPlugin("SocialLogin");
|
||||||
|
if (pluginHandle == null) {
|
||||||
|
Log.i("CompassApp", "SocialLogin login handle is null");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Plugin plugin = pluginHandle.getInstance();
|
||||||
|
if (!(plugin instanceof SocialLoginPlugin)) {
|
||||||
|
Log.i("CompassApp", "SocialLogin plugin instance is not SocialLoginPlugin");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Log.i("CompassApp", "handleGoogleLoginIntent");
|
||||||
|
((SocialLoginPlugin) plugin).handleGoogleLoginIntent(requestCode, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This function will never be called, leave it empty
|
||||||
|
@Override
|
||||||
|
public void IHaveModifiedTheMainActivityForTheUseWithSocialLoginPlugin() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportHeight="108"
|
||||||
|
android:viewportWidth="108">
|
||||||
|
<path
|
||||||
|
android:fillType="evenOdd"
|
||||||
|
android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
|
||||||
|
android:strokeColor="#00000000"
|
||||||
|
android:strokeWidth="1">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:endX="78.5885"
|
||||||
|
android:endY="90.9159"
|
||||||
|
android:startX="48.7653"
|
||||||
|
android:startY="61.0927"
|
||||||
|
android:type="linear">
|
||||||
|
<item
|
||||||
|
android:color="#44000000"
|
||||||
|
android:offset="0.0" />
|
||||||
|
<item
|
||||||
|
android:color="#00000000"
|
||||||
|
android:offset="1.0" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
|
||||||
|
android:strokeColor="#00000000"
|
||||||
|
android:strokeWidth="1" />
|
||||||
|
</vector>
|
||||||
170
android/app/src/main/res/drawable/ic_launcher_background.xml
Normal file
170
android/app/src/main/res/drawable/ic_launcher_background.xml
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportHeight="108"
|
||||||
|
android:viewportWidth="108">
|
||||||
|
<path
|
||||||
|
android:fillColor="#26A69A"
|
||||||
|
android:pathData="M0,0h108v108h-108z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M9,0L9,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,0L19,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M29,0L29,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M39,0L39,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M49,0L49,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M59,0L59,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M69,0L69,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M79,0L79,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M89,0L89,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M99,0L99,108"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,9L108,9"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,19L108,19"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,29L108,29"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,39L108,39"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,49L108,49"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,59L108,59"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,69L108,69"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,79L108,79"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,89L108,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,99L108,99"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,29L89,29"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,39L89,39"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,49L89,49"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,59L89,59"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,69L89,69"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,79L89,79"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M29,19L29,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M39,19L39,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M49,19L49,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M59,19L59,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M69,19L69,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M79,19L79,89"
|
||||||
|
android:strokeColor="#33FFFFFF"
|
||||||
|
android:strokeWidth="0.8" />
|
||||||
|
</vector>
|
||||||
12
android/app/src/main/res/layout/activity_main.xml
Normal file
12
android/app/src/main/res/layout/activity_main.xml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context=".MainActivity">
|
||||||
|
|
||||||
|
<WebView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background>
|
||||||
|
<inset android:drawable="@mipmap/ic_launcher_background" android:inset="16.7%" />
|
||||||
|
</background>
|
||||||
|
<foreground>
|
||||||
|
<inset android:drawable="@mipmap/ic_launcher_foreground" android:inset="16.7%" />
|
||||||
|
</foreground>
|
||||||
|
</adaptive-icon>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<background>
|
||||||
|
<inset android:drawable="@mipmap/ic_launcher_background" android:inset="16.7%" />
|
||||||
|
</background>
|
||||||
|
<foreground>
|
||||||
|
<inset android:drawable="@mipmap/ic_launcher_foreground" android:inset="16.7%" />
|
||||||
|
</foreground>
|
||||||
|
</adaptive-icon>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#FFFFFF</color>
|
||||||
|
</resources>
|
||||||
7
android/app/src/main/res/values/strings.xml
Normal file
7
android/app/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version='1.0' encoding='utf-8'?>
|
||||||
|
<resources>
|
||||||
|
<string name="app_name">Compass</string>
|
||||||
|
<string name="title_activity_main">Compass</string>
|
||||||
|
<string name="package_name">com.compassconnections.app</string>
|
||||||
|
<string name="custom_url_scheme">com.compassconnections.app</string>
|
||||||
|
</resources>
|
||||||
25
android/app/src/main/res/values/styles.xml
Normal file
25
android/app/src/main/res/values/styles.xml
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
|
||||||
|
<!-- Base application theme. -->
|
||||||
|
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
|
||||||
|
<!-- Customize your theme here. -->
|
||||||
|
<item name="colorPrimary">@color/colorPrimary</item>
|
||||||
|
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||||
|
<item name="colorAccent">@color/colorAccent</item>
|
||||||
|
<item name="android:windowTranslucentStatus">true</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||||
|
<item name="windowActionBar">false</item>
|
||||||
|
<item name="windowNoTitle">true</item>
|
||||||
|
<item name="android:background">@null</item>
|
||||||
|
<item name="android:windowTranslucentStatus">true</item>
|
||||||
|
<item name="android:windowTranslucentNavigation">true</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|
||||||
|
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
|
||||||
|
<item name="android:background">@drawable/splash</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
5
android/app/src/main/res/xml/file_paths.xml
Normal file
5
android/app/src/main/res/xml/file_paths.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<paths xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<external-path name="my_images" path="." />
|
||||||
|
<cache-path name="my_cache_images" path="." />
|
||||||
|
</paths>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.getcapacitor.myapp;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example local unit test, which will execute on the development machine (host).
|
||||||
|
*
|
||||||
|
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||||
|
*/
|
||||||
|
public class ExampleUnitTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void addition_isCorrect() throws Exception {
|
||||||
|
assertEquals(4, 2 + 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
29
android/build.gradle
Normal file
29
android/build.gradle
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath 'com.android.tools.build:gradle:8.13.0'
|
||||||
|
classpath 'com.google.gms:google-services:4.4.4'
|
||||||
|
|
||||||
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
|
// in the individual module build.gradle files
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "variables.gradle"
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
task clean(type: Delete) {
|
||||||
|
delete rootProject.buildDir
|
||||||
|
}
|
||||||
21
android/capacitor.settings.gradle
Normal file
21
android/capacitor.settings.gradle
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
|
||||||
|
include ':capacitor-android'
|
||||||
|
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
|
||||||
|
|
||||||
|
include ':capacitor-app'
|
||||||
|
project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android')
|
||||||
|
|
||||||
|
include ':capacitor-keyboard'
|
||||||
|
project(':capacitor-keyboard').projectDir = new File('../node_modules/@capacitor/keyboard/android')
|
||||||
|
|
||||||
|
include ':capacitor-push-notifications'
|
||||||
|
project(':capacitor-push-notifications').projectDir = new File('../node_modules/@capacitor/push-notifications/android')
|
||||||
|
|
||||||
|
include ':capacitor-status-bar'
|
||||||
|
project(':capacitor-status-bar').projectDir = new File('../node_modules/@capacitor/status-bar/android')
|
||||||
|
|
||||||
|
include ':capawesome-capacitor-live-update'
|
||||||
|
project(':capawesome-capacitor-live-update').projectDir = new File('../node_modules/@capawesome/capacitor-live-update/android')
|
||||||
|
|
||||||
|
include ':capgo-capacitor-social-login'
|
||||||
|
project(':capgo-capacitor-social-login').projectDir = new File('../node_modules/@capgo/capacitor-social-login/android')
|
||||||
22
android/gradle.properties
Normal file
22
android/gradle.properties
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# Project-wide Gradle settings.
|
||||||
|
|
||||||
|
# IDE (e.g. Android Studio) users:
|
||||||
|
# Gradle settings configured through the IDE *will override*
|
||||||
|
# any settings specified in this file.
|
||||||
|
|
||||||
|
# For more details on how to configure your build environment visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/build_environment.html
|
||||||
|
|
||||||
|
# Specifies the JVM arguments used for the daemon process.
|
||||||
|
# The setting is particularly useful for tweaking memory settings.
|
||||||
|
org.gradle.jvmargs=-Xmx1536m
|
||||||
|
|
||||||
|
# When configured, Gradle will run in incubating parallel mode.
|
||||||
|
# This option should only be used with decoupled projects. More details, visit
|
||||||
|
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||||
|
# org.gradle.parallel=true
|
||||||
|
|
||||||
|
# AndroidX package structure to make it clearer which packages are bundled with the
|
||||||
|
# Android operating system, and which are packaged with your app's APK
|
||||||
|
# https://developer.android.com/topic/libraries/support-library/androidx-rn
|
||||||
|
android.useAndroidX=true
|
||||||
BIN
android/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
android/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
7
android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
7
android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip
|
||||||
|
networkTimeout=10000
|
||||||
|
validateDistributionUrl=true
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user