mirror of
https://github.com/CompassConnections/Compass.git
synced 2026-04-04 14:53:33 -04:00
Compare commits
267 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -6,7 +6,7 @@ open_collective: compass-connection # Replace with a single Open Collective user
|
|||||||
ko_fi: compassconnections # Replace with a single Ko-fi 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
|
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
|
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||||
liberapay: # Replace with a single Liberapay username
|
liberapay: CompassConnections # Replace with a single Liberapay username
|
||||||
issuehunt: # Replace with a single IssueHunt username
|
issuehunt: # Replace with a single IssueHunt username
|
||||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||||
polar: # Replace with a single Polar username
|
polar: # Replace with a single Polar username
|
||||||
|
|||||||
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -48,13 +48,13 @@ jobs:
|
|||||||
- name: Run E2E tests
|
- name: Run E2E tests
|
||||||
env:
|
env:
|
||||||
NEXT_PUBLIC_API_URL: localhost:8088
|
NEXT_PUBLIC_API_URL: localhost:8088
|
||||||
NEXT_PUBLIC_FIREBASE_ENV: PROD
|
NEXT_PUBLIC_FIREBASE_ENV: DEV
|
||||||
NEXT_PUBLIC_FIREBASE_API_KEY: ${{ secrets.NEXT_PUBLIC_FIREBASE_API_KEY }}
|
NEXT_PUBLIC_FIREBASE_API_KEY: ${{ secrets.NEXT_PUBLIC_FIREBASE_API_KEY }}
|
||||||
NEXT_PUBLIC_SUPABASE_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_KEY }}
|
NEXT_PUBLIC_SUPABASE_KEY: ${{ secrets.NEXT_PUBLIC_SUPABASE_KEY }}
|
||||||
run: |
|
run: |
|
||||||
yarn --cwd=web serve &
|
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
|
||||||
|
|||||||
8
.gitignore
vendored
8
.gitignore
vendored
@@ -13,6 +13,9 @@
|
|||||||
# testing
|
# testing
|
||||||
/coverage
|
/coverage
|
||||||
|
|
||||||
|
# Playwright
|
||||||
|
/tests/reports/playwright-report
|
||||||
|
|
||||||
# next.js
|
# next.js
|
||||||
/.next/
|
/.next/
|
||||||
/out/
|
/out/
|
||||||
@@ -71,6 +74,7 @@ email-preview
|
|||||||
*.ico
|
*.ico
|
||||||
*.mp4
|
*.mp4
|
||||||
*.mov
|
*.mov
|
||||||
|
*.webp
|
||||||
*.avi
|
*.avi
|
||||||
*.wmv
|
*.wmv
|
||||||
*.mp3
|
*.mp3
|
||||||
@@ -87,3 +91,7 @@ email-preview
|
|||||||
*.terraform
|
*.terraform
|
||||||
/backups/firebase/auth/data/
|
/backups/firebase/auth/data/
|
||||||
/backups/firebase/storage/data/
|
/backups/firebase/storage/data/
|
||||||
|
|
||||||
|
android/app/release*
|
||||||
|
icons/
|
||||||
|
*.bak
|
||||||
|
|||||||
16
README.md
16
README.md
@@ -7,8 +7,6 @@
|
|||||||
|
|
||||||
This repository contains the source code for [Compass](https://compassmeet.com) — an open platform for forming deep, authentic 1-on-1 connections with clarity and efficiency.
|
This repository contains the source code for [Compass](https://compassmeet.com) — an open 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/support) in any way you can and help our community thrive!
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Extremely detailed profiles for deep connections
|
- Extremely detailed profiles for deep connections
|
||||||
@@ -21,9 +19,9 @@ This repository contains the source code for [Compass](https://compassmeet.com)
|
|||||||
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.
|
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).
|
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).
|
||||||
|
|
||||||
<p style="text-align: center;">
|
**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!
|
||||||
<img src="https://firebasestorage.googleapis.com/v0/b/compass-130ba.firebasestorage.app/o/misc%2Fdemo_compass.gif?alt=media&token=e3ae4334-4e3f-4026-b121-c08b4b724cd1" alt="Compass Demo" width="600">
|
|
||||||
</p>
|

|
||||||
|
|
||||||
## To Do
|
## To Do
|
||||||
|
|
||||||
@@ -73,14 +71,14 @@ Everything is open to anyone for collaboration, but the following ones are parti
|
|||||||
- [ ] Clean up terms and conditions (convert to Markdown)
|
- [ ] Clean up terms and conditions (convert to Markdown)
|
||||||
- [ ] Clean up privacy notice (convert to Markdown)
|
- [ ] Clean up privacy notice (convert to Markdown)
|
||||||
- [ ] Add other authentication methods (GitHub, Facebook, Apple, phone, etc.)
|
- [ ] Add other authentication methods (GitHub, Facebook, Apple, phone, etc.)
|
||||||
- [ ] Add email verification
|
- [x] Add email verification
|
||||||
- [ ] Add password reset
|
- [x] Add password reset
|
||||||
- [x] Add automated welcome email
|
- [x] Add automated welcome email
|
||||||
- [ ] Security audit and penetration testing
|
- [ ] Security audit and penetration testing
|
||||||
- [ ] Make `deploy-api.sh` run automatically on push to `main` branch
|
- [ ] Make `deploy-api.sh` run automatically on push to `main` branch
|
||||||
- [ ] Create settings page (change email, password, delete account, etc.)
|
- [x] Create settings page (change email, password, delete account, etc.)
|
||||||
- [ ] Improve [financials](web/public/md/financials.md) page (donor / acknowledgments, etc.)
|
- [ ] Improve [financials](web/public/md/financials.md) page (donor / acknowledgments, etc.)
|
||||||
- [ ] Improve loading sign (e.g., animation of a compass moving around)
|
- [x] Improve loading sign (e.g., animation of a compass moving around)
|
||||||
- [ ] Show compatibility score in profile page
|
- [ ] Show compatibility score in profile page
|
||||||
|
|
||||||
## Implementation
|
## Implementation
|
||||||
|
|||||||
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/
|
||||||
364
android/README.md
Normal file
364
android/README.md
Normal file
@@ -0,0 +1,364 @@
|
|||||||
|
# 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
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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 # 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. Note that right now you can't use a physical device for the local web version (`10.0.2.2:3000 time out` )
|
||||||
|
|
||||||
|
```
|
||||||
|
npx cap open android
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
## 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 # if you made changes to web app
|
||||||
|
npx cap sync android
|
||||||
|
# Run on emulator or device
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. Deployment Workflow
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Build web app for production
|
||||||
|
yarn build-web
|
||||||
|
|
||||||
|
# 2. Sync assets to Android
|
||||||
|
npx cap sync android
|
||||||
|
|
||||||
|
# 3. Build signed release APK in Android Studio
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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));
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
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 13
|
||||||
|
versionName "1.1.2"
|
||||||
|
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")
|
||||||
|
}
|
||||||
23
android/app/capacitor.build.gradle
Normal file
23
android/app/capacitor.build.gradle
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
// 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(':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
|
||||||
37
android/app/release/output-metadata.json
Normal file
37
android/app/release/output-metadata.json
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"version": 3,
|
||||||
|
"artifactType": {
|
||||||
|
"type": "APK",
|
||||||
|
"kind": "Directory"
|
||||||
|
},
|
||||||
|
"applicationId": "com.compassconnections.app",
|
||||||
|
"variantName": "release",
|
||||||
|
"elements": [
|
||||||
|
{
|
||||||
|
"type": "SINGLE",
|
||||||
|
"filters": [],
|
||||||
|
"attributes": [],
|
||||||
|
"versionCode": 13,
|
||||||
|
"versionName": "1.1.2",
|
||||||
|
"outputFile": "app-release.apk"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"elementType": "File",
|
||||||
|
"baselineProfiles": [
|
||||||
|
{
|
||||||
|
"minApi": 28,
|
||||||
|
"maxApi": 30,
|
||||||
|
"baselineProfiles": [
|
||||||
|
"baselineProfiles/1/app-release.dm"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"minApi": 31,
|
||||||
|
"maxApi": 2147483647,
|
||||||
|
"baselineProfiles": [
|
||||||
|
"baselineProfiles/0/app-release.dm"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"minSdkVersionForDexing": 23
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
18
android/capacitor.settings.gradle
Normal file
18
android/capacitor.settings.gradle
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
// 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 ':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
|
||||||
252
android/gradlew
vendored
Executable file
252
android/gradlew
vendored
Executable file
@@ -0,0 +1,252 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
#
|
||||||
|
# Copyright © 2015-2021 the original authors.
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
# you may not use this file except in compliance with the License.
|
||||||
|
# You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
# See the License for the specific language governing permissions and
|
||||||
|
# limitations under the License.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# Gradle start up script for POSIX generated by Gradle.
|
||||||
|
#
|
||||||
|
# Important for running:
|
||||||
|
#
|
||||||
|
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
|
||||||
|
# noncompliant, but you have some other compliant shell such as ksh or
|
||||||
|
# bash, then to run this script, type that shell name before the whole
|
||||||
|
# command line, like:
|
||||||
|
#
|
||||||
|
# ksh Gradle
|
||||||
|
#
|
||||||
|
# Busybox and similar reduced shells will NOT work, because this script
|
||||||
|
# requires all of these POSIX shell features:
|
||||||
|
# * functions;
|
||||||
|
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
|
||||||
|
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
|
||||||
|
# * compound commands having a testable exit status, especially «case»;
|
||||||
|
# * various built-in commands including «command», «set», and «ulimit».
|
||||||
|
#
|
||||||
|
# Important for patching:
|
||||||
|
#
|
||||||
|
# (2) This script targets any POSIX shell, so it avoids extensions provided
|
||||||
|
# by Bash, Ksh, etc; in particular arrays are avoided.
|
||||||
|
#
|
||||||
|
# The "traditional" practice of packing multiple parameters into a
|
||||||
|
# space-separated string is a well documented source of bugs and security
|
||||||
|
# problems, so this is (mostly) avoided, by progressively accumulating
|
||||||
|
# options in "$@", and eventually passing that to Java.
|
||||||
|
#
|
||||||
|
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
|
||||||
|
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
|
||||||
|
# see the in-line comments for details.
|
||||||
|
#
|
||||||
|
# There are tweaks for specific operating systems such as AIX, CygWin,
|
||||||
|
# Darwin, MinGW, and NonStop.
|
||||||
|
#
|
||||||
|
# (3) This script is generated from the Groovy template
|
||||||
|
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
|
# within the Gradle project.
|
||||||
|
#
|
||||||
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
|
#
|
||||||
|
##############################################################################
|
||||||
|
|
||||||
|
# Attempt to set APP_HOME
|
||||||
|
|
||||||
|
# Resolve links: $0 may be a link
|
||||||
|
app_path=$0
|
||||||
|
|
||||||
|
# Need this for daisy-chained symlinks.
|
||||||
|
while
|
||||||
|
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||||
|
[ -h "$app_path" ]
|
||||||
|
do
|
||||||
|
ls=$( ls -ld "$app_path" )
|
||||||
|
link=${ls#*' -> '}
|
||||||
|
case $link in #(
|
||||||
|
/*) app_path=$link ;; #(
|
||||||
|
*) app_path=$APP_HOME$link ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# This is normally unused
|
||||||
|
# shellcheck disable=SC2034
|
||||||
|
APP_BASE_NAME=${0##*/}
|
||||||
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
|
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
|
||||||
|
' "$PWD" ) || exit
|
||||||
|
|
||||||
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
|
MAX_FD=maximum
|
||||||
|
|
||||||
|
warn () {
|
||||||
|
echo "$*"
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
die () {
|
||||||
|
echo
|
||||||
|
echo "$*"
|
||||||
|
echo
|
||||||
|
exit 1
|
||||||
|
} >&2
|
||||||
|
|
||||||
|
# OS specific support (must be 'true' or 'false').
|
||||||
|
cygwin=false
|
||||||
|
msys=false
|
||||||
|
darwin=false
|
||||||
|
nonstop=false
|
||||||
|
case "$( uname )" in #(
|
||||||
|
CYGWIN* ) cygwin=true ;; #(
|
||||||
|
Darwin* ) darwin=true ;; #(
|
||||||
|
MSYS* | MINGW* ) msys=true ;; #(
|
||||||
|
NONSTOP* ) nonstop=true ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
# Determine the Java command to use to start the JVM.
|
||||||
|
if [ -n "$JAVA_HOME" ] ; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||||
|
else
|
||||||
|
JAVACMD=$JAVA_HOME/bin/java
|
||||||
|
fi
|
||||||
|
if [ ! -x "$JAVACMD" ] ; then
|
||||||
|
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD=java
|
||||||
|
if ! command -v java >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
|
location of your Java installation."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Increase the maximum file descriptors if we can.
|
||||||
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
|
case $MAX_FD in #(
|
||||||
|
max*)
|
||||||
|
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
|
warn "Could not query maximum file descriptor limit"
|
||||||
|
esac
|
||||||
|
case $MAX_FD in #(
|
||||||
|
'' | soft) :;; #(
|
||||||
|
*)
|
||||||
|
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
|
ulimit -n "$MAX_FD" ||
|
||||||
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Collect all arguments for the java command, stacking in reverse order:
|
||||||
|
# * args from the command line
|
||||||
|
# * the main class name
|
||||||
|
# * -classpath
|
||||||
|
# * -D...appname settings
|
||||||
|
# * --module-path (only if needed)
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
|
||||||
|
|
||||||
|
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||||
|
if "$cygwin" || "$msys" ; then
|
||||||
|
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
|
||||||
|
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
|
||||||
|
|
||||||
|
JAVACMD=$( cygpath --unix "$JAVACMD" )
|
||||||
|
|
||||||
|
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||||
|
for arg do
|
||||||
|
if
|
||||||
|
case $arg in #(
|
||||||
|
-*) false ;; # don't mess with options #(
|
||||||
|
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
|
||||||
|
[ -e "$t" ] ;; #(
|
||||||
|
*) false ;;
|
||||||
|
esac
|
||||||
|
then
|
||||||
|
arg=$( cygpath --path --ignore --mixed "$arg" )
|
||||||
|
fi
|
||||||
|
# Roll the args list around exactly as many times as the number of
|
||||||
|
# args, so each arg winds up back in the position where it started, but
|
||||||
|
# possibly modified.
|
||||||
|
#
|
||||||
|
# NB: a `for` loop captures its iteration list before it begins, so
|
||||||
|
# changing the positional parameters here affects neither the number of
|
||||||
|
# iterations, nor the values presented in `arg`.
|
||||||
|
shift # remove old arg
|
||||||
|
set -- "$@" "$arg" # push replacement arg
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
|
# Collect all arguments for the java command:
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||||
|
# and any embedded shellness will be escaped.
|
||||||
|
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||||
|
# treated as '${Hostname}' itself on the command line.
|
||||||
|
|
||||||
|
set -- \
|
||||||
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
-classpath "$CLASSPATH" \
|
||||||
|
org.gradle.wrapper.GradleWrapperMain \
|
||||||
|
"$@"
|
||||||
|
|
||||||
|
# Stop when "xargs" is not available.
|
||||||
|
if ! command -v xargs >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "xargs is not available"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Use "xargs" to parse quoted args.
|
||||||
|
#
|
||||||
|
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
|
||||||
|
#
|
||||||
|
# In Bash we could simply go:
|
||||||
|
#
|
||||||
|
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
|
||||||
|
# set -- "${ARGS[@]}" "$@"
|
||||||
|
#
|
||||||
|
# but POSIX shell has neither arrays nor command substitution, so instead we
|
||||||
|
# post-process each arg (as a line of input to sed) to backslash-escape any
|
||||||
|
# character that might be a shell metacharacter, then use eval to reverse
|
||||||
|
# that process (while maintaining the separation between arguments), and wrap
|
||||||
|
# the whole thing up as a single "set" statement.
|
||||||
|
#
|
||||||
|
# This will of course break if any of these variables contains a newline or
|
||||||
|
# an unmatched quote.
|
||||||
|
#
|
||||||
|
|
||||||
|
eval "set -- $(
|
||||||
|
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
|
||||||
|
xargs -n1 |
|
||||||
|
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
|
||||||
|
tr '\n' ' '
|
||||||
|
)" '"$@"'
|
||||||
|
|
||||||
|
exec "$JAVACMD" "$@"
|
||||||
94
android/gradlew.bat
vendored
Normal file
94
android/gradlew.bat
vendored
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
@rem
|
||||||
|
@rem Copyright 2015 the original author or authors.
|
||||||
|
@rem
|
||||||
|
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
@rem you may not use this file except in compliance with the License.
|
||||||
|
@rem You may obtain a copy of the License at
|
||||||
|
@rem
|
||||||
|
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@rem
|
||||||
|
@rem Unless required by applicable law or agreed to in writing, software
|
||||||
|
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
@rem See the License for the specific language governing permissions and
|
||||||
|
@rem limitations under the License.
|
||||||
|
@rem
|
||||||
|
@rem SPDX-License-Identifier: Apache-2.0
|
||||||
|
@rem
|
||||||
|
|
||||||
|
@if "%DEBUG%"=="" @echo off
|
||||||
|
@rem ##########################################################################
|
||||||
|
@rem
|
||||||
|
@rem Gradle startup script for Windows
|
||||||
|
@rem
|
||||||
|
@rem ##########################################################################
|
||||||
|
|
||||||
|
@rem Set local scope for the variables with windows NT shell
|
||||||
|
if "%OS%"=="Windows_NT" setlocal
|
||||||
|
|
||||||
|
set DIRNAME=%~dp0
|
||||||
|
if "%DIRNAME%"=="" set DIRNAME=.
|
||||||
|
@rem This is normally unused
|
||||||
|
set APP_BASE_NAME=%~n0
|
||||||
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
|
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||||
|
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||||
|
|
||||||
|
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
|
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||||
|
|
||||||
|
@rem Find java.exe
|
||||||
|
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||||
|
|
||||||
|
set JAVA_EXE=java.exe
|
||||||
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
|
if %ERRORLEVEL% equ 0 goto execute
|
||||||
|
|
||||||
|
echo. 1>&2
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||||
|
echo. 1>&2
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
|
echo. 1>&2
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||||
|
echo. 1>&2
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||||
|
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||||
|
|
||||||
|
:end
|
||||||
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
if %ERRORLEVEL% equ 0 goto mainEnd
|
||||||
|
|
||||||
|
:fail
|
||||||
|
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||||
|
rem the _cmd.exe /c_ return code!
|
||||||
|
set EXIT_CODE=%ERRORLEVEL%
|
||||||
|
if %EXIT_CODE% equ 0 set EXIT_CODE=1
|
||||||
|
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
|
||||||
|
exit /b %EXIT_CODE%
|
||||||
|
|
||||||
|
:mainEnd
|
||||||
|
if "%OS%"=="Windows_NT" endlocal
|
||||||
|
|
||||||
|
:omega
|
||||||
5
android/settings.gradle
Normal file
5
android/settings.gradle
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
include ':app'
|
||||||
|
include ':capacitor-cordova-android-plugins'
|
||||||
|
project(':capacitor-cordova-android-plugins').projectDir = new File('./capacitor-cordova-android-plugins/')
|
||||||
|
|
||||||
|
apply from: 'capacitor.settings.gradle'
|
||||||
16
android/variables.gradle
Normal file
16
android/variables.gradle
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
ext {
|
||||||
|
minSdkVersion = 23
|
||||||
|
compileSdkVersion = 35
|
||||||
|
targetSdkVersion = 35
|
||||||
|
androidxActivityVersion = '1.9.2'
|
||||||
|
androidxAppCompatVersion = '1.7.0'
|
||||||
|
androidxCoordinatorLayoutVersion = '1.2.0'
|
||||||
|
androidxCoreVersion = '1.15.0'
|
||||||
|
androidxFragmentVersion = '1.8.4'
|
||||||
|
coreSplashScreenVersion = '1.0.1'
|
||||||
|
androidxWebkitVersion = '1.12.1'
|
||||||
|
junitVersion = '4.13.2'
|
||||||
|
androidxJunitVersion = '1.2.1'
|
||||||
|
androidxEspressoCoreVersion = '3.6.1'
|
||||||
|
cordovaAndroidVersion = '10.1.1'
|
||||||
|
}
|
||||||
@@ -11,11 +11,11 @@
|
|||||||
"build": "yarn compile && yarn dist:clean && yarn dist:copy",
|
"build": "yarn compile && yarn dist:clean && yarn dist:copy",
|
||||||
"build:fast": "yarn compile && yarn dist:copy",
|
"build:fast": "yarn compile && yarn dist:copy",
|
||||||
"clean": "rm -rf lib && (cd ../../common && rm -rf lib) && (cd ../shared && rm -rf lib) && (cd ../email && rm -rf lib)",
|
"clean": "rm -rf lib && (cd ../../common && rm -rf lib) && (cd ../shared && rm -rf lib) && (cd ../email && rm -rf lib)",
|
||||||
"compile": "tsc -b && tsc-alias && (cd ../../common && tsc-alias) && (cd ../shared && tsc-alias) && (cd ../email && tsc-alias)",
|
"compile": "tsc -b && tsc-alias && (cd ../../common && tsc-alias) && (cd ../shared && tsc-alias) && (cd ../email && tsc-alias) && cp -r src/public/ lib/",
|
||||||
"debug": "nodemon -r tsconfig-paths/register --watch src -e ts --watch ../../common/src --watch ../shared/src --exec \"yarn build && node --inspect-brk src/serve.ts\"",
|
"debug": "nodemon -r tsconfig-paths/register --watch src -e ts --watch ../../common/src --watch ../shared/src --exec \"yarn build && node --inspect-brk src/serve.ts\"",
|
||||||
"dist": "yarn dist:clean && yarn dist:copy",
|
"dist": "yarn dist:clean && yarn dist:copy",
|
||||||
"dist:clean": "rm -rf dist && mkdir -p dist/common/lib dist/backend/shared/lib dist/backend/api/lib dist/backend/email/lib",
|
"dist:clean": "rm -rf dist && mkdir -p dist/common/lib dist/backend/shared/lib dist/backend/api/lib dist/backend/email/lib",
|
||||||
"dist:copy": "rsync -a --delete ../../common/lib/ dist/common/lib && rsync -a --delete ../shared/lib/ dist/backend/shared/lib && rsync -a --delete ../email/lib/ dist/backend/email/lib && rsync -a --delete ./lib/* dist/backend/api/lib && cp ../../yarn.lock dist && cp package.json dist && cp package.json dist/backend/api",
|
"dist:copy": "rsync -a --delete ../../common/lib/ dist/common/lib && rsync -a --delete ../shared/lib/ dist/backend/shared/lib && rsync -a --delete ../email/lib/ dist/backend/email/lib && rsync -a --delete ./lib/* dist/backend/api/lib && cp ../../yarn.lock dist && cp package.json dist && cp package.json dist/backend/api",
|
||||||
"watch": "tsc -w",
|
"watch": "tsc -w",
|
||||||
"verify": "yarn --cwd=../.. verify",
|
"verify": "yarn --cwd=../.. verify",
|
||||||
"verify:dir": "npx eslint . --max-warnings 0",
|
"verify:dir": "npx eslint . --max-warnings 0",
|
||||||
|
|||||||
@@ -65,6 +65,13 @@ import {OpenAPIV3} from 'openapi-types';
|
|||||||
import {version as pkgVersion} from './../package.json'
|
import {version as pkgVersion} from './../package.json'
|
||||||
import {z, ZodFirstPartyTypeKind, ZodTypeAny} from "zod";
|
import {z, ZodFirstPartyTypeKind, ZodTypeAny} from "zod";
|
||||||
import {getUser} from "api/get-user";
|
import {getUser} from "api/get-user";
|
||||||
|
import {localSendTestEmail} from "api/test";
|
||||||
|
import path from "node:path";
|
||||||
|
import {saveSubscriptionMobile} from "api/save-subscription-mobile";
|
||||||
|
import {IS_LOCAL} from "common/hosting/constants";
|
||||||
|
import {editMessage} from "api/edit-message";
|
||||||
|
import {reactToMessage} from "api/react-to-message";
|
||||||
|
import {deleteMessage} from "api/delete-message";
|
||||||
|
|
||||||
// const corsOptions: CorsOptions = {
|
// const corsOptions: CorsOptions = {
|
||||||
// origin: ['*'], // Only allow requests from this domain
|
// origin: ['*'], // Only allow requests from this domain
|
||||||
@@ -119,12 +126,10 @@ const apiErrorHandler: ErrorRequestHandler = (error, _req, res, _next) => {
|
|||||||
export const app = express()
|
export const app = express()
|
||||||
app.use(requestMonitoring)
|
app.use(requestMonitoring)
|
||||||
|
|
||||||
|
|
||||||
const schemaCache = new WeakMap<ZodTypeAny, any>();
|
const schemaCache = new WeakMap<ZodTypeAny, any>();
|
||||||
|
|
||||||
export function zodToOpenApiSchema(
|
export function zodToOpenApiSchema(zodObj: ZodTypeAny,): any {
|
||||||
zodObj: ZodTypeAny,
|
|
||||||
nameHint?: string
|
|
||||||
): any { // Prevent infinite recursion
|
|
||||||
if (schemaCache.has(zodObj)) {
|
if (schemaCache.has(zodObj)) {
|
||||||
return schemaCache.get(zodObj);
|
return schemaCache.get(zodObj);
|
||||||
}
|
}
|
||||||
@@ -140,19 +145,19 @@ export function zodToOpenApiSchema(
|
|||||||
|
|
||||||
switch (typeName) {
|
switch (typeName) {
|
||||||
case 'ZodString':
|
case 'ZodString':
|
||||||
schema = { type: 'string' };
|
schema = {type: 'string'};
|
||||||
break;
|
break;
|
||||||
case 'ZodNumber':
|
case 'ZodNumber':
|
||||||
schema = { type: 'number' };
|
schema = {type: 'number'};
|
||||||
break;
|
break;
|
||||||
case 'ZodBoolean':
|
case 'ZodBoolean':
|
||||||
schema = { type: 'boolean' };
|
schema = {type: 'boolean'};
|
||||||
break;
|
break;
|
||||||
case 'ZodEnum':
|
case 'ZodEnum':
|
||||||
schema = { type: 'string', enum: def.values };
|
schema = {type: 'string', enum: def.values};
|
||||||
break;
|
break;
|
||||||
case 'ZodArray':
|
case 'ZodArray':
|
||||||
schema = { type: 'array', items: zodToOpenApiSchema(def.type) };
|
schema = {type: 'array', items: zodToOpenApiSchema(def.type)};
|
||||||
break;
|
break;
|
||||||
case 'ZodObject': {
|
case 'ZodObject': {
|
||||||
const shape = def.shape();
|
const shape = def.shape();
|
||||||
@@ -161,14 +166,14 @@ export function zodToOpenApiSchema(
|
|||||||
|
|
||||||
for (const key in shape) {
|
for (const key in shape) {
|
||||||
const child = shape[key];
|
const child = shape[key];
|
||||||
properties[key] = zodToOpenApiSchema(child, key);
|
properties[key] = zodToOpenApiSchema(child);
|
||||||
if (!child.isOptional()) required.push(key);
|
if (!child.isOptional()) required.push(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
schema = {
|
schema = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties,
|
properties,
|
||||||
...(required.length ? { required } : {}),
|
...(required.length ? {required} : {}),
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -181,14 +186,11 @@ export function zodToOpenApiSchema(
|
|||||||
case 'ZodIntersection': {
|
case 'ZodIntersection': {
|
||||||
const left = zodToOpenApiSchema(def.left);
|
const left = zodToOpenApiSchema(def.left);
|
||||||
const right = zodToOpenApiSchema(def.right);
|
const right = zodToOpenApiSchema(def.right);
|
||||||
schema = { allOf: [left, right] };
|
schema = {allOf: [left, right]};
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'ZodLazy':
|
case 'ZodLazy':
|
||||||
// Recursive schema: use a $ref placeholder name
|
schema = {type: 'object', description: 'Lazy schema - details omitted'};
|
||||||
schema = {
|
|
||||||
$ref: `#/components/schemas/${nameHint ?? 'RecursiveType'}`,
|
|
||||||
};
|
|
||||||
break;
|
break;
|
||||||
case 'ZodUnion':
|
case 'ZodUnion':
|
||||||
schema = {
|
schema = {
|
||||||
@@ -196,7 +198,7 @@ export function zodToOpenApiSchema(
|
|||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
schema = { type: 'string' }; // fallback for unhandled
|
schema = {type: 'string'}; // fallback for unhandled
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.assign(placeholder, schema);
|
Object.assign(placeholder, schema);
|
||||||
@@ -291,15 +293,15 @@ const swaggerDocument: OpenAPIV3.Document = {
|
|||||||
scheme: 'bearer',
|
scheme: 'bearer',
|
||||||
bearerFormat: 'JWT',
|
bearerFormat: 'JWT',
|
||||||
},
|
},
|
||||||
|
ApiKeyAuth: {
|
||||||
|
type: 'apiKey',
|
||||||
|
in: 'header',
|
||||||
|
name: 'x-api-key',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
} as OpenAPIV3.Document;
|
} as OpenAPIV3.Document;
|
||||||
|
|
||||||
|
|
||||||
const rootPath = pathWithPrefix("/")
|
|
||||||
app.get(rootPath, swaggerUi.setup(swaggerDocument))
|
|
||||||
app.use(rootPath, swaggerUi.serve)
|
|
||||||
|
|
||||||
// Triggers Missing parameter name at index 3: *; visit https://git.new/pathToRegexpError for info
|
// Triggers Missing parameter name at index 3: *; visit https://git.new/pathToRegexpError for info
|
||||||
// May not be necessary
|
// May not be necessary
|
||||||
// app.options('*', allowCorsUnrestricted)
|
// app.options('*', allowCorsUnrestricted)
|
||||||
@@ -356,8 +358,13 @@ const handlers: { [k in APIPath]: APIHandler<k> } = {
|
|||||||
'get-messages-count': getMessagesCount,
|
'get-messages-count': getMessagesCount,
|
||||||
'set-last-online-time': setLastOnlineTime,
|
'set-last-online-time': setLastOnlineTime,
|
||||||
'save-subscription': saveSubscription,
|
'save-subscription': saveSubscription,
|
||||||
|
'save-subscription-mobile': saveSubscriptionMobile,
|
||||||
'create-bookmarked-search': createBookmarkedSearch,
|
'create-bookmarked-search': createBookmarkedSearch,
|
||||||
'delete-bookmarked-search': deleteBookmarkedSearch,
|
'delete-bookmarked-search': deleteBookmarkedSearch,
|
||||||
|
'delete-message': deleteMessage,
|
||||||
|
'edit-message': editMessage,
|
||||||
|
'react-to-message': reactToMessage,
|
||||||
|
// 'auth-google': authGoogle,
|
||||||
}
|
}
|
||||||
|
|
||||||
Object.entries(handlers).forEach(([path, handler]) => {
|
Object.entries(handlers).forEach(([path, handler]) => {
|
||||||
@@ -407,6 +414,108 @@ app.post(pathWithPrefix("/internal/send-search-notifications"),
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const responses = {
|
||||||
|
200: {
|
||||||
|
description: "Request successful",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
status: {type: "string", example: "success"}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
401: {
|
||||||
|
description: "Unauthorized (e.g., invalid or missing API key)",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
error: {type: "string", example: "Unauthorized"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
500: {
|
||||||
|
description: "Internal server error during request processing",
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
error: {type: "string", example: "Internal server error"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
swaggerDocument.paths["/internal/send-search-notifications"] = {
|
||||||
|
post: {
|
||||||
|
summary: "Trigger daily search notifications",
|
||||||
|
description:
|
||||||
|
"Internal endpoint used by Compass schedulers to send daily notifications for bookmarked searches. Requires a valid `x-api-key` header.",
|
||||||
|
tags: ["Internal"],
|
||||||
|
security: [
|
||||||
|
{
|
||||||
|
ApiKeyAuth: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
requestBody: {
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
responses: responses,
|
||||||
|
},
|
||||||
|
} as any
|
||||||
|
|
||||||
|
|
||||||
|
// Local Endpoints
|
||||||
|
if (IS_LOCAL) {
|
||||||
|
app.post(pathWithPrefix("/local/send-test-email"),
|
||||||
|
async (req, res) => {
|
||||||
|
if (!IS_LOCAL) {
|
||||||
|
return res.status(401).json({error: "Unauthorized"});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await localSendTestEmail()
|
||||||
|
return res.status(200).json(result)
|
||||||
|
} catch (err) {
|
||||||
|
return res.status(500).json({error: err});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
swaggerDocument.paths["/local/send-test-email"] = {
|
||||||
|
post: {
|
||||||
|
summary: "Send a test email",
|
||||||
|
description: "Local endpoint to send a test email.",
|
||||||
|
tags: ["Local"],
|
||||||
|
requestBody: {
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
responses: responses,
|
||||||
|
},
|
||||||
|
} as any
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const rootPath = pathWithPrefix("/")
|
||||||
|
app.get(
|
||||||
|
rootPath,
|
||||||
|
swaggerUi.setup(swaggerDocument, {
|
||||||
|
customSiteTitle: 'Compass API Docs',
|
||||||
|
customCssUrl: '/swagger.css',
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
app.use(rootPath, swaggerUi.serve)
|
||||||
|
|
||||||
|
app.use(express.static(path.join(__dirname, 'public')));
|
||||||
|
|
||||||
app.use(allowCorsUnrestricted, (req, res) => {
|
app.use(allowCorsUnrestricted, (req, res) => {
|
||||||
if (req.method === 'OPTIONS') {
|
if (req.method === 'OPTIONS') {
|
||||||
|
|||||||
37
backend/api/src/auth-google.ts
Normal file
37
backend/api/src/auth-google.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
// import {APIError, APIHandler} from './helpers/endpoint'
|
||||||
|
// import {GOOGLE_CLIENT_ID} from "common/constants";
|
||||||
|
// import {REDIRECT_URI} from "common/envs/constants";
|
||||||
|
//
|
||||||
|
// export const authGoogle: APIHandler<'auth-google'> = async (
|
||||||
|
// {code},
|
||||||
|
// _auth
|
||||||
|
// ) => {
|
||||||
|
// console.log('Google Auth Codes:', code)
|
||||||
|
// if (!code) return {success: false, result: {}}
|
||||||
|
//
|
||||||
|
// const body = {
|
||||||
|
// client_id: GOOGLE_CLIENT_ID,
|
||||||
|
// client_secret: process.env.GOOGLE_CLIENT_SECRET!,
|
||||||
|
// code: code as string,
|
||||||
|
// grant_type: 'authorization_code',
|
||||||
|
// redirect_uri: REDIRECT_URI,
|
||||||
|
// };
|
||||||
|
// console.log('Body:', body)
|
||||||
|
// const tokenRes = await fetch('https://oauth2.googleapis.com/token', {
|
||||||
|
// method: 'POST',
|
||||||
|
// headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||||
|
// body: new URLSearchParams(body),
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// const tokens = await tokenRes.json();
|
||||||
|
// if (tokens.error) {
|
||||||
|
// console.error('Google token error:', tokens);
|
||||||
|
// throw new APIError(400, 'Google token error: ' + JSON.stringify(tokens))
|
||||||
|
// }
|
||||||
|
// console.log('Google Tokens:', tokens);
|
||||||
|
//
|
||||||
|
// return {
|
||||||
|
// success: true,
|
||||||
|
// result: {tokens},
|
||||||
|
// }
|
||||||
|
// }
|
||||||
@@ -4,6 +4,24 @@ import {insertNotificationToSupabase} from 'shared/supabase/notifications'
|
|||||||
import {tryCatch} from "common/util/try-catch";
|
import {tryCatch} from "common/util/try-catch";
|
||||||
import {Row} from "common/supabase/utils";
|
import {Row} from "common/supabase/utils";
|
||||||
|
|
||||||
|
export const createAndroidTestNotifications = async () => {
|
||||||
|
const createdTime = Date.now();
|
||||||
|
const id = `android-test-${createdTime}`
|
||||||
|
const notification: Notification = {
|
||||||
|
id,
|
||||||
|
userId: 'todo',
|
||||||
|
createdTime: createdTime,
|
||||||
|
isSeen: false,
|
||||||
|
sourceType: 'info',
|
||||||
|
sourceUpdateType: 'created',
|
||||||
|
sourceSlug: '/contact',
|
||||||
|
sourceUserAvatarUrl: 'https://firebasestorage.googleapis.com/v0/b/compass-130ba.firebasestorage.app/o/misc%2Fcompass-192.png?alt=media&token=9fd251c5-fc43-4375-b629-1a8f4bbe8185',
|
||||||
|
title: 'Android App Ready for Review — Help Us Unlock the Google Play Release',
|
||||||
|
sourceText: 'To release our app, Google requires a closed test with at least 12 testers for 14 days. Please share your Google Play–registered email address so we can add you as a tester and complete the review process.',
|
||||||
|
}
|
||||||
|
return await createNotifications(notification)
|
||||||
|
}
|
||||||
|
|
||||||
export const createShareNotifications = async () => {
|
export const createShareNotifications = async () => {
|
||||||
const createdTime = Date.now();
|
const createdTime = Date.now();
|
||||||
const id = `share-${createdTime}`
|
const id = `share-${createdTime}`
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { APIError, APIHandler } from 'api/helpers/endpoint'
|
import { APIError, APIHandler } from 'api/helpers/endpoint'
|
||||||
import { createSupabaseDirectClient } from 'shared/supabase/init'
|
import { createSupabaseDirectClient } from 'shared/supabase/init'
|
||||||
import { log, getUser } from 'shared/utils'
|
import { log, getUser } from 'shared/utils'
|
||||||
import { HOUR_MS } from 'common/util/time'
|
import {HOUR_MS, MINUTE_MS, sleep} from 'common/util/time'
|
||||||
import { removePinnedUrlFromPhotoUrls } from 'shared/profiles/parse-photos'
|
import { removePinnedUrlFromPhotoUrls } from 'shared/profiles/parse-photos'
|
||||||
import { track } from 'shared/analytics'
|
import { track } from 'shared/analytics'
|
||||||
import { updateUser } from 'shared/supabase/users'
|
import { updateUser } from 'shared/supabase/users'
|
||||||
@@ -41,7 +41,7 @@ export const createProfile: APIHandler<'create-profile'> = async (body, auth) =>
|
|||||||
throw new APIError(500, 'Error creating user')
|
throw new APIError(500, 'Error creating user')
|
||||||
}
|
}
|
||||||
|
|
||||||
log('Created user', data)
|
log('Created profile', data)
|
||||||
|
|
||||||
const continuation = async () => {
|
const continuation = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -50,6 +50,10 @@ export const createProfile: APIHandler<'create-profile'> = async (body, auth) =>
|
|||||||
console.error('Failed to track create profile', e)
|
console.error('Failed to track create profile', e)
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
// Let the user fill in the optional form with all their info and pictures before notifying discord of their arrival.
|
||||||
|
// So we can sse their full profile as soon as we get the notif on discord. And that allows OG to pull their pic for the link preview.
|
||||||
|
// Regardless, you need to wait for at least 5 seconds that the profile is fully in the db—otherwise ISR may cache "profile not created yet"
|
||||||
|
await sleep(10 * MINUTE_MS)
|
||||||
let message: string = `[**${user.name}**](https://www.compassmeet.com/${user.username}) just created a profile`
|
let message: string = `[**${user.name}**](https://www.compassmeet.com/${user.username}) just created a profile`
|
||||||
if (body.bio) {
|
if (body.bio) {
|
||||||
const bioText = jsonToMarkdown(body.bio)
|
const bioText = jsonToMarkdown(body.bio)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {APIError, APIHandler} from './helpers/endpoint'
|
|||||||
import {getDefaultNotificationPreferences} from 'common/user-notification-preferences'
|
import {getDefaultNotificationPreferences} from 'common/user-notification-preferences'
|
||||||
import {removeUndefinedProps} from 'common/util/object'
|
import {removeUndefinedProps} from 'common/util/object'
|
||||||
import {generateAvatarUrl} from 'shared/helpers/generate-and-update-avatar-urls'
|
import {generateAvatarUrl} from 'shared/helpers/generate-and-update-avatar-urls'
|
||||||
import {IS_LOCAL, RESERVED_PATHS} from 'common/envs/constants'
|
import {RESERVED_PATHS} from 'common/envs/constants'
|
||||||
import {getUser, getUserByUsername, log} from 'shared/utils'
|
import {getUser, getUserByUsername, log} from 'shared/utils'
|
||||||
import {createSupabaseDirectClient} from 'shared/supabase/init'
|
import {createSupabaseDirectClient} from 'shared/supabase/init'
|
||||||
import {insert} from 'shared/supabase/utils'
|
import {insert} from 'shared/supabase/utils'
|
||||||
@@ -15,6 +15,7 @@ import {convertPrivateUser, convertUser} from 'common/supabase/users'
|
|||||||
import {getBucket} from "shared/firebase-utils";
|
import {getBucket} from "shared/firebase-utils";
|
||||||
import {sendWelcomeEmail} from "email/functions/helpers";
|
import {sendWelcomeEmail} from "email/functions/helpers";
|
||||||
import {setLastOnlineTimeUser} from "api/set-last-online-time";
|
import {setLastOnlineTimeUser} from "api/set-last-online-time";
|
||||||
|
import {IS_LOCAL} from "common/hosting/constants";
|
||||||
|
|
||||||
export const createUser: APIHandler<'create-user'> = async (
|
export const createUser: APIHandler<'create-user'> = async (
|
||||||
props,
|
props,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export const createVote: APIHandler<
|
|||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
is_anonymous: isAnonymous,
|
is_anonymous: isAnonymous,
|
||||||
|
status: 'voting_open',
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -4,18 +4,11 @@ import {createSupabaseDirectClient} from 'shared/supabase/init'
|
|||||||
import * as admin from "firebase-admin";
|
import * as admin from "firebase-admin";
|
||||||
import {deleteUserFiles} from "shared/firebase-utils";
|
import {deleteUserFiles} from "shared/firebase-utils";
|
||||||
|
|
||||||
export const deleteMe: APIHandler<'me/delete'> = async (body, auth) => {
|
export const deleteMe: APIHandler<'me/delete'> = async (_, auth) => {
|
||||||
const {username} = body
|
|
||||||
const user = await getUser(auth.uid)
|
const user = await getUser(auth.uid)
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new APIError(401, 'Your account was not found')
|
throw new APIError(401, 'Your account was not found')
|
||||||
}
|
}
|
||||||
if (user.username != username) {
|
|
||||||
throw new APIError(
|
|
||||||
400,
|
|
||||||
`Incorrect username. You are logged in as ${user.username}. Are you sure you want to delete this account?`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const userId = user.id
|
const userId = user.id
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
throw new APIError(400, 'Invalid user ID')
|
throw new APIError(400, 'Invalid user ID')
|
||||||
@@ -25,11 +18,6 @@ export const deleteMe: APIHandler<'me/delete'> = async (body, auth) => {
|
|||||||
const pg = createSupabaseDirectClient()
|
const pg = createSupabaseDirectClient()
|
||||||
await pg.none('DELETE FROM users WHERE id = $1', [userId])
|
await pg.none('DELETE FROM users WHERE id = $1', [userId])
|
||||||
// Should cascade delete in other tables
|
// Should cascade delete in other tables
|
||||||
// await pg.none('DELETE FROM private_users WHERE id = $1', [userId])
|
|
||||||
// await pg.none('DELETE FROM profiles WHERE user_id = $1', [userId])
|
|
||||||
// await pg.none('DELETE FROM bookmarked_searches WHERE creator_id = $1', [userId])
|
|
||||||
// await pg.none('DELETE FROM compatibility_answers WHERE creator_id = $1', [userId])
|
|
||||||
// May need to also delete from other tables in the future (such as messages, compatibility responses, etc.)
|
|
||||||
|
|
||||||
// Delete user files from Firebase Storage
|
// Delete user files from Firebase Storage
|
||||||
await deleteUserFiles(user.username)
|
await deleteUserFiles(user.username)
|
||||||
|
|||||||
64
backend/api/src/delete-message.ts
Normal file
64
backend/api/src/delete-message.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import {APIError, APIHandler} from './helpers/endpoint'
|
||||||
|
import {createSupabaseDirectClient} from 'shared/supabase/init'
|
||||||
|
import {broadcastPrivateMessages} from "api/helpers/private-messages";
|
||||||
|
|
||||||
|
// const DELETED_MESSAGE_CONTENT: JSONContent = {
|
||||||
|
// type: 'doc',
|
||||||
|
// content: [
|
||||||
|
// {
|
||||||
|
// type: 'paragraph',
|
||||||
|
// content: [
|
||||||
|
// {
|
||||||
|
// type: 'text',
|
||||||
|
// text: '[deleted]',
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
// },
|
||||||
|
// ],
|
||||||
|
// }
|
||||||
|
|
||||||
|
export const deleteMessage: APIHandler<'delete-message'> = async ({messageId}, auth) => {
|
||||||
|
const pg = createSupabaseDirectClient()
|
||||||
|
|
||||||
|
// Verify user is the message author and message is not too old
|
||||||
|
const message = await pg.oneOrNone(
|
||||||
|
`SELECT *
|
||||||
|
FROM private_user_messages
|
||||||
|
WHERE id = $1
|
||||||
|
AND user_id = $2`,
|
||||||
|
[messageId, auth.uid]
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!message) {
|
||||||
|
throw new APIError(404, 'Message not found')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Soft delete the message
|
||||||
|
// await pg.none(
|
||||||
|
// `UPDATE private_user_messages
|
||||||
|
// SET deleted = TRUE,
|
||||||
|
// content = $2::jsonb,
|
||||||
|
// ciphertext = NULL,
|
||||||
|
// iv = NULL,
|
||||||
|
// tag = NULL
|
||||||
|
// WHERE id = $1`,
|
||||||
|
// [messageId, DELETED_MESSAGE_CONTENT]
|
||||||
|
// )
|
||||||
|
|
||||||
|
// Hard delete the message
|
||||||
|
await pg.none(
|
||||||
|
`DELETE
|
||||||
|
FROM private_user_messages
|
||||||
|
WHERE id = $1
|
||||||
|
AND user_id = $2`,
|
||||||
|
[messageId, auth.uid]
|
||||||
|
)
|
||||||
|
|
||||||
|
void broadcastPrivateMessages(pg, message.channel_id, auth.uid)
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('broadcastPrivateMessages failed', err)
|
||||||
|
})
|
||||||
|
|
||||||
|
return {success: true}
|
||||||
|
}
|
||||||
|
|
||||||
44
backend/api/src/edit-message.ts
Normal file
44
backend/api/src/edit-message.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import {APIError, APIHandler} from './helpers/endpoint'
|
||||||
|
import {createSupabaseDirectClient} from 'shared/supabase/init'
|
||||||
|
import {encryptMessage} from "shared/encryption";
|
||||||
|
import {broadcastPrivateMessages} from "api/helpers/private-messages";
|
||||||
|
|
||||||
|
|
||||||
|
export const editMessage: APIHandler<'edit-message'> = async ({messageId, content}, auth) => {
|
||||||
|
const pg = createSupabaseDirectClient()
|
||||||
|
|
||||||
|
// Verify user is the message author and message is not too old
|
||||||
|
const message = await pg.oneOrNone(
|
||||||
|
`SELECT *
|
||||||
|
FROM private_user_messages
|
||||||
|
WHERE id = $1
|
||||||
|
AND user_id = $2
|
||||||
|
-- AND created_time > NOW() - INTERVAL '1 day'
|
||||||
|
AND deleted = FALSE`,
|
||||||
|
[messageId, auth.uid]
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!message) {
|
||||||
|
throw new APIError(404, 'Message not found or cannot be edited')
|
||||||
|
}
|
||||||
|
|
||||||
|
const plaintext = JSON.stringify(content)
|
||||||
|
const {ciphertext, iv, tag} = encryptMessage(plaintext)
|
||||||
|
await pg.none(
|
||||||
|
`UPDATE private_user_messages
|
||||||
|
SET ciphertext = $1,
|
||||||
|
iv = $2,
|
||||||
|
tag = $3,
|
||||||
|
is_edited = TRUE,
|
||||||
|
edited_at = NOW()
|
||||||
|
WHERE id = $4`,
|
||||||
|
[ciphertext, iv, tag, messageId]
|
||||||
|
)
|
||||||
|
|
||||||
|
void broadcastPrivateMessages(pg, message.channel_id, auth.uid)
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('broadcastPrivateMessages failed', err)
|
||||||
|
})
|
||||||
|
|
||||||
|
return {success: true}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import {APIHandler} from './helpers/endpoint'
|
import {APIHandler} from './helpers/endpoint'
|
||||||
import {createSupabaseDirectClient} from "shared/supabase/init";
|
import {createSupabaseDirectClient} from "shared/supabase/init";
|
||||||
|
|
||||||
export const getMessagesCount: APIHandler<'get-messages-count'> = async (_, auth) => {
|
export const getMessagesCount: APIHandler<'get-messages-count'> = async (_, _auth) => {
|
||||||
const pg = createSupabaseDirectClient()
|
const pg = createSupabaseDirectClient()
|
||||||
const result = await pg.one(
|
const result = await pg.one(
|
||||||
`
|
`
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export type profileQueryType = {
|
|||||||
pref_romantic_styles?: String[] | undefined,
|
pref_romantic_styles?: String[] | undefined,
|
||||||
diet?: String[] | undefined,
|
diet?: String[] | undefined,
|
||||||
political_beliefs?: String[] | undefined,
|
political_beliefs?: String[] | undefined,
|
||||||
|
religion?: String[] | undefined,
|
||||||
wants_kids_strength?: number | undefined,
|
wants_kids_strength?: number | undefined,
|
||||||
has_kids?: number | undefined,
|
has_kids?: number | undefined,
|
||||||
is_smoker?: boolean | undefined,
|
is_smoker?: boolean | undefined,
|
||||||
@@ -57,6 +58,7 @@ export const loadProfiles = async (props: profileQueryType) => {
|
|||||||
pref_romantic_styles,
|
pref_romantic_styles,
|
||||||
diet,
|
diet,
|
||||||
political_beliefs,
|
political_beliefs,
|
||||||
|
religion,
|
||||||
wants_kids_strength,
|
wants_kids_strength,
|
||||||
has_kids,
|
has_kids,
|
||||||
is_smoker,
|
is_smoker,
|
||||||
@@ -87,7 +89,7 @@ export const loadProfiles = async (props: profileQueryType) => {
|
|||||||
const profiles = compatibleProfiles.filter(
|
const profiles = compatibleProfiles.filter(
|
||||||
(l) =>
|
(l) =>
|
||||||
(!name || l.user.name.toLowerCase().includes(name.toLowerCase())) &&
|
(!name || l.user.name.toLowerCase().includes(name.toLowerCase())) &&
|
||||||
(!genders || genders.includes(l.gender)) &&
|
(!genders || genders.includes(l.gender ?? '')) &&
|
||||||
(!education_levels || education_levels.includes(l.education_level ?? '')) &&
|
(!education_levels || education_levels.includes(l.education_level ?? '')) &&
|
||||||
(!pref_gender || intersection(pref_gender, l.pref_gender).length) &&
|
(!pref_gender || intersection(pref_gender, l.pref_gender).length) &&
|
||||||
(!pref_age_min || (l.age ?? MAX_INT) >= pref_age_min) &&
|
(!pref_age_min || (l.age ?? MAX_INT) >= pref_age_min) &&
|
||||||
@@ -102,6 +104,8 @@ export const loadProfiles = async (props: profileQueryType) => {
|
|||||||
intersection(diet, l.diet).length) &&
|
intersection(diet, l.diet).length) &&
|
||||||
(!political_beliefs ||
|
(!political_beliefs ||
|
||||||
intersection(political_beliefs, l.political_beliefs).length) &&
|
intersection(political_beliefs, l.political_beliefs).length) &&
|
||||||
|
(!religion ||
|
||||||
|
intersection(religion, l.religion).length) &&
|
||||||
(!wants_kids_strength ||
|
(!wants_kids_strength ||
|
||||||
wants_kids_strength == -1 ||
|
wants_kids_strength == -1 ||
|
||||||
!l.wants_kids_strength ||
|
!l.wants_kids_strength ||
|
||||||
@@ -114,6 +118,7 @@ export const loadProfiles = async (props: profileQueryType) => {
|
|||||||
(has_kids == 0 && !l.has_kids) ||
|
(has_kids == 0 && !l.has_kids) ||
|
||||||
(l.has_kids && l.has_kids > 0)) &&
|
(l.has_kids && l.has_kids > 0)) &&
|
||||||
(is_smoker === undefined || l.is_smoker === is_smoker) &&
|
(is_smoker === undefined || l.is_smoker === is_smoker) &&
|
||||||
|
(!l.disabled) &&
|
||||||
(l.id.toString() != skipId) &&
|
(l.id.toString() != skipId) &&
|
||||||
(!geodbCityIds ||
|
(!geodbCityIds ||
|
||||||
(l.geodb_city_id && geodbCityIds.includes(l.geodb_city_id))) &&
|
(l.geodb_city_id && geodbCityIds.includes(l.geodb_city_id))) &&
|
||||||
@@ -145,6 +150,7 @@ export const loadProfiles = async (props: profileQueryType) => {
|
|||||||
join('users on users.id = profiles.user_id'),
|
join('users on users.id = profiles.user_id'),
|
||||||
leftJoin(userActivityJoin),
|
leftJoin(userActivityJoin),
|
||||||
where('looking_for_matches = true'),
|
where('looking_for_matches = true'),
|
||||||
|
where(`profiles.disabled != true`),
|
||||||
// where(`pinned_url is not null and pinned_url != ''`),
|
// where(`pinned_url is not null and pinned_url != ''`),
|
||||||
where(
|
where(
|
||||||
`(data->>'isBannedFromPosting' != 'true' or data->>'isBannedFromPosting' is null)`
|
`(data->>'isBannedFromPosting' != 'true' or data->>'isBannedFromPosting' is null)`
|
||||||
@@ -199,6 +205,12 @@ export const loadProfiles = async (props: profileQueryType) => {
|
|||||||
{political_beliefs}
|
{political_beliefs}
|
||||||
),
|
),
|
||||||
|
|
||||||
|
religion?.length &&
|
||||||
|
where(
|
||||||
|
`religion IS NULL OR religion = '{}' OR religion && $(religion)`,
|
||||||
|
{religion}
|
||||||
|
),
|
||||||
|
|
||||||
!!wants_kids_strength &&
|
!!wants_kids_strength &&
|
||||||
wants_kids_strength !== -1 &&
|
wants_kids_strength !== -1 &&
|
||||||
where(
|
where(
|
||||||
|
|||||||
@@ -13,9 +13,11 @@ import {sendNewMessageEmail} from 'email/functions/helpers'
|
|||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import utc from 'dayjs/plugin/utc'
|
import utc from 'dayjs/plugin/utc'
|
||||||
import timezone from 'dayjs/plugin/timezone'
|
import timezone from 'dayjs/plugin/timezone'
|
||||||
import webPush from 'web-push';
|
import webPush from 'web-push'
|
||||||
import {parseJsonContentToText} from "common/util/parse";
|
import {parseJsonContentToText} from "common/util/parse"
|
||||||
import {encryptMessage} from "shared/encryption";
|
import {encryptMessage} from "shared/encryption"
|
||||||
|
import * as admin from 'firebase-admin'
|
||||||
|
import {TokenMessage} from "firebase-admin/lib/messaging/messaging-api";
|
||||||
|
|
||||||
dayjs.extend(utc)
|
dayjs.extend(utc)
|
||||||
dayjs.extend(timezone)
|
dayjs.extend(timezone)
|
||||||
@@ -29,17 +31,17 @@ export const leaveChatContent = (userName: string) => ({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export const joinChatContent = (userName: string) => {
|
// export const joinChatContent = (userName: string) => {
|
||||||
return {
|
// return {
|
||||||
type: 'doc',
|
// type: 'doc',
|
||||||
content: [
|
// content: [
|
||||||
{
|
// {
|
||||||
type: 'paragraph',
|
// type: 'paragraph',
|
||||||
content: [{text: `${userName} joined the chat!`, type: 'text'}],
|
// content: [{text: `${userName} joined the chat!`, type: 'text'}],
|
||||||
},
|
// },
|
||||||
],
|
// ],
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
export const insertPrivateMessage = async (
|
export const insertPrivateMessage = async (
|
||||||
content: Json,
|
content: Json,
|
||||||
@@ -48,8 +50,8 @@ export const insertPrivateMessage = async (
|
|||||||
visibility: ChatVisibility,
|
visibility: ChatVisibility,
|
||||||
pg: SupabaseDirectClient
|
pg: SupabaseDirectClient
|
||||||
) => {
|
) => {
|
||||||
const plaintext = JSON.stringify(content);
|
const plaintext = JSON.stringify(content)
|
||||||
const {ciphertext, iv, tag} = encryptMessage(plaintext);
|
const {ciphertext, iv, tag} = encryptMessage(plaintext)
|
||||||
const lastMessage = await pg.one(
|
const lastMessage = await pg.one(
|
||||||
`insert into private_user_messages (ciphertext, iv, tag, channel_id, user_id, visibility)
|
`insert into private_user_messages (ciphertext, iv, tag, channel_id, user_id, visibility)
|
||||||
values ($1, $2, $3, $4, $5, $6)
|
values ($1, $2, $3, $4, $5, $6)
|
||||||
@@ -88,6 +90,27 @@ export const addUsersToPrivateMessageChannel = async (
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function broadcastPrivateMessages(
|
||||||
|
pg: SupabaseDirectClient,
|
||||||
|
channelId: number,
|
||||||
|
userId: string,
|
||||||
|
) {
|
||||||
|
const otherUserIds = await pg.map<string>(
|
||||||
|
`select user_id
|
||||||
|
from private_user_message_channel_members
|
||||||
|
where channel_id = $1
|
||||||
|
and user_id != $2
|
||||||
|
and status != 'left'
|
||||||
|
`,
|
||||||
|
[channelId, userId],
|
||||||
|
(r) => r.user_id
|
||||||
|
)
|
||||||
|
otherUserIds.concat(userId).forEach((otherUserId) => {
|
||||||
|
broadcast(`private-user-messages/${otherUserId}`, {})
|
||||||
|
})
|
||||||
|
return otherUserIds;
|
||||||
|
}
|
||||||
|
|
||||||
export const createPrivateUserMessageMain = async (
|
export const createPrivateUserMessageMain = async (
|
||||||
creator: User,
|
creator: User,
|
||||||
channelId: number,
|
channelId: number,
|
||||||
@@ -115,26 +138,13 @@ export const createPrivateUserMessageMain = async (
|
|||||||
channel_id: channelId,
|
channel_id: channelId,
|
||||||
user_id: creator.id,
|
user_id: creator.id,
|
||||||
}
|
}
|
||||||
|
const otherUserIds = await broadcastPrivateMessages(pg, channelId, creator.id);
|
||||||
const otherUserIds = await pg.map<string>(
|
|
||||||
`select user_id
|
|
||||||
from private_user_message_channel_members
|
|
||||||
where channel_id = $1
|
|
||||||
and user_id != $2
|
|
||||||
and status != 'left'
|
|
||||||
`,
|
|
||||||
[channelId, creator.id],
|
|
||||||
(r) => r.user_id
|
|
||||||
)
|
|
||||||
otherUserIds.concat(creator.id).forEach((otherUserId) => {
|
|
||||||
broadcast(`private-user-messages/${otherUserId}`, {})
|
|
||||||
})
|
|
||||||
|
|
||||||
// Fire and forget safely
|
// Fire and forget safely
|
||||||
void notifyOtherUserInChannelIfInactive(channelId, creator, content, pg)
|
void notifyOtherUserInChannelIfInactive(channelId, creator, content, pg)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error('notifyOtherUserInChannelIfInactive failed', err)
|
console.error('notifyOtherUserInChannelIfInactive failed', err)
|
||||||
});
|
})
|
||||||
|
|
||||||
track(creator.id, 'send private message', {
|
track(creator.id, 'send private message', {
|
||||||
channelId,
|
channelId,
|
||||||
@@ -162,49 +172,24 @@ const notifyOtherUserInChannelIfInactive = async (
|
|||||||
// We're only sending notifs for 1:1 channels
|
// We're only sending notifs for 1:1 channels
|
||||||
if (!otherUserIds || otherUserIds.length > 1) return
|
if (!otherUserIds || otherUserIds.length > 1) return
|
||||||
|
|
||||||
const otherUserId = first(otherUserIds)
|
const receiverId = first(otherUserIds)?.user_id
|
||||||
if (!otherUserId) return
|
if (!receiverId) return
|
||||||
|
|
||||||
// TODO: notification only for active user
|
// TODO: notification only for active user
|
||||||
|
|
||||||
const otherUser = await getUser(otherUserId.user_id)
|
const receiver = await getUser(receiverId)
|
||||||
console.debug('otherUser:', otherUser)
|
console.debug('receiver:', receiver)
|
||||||
if (!otherUser) return
|
if (!receiver) return
|
||||||
|
|
||||||
// Push notif
|
// Push notifs
|
||||||
webPush.setVapidDetails(
|
|
||||||
'mailto:hello@compassmeet.com',
|
|
||||||
process.env.VAPID_PUBLIC_KEY!,
|
|
||||||
process.env.VAPID_PRIVATE_KEY!
|
|
||||||
);
|
|
||||||
const textContent = parseJsonContentToText(content)
|
const textContent = parseJsonContentToText(content)
|
||||||
// Retrieve subscription from the database
|
const payload = {
|
||||||
const subscriptions = await getSubscriptionsFromDB(otherUser.id, pg);
|
title: `${creator.name}`,
|
||||||
for (const subscription of subscriptions) {
|
body: textContent,
|
||||||
try {
|
url: `/messages/${channelId}`,
|
||||||
const payload = JSON.stringify({
|
|
||||||
title: `${creator.name}`,
|
|
||||||
body: textContent,
|
|
||||||
url: `/messages/${channelId}`,
|
|
||||||
})
|
|
||||||
console.log('Sending notification to:', subscription.endpoint, payload);
|
|
||||||
await webPush.sendNotification(subscription, payload);
|
|
||||||
} catch (err: any) {
|
|
||||||
console.log('Failed to send notification', err);
|
|
||||||
if (err.statusCode === 410 || err.statusCode === 404) {
|
|
||||||
console.warn('Removing expired subscription', subscription.endpoint);
|
|
||||||
await pg.none(
|
|
||||||
`DELETE
|
|
||||||
FROM push_subscriptions
|
|
||||||
WHERE endpoint = $1
|
|
||||||
AND user_id = $2`,
|
|
||||||
[subscription.endpoint, otherUser.id]
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
console.error('Push failed', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
await sendWebNotifications(pg, receiverId, JSON.stringify(payload))
|
||||||
|
await sendMobileNotifications(pg, receiverId, payload)
|
||||||
|
|
||||||
const startOfDay = dayjs()
|
const startOfDay = dayjs()
|
||||||
.tz('America/Los_Angeles')
|
.tz('America/Los_Angeles')
|
||||||
@@ -220,9 +205,9 @@ const notifyOtherUserInChannelIfInactive = async (
|
|||||||
[channelId, creator.id, startOfDay]
|
[channelId, creator.id, startOfDay]
|
||||||
)
|
)
|
||||||
log('previous messages this day', previousMessagesThisDayBetweenTheseUsers)
|
log('previous messages this day', previousMessagesThisDayBetweenTheseUsers)
|
||||||
if (previousMessagesThisDayBetweenTheseUsers.count > 0) return
|
if (previousMessagesThisDayBetweenTheseUsers.count > 1) return
|
||||||
|
|
||||||
await createNewMessageNotification(creator, otherUser, channelId)
|
await createNewMessageNotification(creator, receiver, channelId)
|
||||||
}
|
}
|
||||||
|
|
||||||
const createNewMessageNotification = async (
|
const createNewMessageNotification = async (
|
||||||
@@ -237,9 +222,38 @@ const createNewMessageNotification = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function getSubscriptionsFromDB(
|
async function sendWebNotifications(
|
||||||
|
pg: SupabaseDirectClient,
|
||||||
|
userId: string,
|
||||||
|
payload: string,
|
||||||
|
) {
|
||||||
|
webPush.setVapidDetails(
|
||||||
|
'mailto:hello@compassmeet.com',
|
||||||
|
process.env.VAPID_PUBLIC_KEY!,
|
||||||
|
process.env.VAPID_PRIVATE_KEY!
|
||||||
|
)
|
||||||
|
// Retrieve subscription from the database
|
||||||
|
const subscriptions = await getSubscriptionsFromDB(pg, userId)
|
||||||
|
for (const subscription of subscriptions) {
|
||||||
|
try {
|
||||||
|
console.log('Sending notification to:', subscription.endpoint, payload)
|
||||||
|
await webPush.sendNotification(subscription, payload)
|
||||||
|
} catch (err: any) {
|
||||||
|
console.log('Failed to send notification', err)
|
||||||
|
if (err.statusCode === 410 || err.statusCode === 404) {
|
||||||
|
console.warn('Removing expired subscription', subscription.endpoint)
|
||||||
|
await removeSubscription(pg, subscription.endpoint, userId)
|
||||||
|
} else {
|
||||||
|
console.error('Push failed', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function getSubscriptionsFromDB(
|
||||||
|
pg: SupabaseDirectClient,
|
||||||
userId: string,
|
userId: string,
|
||||||
pg: SupabaseDirectClient
|
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const subscriptions = await pg.manyOrNone(`
|
const subscriptions = await pg.manyOrNone(`
|
||||||
@@ -247,14 +261,127 @@ export async function getSubscriptionsFromDB(
|
|||||||
from push_subscriptions
|
from push_subscriptions
|
||||||
where user_id = $1
|
where user_id = $1
|
||||||
`, [userId]
|
`, [userId]
|
||||||
);
|
)
|
||||||
|
|
||||||
return subscriptions.map(sub => ({
|
return subscriptions.map(sub => ({
|
||||||
endpoint: sub.endpoint,
|
endpoint: sub.endpoint,
|
||||||
keys: sub.keys,
|
keys: sub.keys,
|
||||||
}));
|
}))
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error fetching subscriptions', err);
|
console.error('Error fetching subscriptions', err)
|
||||||
return [];
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeSubscription(
|
||||||
|
pg: SupabaseDirectClient,
|
||||||
|
endpoint: any,
|
||||||
|
userId: string,
|
||||||
|
) {
|
||||||
|
await pg.none(
|
||||||
|
`DELETE
|
||||||
|
FROM push_subscriptions
|
||||||
|
WHERE endpoint = $1
|
||||||
|
AND user_id = $2`,
|
||||||
|
[endpoint, userId]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeMobileSubscription(
|
||||||
|
pg: SupabaseDirectClient,
|
||||||
|
token: any,
|
||||||
|
userId: string,
|
||||||
|
) {
|
||||||
|
await pg.none(
|
||||||
|
`DELETE
|
||||||
|
FROM push_subscriptions_mobile
|
||||||
|
WHERE token = $1
|
||||||
|
AND user_id = $2`,
|
||||||
|
[token, userId]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async function sendMobileNotifications(
|
||||||
|
pg: SupabaseDirectClient,
|
||||||
|
userId: string,
|
||||||
|
payload: PushPayload,
|
||||||
|
) {
|
||||||
|
const subscriptions = await getMobileSubscriptionsFromDB(pg, userId)
|
||||||
|
for (const subscription of subscriptions) {
|
||||||
|
await sendPushToToken(pg, userId, subscription.token, payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PushPayload {
|
||||||
|
title: string
|
||||||
|
body: string
|
||||||
|
url: string
|
||||||
|
data?: Record<string, string>
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendPushToToken(
|
||||||
|
pg: SupabaseDirectClient,
|
||||||
|
userId: string,
|
||||||
|
token: string,
|
||||||
|
payload: PushPayload,
|
||||||
|
) {
|
||||||
|
const message: TokenMessage = {
|
||||||
|
token,
|
||||||
|
android: {
|
||||||
|
notification: {
|
||||||
|
title: payload.title,
|
||||||
|
body: payload.body,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
endpoint: payload.url,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Fine to create at each call, as it's a cached singleton
|
||||||
|
const fcm = admin.messaging()
|
||||||
|
console.log('Sending notification to:', token, message)
|
||||||
|
const response = await fcm.send(message)
|
||||||
|
console.log('Push sent successfully:', response)
|
||||||
|
return response
|
||||||
|
} catch (err: unknown) {
|
||||||
|
// Check if it's a Firebase Messaging error
|
||||||
|
if (err instanceof Error && 'code' in err) {
|
||||||
|
const firebaseError = err as { code: string; message: string }
|
||||||
|
console.warn('Firebase error:', firebaseError.code, firebaseError.message)
|
||||||
|
|
||||||
|
// Handle specific error cases here if needed
|
||||||
|
// For example, if token is no longer valid:
|
||||||
|
if (firebaseError.code === 'messaging/registration-token-not-registered' ||
|
||||||
|
firebaseError.code === 'messaging/invalid-argument') {
|
||||||
|
console.warn('Removing invalid FCM token')
|
||||||
|
await removeMobileSubscription(pg, token, userId)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('Unknown error:', err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function getMobileSubscriptionsFromDB(
|
||||||
|
pg: SupabaseDirectClient,
|
||||||
|
userId: string,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const subscriptions = await pg.manyOrNone(`
|
||||||
|
select token
|
||||||
|
from push_subscriptions_mobile
|
||||||
|
where user_id = $1
|
||||||
|
`, [userId]
|
||||||
|
)
|
||||||
|
|
||||||
|
return subscriptions
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching subscriptions', err)
|
||||||
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
64
backend/api/src/public/swagger.css
Normal file
64
backend/api/src/public/swagger.css
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
body {
|
||||||
|
background-color: #1e1e1e !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
.swagger-ui p,
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6,
|
||||||
|
label,
|
||||||
|
.btn,
|
||||||
|
.parameter__name,
|
||||||
|
.parameter__type,
|
||||||
|
.parameter__in,
|
||||||
|
.response-control-media-type__title,
|
||||||
|
table thead tr td,
|
||||||
|
table thead tr th,
|
||||||
|
.tab li,
|
||||||
|
.response-col_links,
|
||||||
|
.opblock-summary-description {
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
.swagger-ui .topbar, .opblock-body select, textarea {
|
||||||
|
background-color: #2b2b2b !important;
|
||||||
|
color: #ffffff !important;
|
||||||
|
}
|
||||||
|
.swagger-ui .opblock {
|
||||||
|
background-color: #2c2c2c !important;
|
||||||
|
border-color: #fff !important;
|
||||||
|
}
|
||||||
|
.swagger-ui .opblock .opblock-summary-method {
|
||||||
|
background-color: #1f1f1f !important;
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
.swagger-ui .opblock .opblock-section-header {
|
||||||
|
background: #1f1f1f !important;
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
.swagger-ui .responses-wrapper {
|
||||||
|
background-color: #1f1f1f !important;
|
||||||
|
}
|
||||||
|
.swagger-ui .response-col_status {
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
.swagger-ui .scheme-container {
|
||||||
|
background-color: #1f1f1f !important;
|
||||||
|
}
|
||||||
|
.swagger-ui .modal-ux, input {
|
||||||
|
background-color: #1f1f1f !important;
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
.swagger-ui svg path {
|
||||||
|
fill: white !important;
|
||||||
|
}
|
||||||
|
.swagger-ui .close-modal svg {
|
||||||
|
color: #1e90ff !important;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: #1e90ff !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
60
backend/api/src/react-to-message.ts
Normal file
60
backend/api/src/react-to-message.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import {APIError, APIHandler} from './helpers/endpoint'
|
||||||
|
import {createSupabaseDirectClient} from 'shared/supabase/init'
|
||||||
|
import {broadcastPrivateMessages} from "api/helpers/private-messages";
|
||||||
|
|
||||||
|
|
||||||
|
export const reactToMessage: APIHandler<'react-to-message'> = async ({messageId, reaction, toDelete}, auth) => {
|
||||||
|
const pg = createSupabaseDirectClient()
|
||||||
|
|
||||||
|
// Verify user is a member of the channel
|
||||||
|
const message = await pg.oneOrNone(
|
||||||
|
`SELECT *
|
||||||
|
FROM private_user_message_channel_members m
|
||||||
|
JOIN private_user_messages msg ON msg.channel_id = m.channel_id
|
||||||
|
WHERE m.user_id = $1
|
||||||
|
AND msg.id = $2`,
|
||||||
|
[auth.uid, messageId]
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!message) {
|
||||||
|
throw new APIError(403, 'Not authorized to react to this message')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toDelete) {
|
||||||
|
// Remove the reaction
|
||||||
|
await pg.none(
|
||||||
|
`UPDATE private_user_messages
|
||||||
|
SET reactions = reactions - $1
|
||||||
|
WHERE id = $2
|
||||||
|
AND reactions -> $1 ? $3`,
|
||||||
|
[reaction, messageId, auth.uid]
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Toggle reaction
|
||||||
|
await pg.none(
|
||||||
|
`UPDATE private_user_messages
|
||||||
|
SET reactions =
|
||||||
|
CASE
|
||||||
|
WHEN reactions -> $1 IS NOT NULL
|
||||||
|
THEN reactions - $1
|
||||||
|
ELSE jsonb_set(
|
||||||
|
COALESCE(reactions, '{}'::jsonb),
|
||||||
|
ARRAY [$1],
|
||||||
|
(
|
||||||
|
COALESCE(reactions -> $1, '[]'::jsonb) || to_jsonb($2::text)
|
||||||
|
),
|
||||||
|
TRUE
|
||||||
|
)
|
||||||
|
END
|
||||||
|
WHERE id = $3`,
|
||||||
|
[reaction, auth.uid, messageId]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
void broadcastPrivateMessages(pg, message.channel_id, auth.uid)
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('broadcastPrivateMessages failed', err)
|
||||||
|
})
|
||||||
|
|
||||||
|
return {success: true}
|
||||||
|
}
|
||||||
28
backend/api/src/save-subscription-mobile.ts
Normal file
28
backend/api/src/save-subscription-mobile.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import {APIError, APIHandler} from './helpers/endpoint'
|
||||||
|
import {createSupabaseDirectClient} from 'shared/supabase/init'
|
||||||
|
|
||||||
|
export const saveSubscriptionMobile: APIHandler<'save-subscription-mobile'> = async (body, auth) => {
|
||||||
|
const {token} = body
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
throw new APIError(400, `Invalid subscription object`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = auth?.uid
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pg = createSupabaseDirectClient()
|
||||||
|
await pg.none(`
|
||||||
|
insert into push_subscriptions_mobile(token, platform, user_id)
|
||||||
|
values ($1, $2, $3)
|
||||||
|
on conflict(token) do update set platform = excluded.platform,
|
||||||
|
user_id = excluded.user_id
|
||||||
|
`,
|
||||||
|
[token, 'android', userId]
|
||||||
|
);
|
||||||
|
return {success: true};
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error saving subscription', err);
|
||||||
|
throw new APIError(500, `Failed to save subscription`)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,28 +1,20 @@
|
|||||||
import { constructPrefixTsQuery } from 'shared/helpers/search'
|
import {constructPrefixTsQuery} from 'shared/helpers/search'
|
||||||
import {
|
import {from, limit, orderBy, renderSql, select, where,} from 'shared/supabase/sql-builder'
|
||||||
from,
|
import {type APIHandler} from './helpers/endpoint'
|
||||||
join,
|
import {convertUser} from 'common/supabase/users'
|
||||||
limit,
|
import {createSupabaseDirectClient} from 'shared/supabase/init'
|
||||||
orderBy,
|
import {toUserAPIResponse} from 'common/api/user-types'
|
||||||
renderSql,
|
import {uniqBy} from 'lodash'
|
||||||
select,
|
|
||||||
where,
|
|
||||||
} from 'shared/supabase/sql-builder'
|
|
||||||
import { type APIHandler } from './helpers/endpoint'
|
|
||||||
import { convertUser } from 'common/supabase/users'
|
|
||||||
import { createSupabaseDirectClient } from 'shared/supabase/init'
|
|
||||||
import { toUserAPIResponse } from 'common/api/user-types'
|
|
||||||
import { uniqBy } from 'lodash'
|
|
||||||
|
|
||||||
export const searchUsers: APIHandler<'search-users'> = async (props, auth) => {
|
export const searchUsers: APIHandler<'search-users'> = async (props, _auth) => {
|
||||||
const { term, page, limit } = props
|
const {term, page, limit} = props
|
||||||
|
|
||||||
const pg = createSupabaseDirectClient()
|
const pg = createSupabaseDirectClient()
|
||||||
|
|
||||||
const offset = page * limit
|
const offset = page * limit
|
||||||
// const userId = auth?.uid
|
// const userId = auth?.uid
|
||||||
// const searchFollowersSQL = getSearchUserSQL({ term, offset, limit, userId })
|
// const searchFollowersSQL = getSearchUserSQL({ term, offset, limit, userId })
|
||||||
const searchAllSQL = getSearchUserSQL({ term, offset, limit })
|
const searchAllSQL = getSearchUserSQL({term, offset, limit})
|
||||||
const [all] = await Promise.all([
|
const [all] = await Promise.all([
|
||||||
// pg.map(searchFollowersSQL, null, convertUser),
|
// pg.map(searchFollowersSQL, null, convertUser),
|
||||||
pg.map(searchAllSQL, null, convertUser),
|
pg.map(searchAllSQL, null, convertUser),
|
||||||
@@ -39,7 +31,7 @@ function getSearchUserSQL(props: {
|
|||||||
limit: number
|
limit: number
|
||||||
userId?: string // search only this user's followers
|
userId?: string // search only this user's followers
|
||||||
}) {
|
}) {
|
||||||
const { term } = props
|
const {term} = props
|
||||||
|
|
||||||
return renderSql(
|
return renderSql(
|
||||||
// userId
|
// userId
|
||||||
@@ -50,21 +42,21 @@ function getSearchUserSQL(props: {
|
|||||||
// where('user_follows.user_id = $1', [userId]),
|
// where('user_follows.user_id = $1', [userId]),
|
||||||
// ]
|
// ]
|
||||||
// :
|
// :
|
||||||
[select('*'), from('users')],
|
[select('*'), from('users')],
|
||||||
term
|
term
|
||||||
? [
|
? [
|
||||||
where(
|
where(
|
||||||
`name_username_vector @@ websearch_to_tsquery('english', $1)
|
`name_username_vector @@ websearch_to_tsquery('english', $1)
|
||||||
or name_username_vector @@ to_tsquery('english', $2)`,
|
or name_username_vector @@ to_tsquery('english', $2)`,
|
||||||
[term, constructPrefixTsQuery(term)]
|
[term, constructPrefixTsQuery(term)]
|
||||||
),
|
),
|
||||||
|
|
||||||
orderBy(
|
orderBy(
|
||||||
`ts_rank(name_username_vector, websearch_to_tsquery($1)) desc,
|
`ts_rank(name_username_vector, websearch_to_tsquery($1)) desc,
|
||||||
data->>'lastBetTime' desc nulls last`,
|
data->>'lastBetTime' desc nulls last`,
|
||||||
[term]
|
[term]
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
: orderBy(`data->'creatorTraders'->'allTime' desc nulls last`),
|
: orderBy(`data->'creatorTraders'->'allTime' desc nulls last`),
|
||||||
limit(props.limit, props.offset)
|
limit(props.limit, props.offset)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export const sendSearchNotifications = async () => {
|
|||||||
|
|
||||||
for (const row of searches) {
|
for (const row of searches) {
|
||||||
if (typeof row.search_filters !== 'object') continue;
|
if (typeof row.search_filters !== 'object') continue;
|
||||||
const { orderBy, ...filters } = (row.search_filters ?? {}) as Record<string, any>
|
const { orderBy: _, ...filters } = (row.search_filters ?? {}) as Record<string, any>
|
||||||
const props = {
|
const props = {
|
||||||
...filters,
|
...filters,
|
||||||
skipId: row.creator_id,
|
skipId: row.creator_id,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import * as admin from 'firebase-admin'
|
|||||||
import {initAdmin} from 'shared/init-admin'
|
import {initAdmin} from 'shared/init-admin'
|
||||||
import {loadSecretsToEnv} from 'common/secrets'
|
import {loadSecretsToEnv} from 'common/secrets'
|
||||||
import {log} from 'shared/utils'
|
import {log} from 'shared/utils'
|
||||||
import {IS_LOCAL} from "common/envs/constants";
|
import {IS_LOCAL} from "common/hosting/constants";
|
||||||
import {METRIC_WRITER} from 'shared/monitoring/metric-writer'
|
import {METRIC_WRITER} from 'shared/monitoring/metric-writer'
|
||||||
import {listen as webSocketListen} from 'shared/websockets/server'
|
import {listen as webSocketListen} from 'shared/websockets/server'
|
||||||
|
|
||||||
@@ -40,4 +40,4 @@ const startupProcess = async () => {
|
|||||||
|
|
||||||
webSocketListen(httpServer, '/ws')
|
webSocketListen(httpServer, '/ws')
|
||||||
}
|
}
|
||||||
startupProcess().then(r => log('Server started successfully'))
|
startupProcess().then(_r => log('Server started successfully'))
|
||||||
|
|||||||
8
backend/api/src/test.ts
Normal file
8
backend/api/src/test.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import {sendTestEmail} from "email/functions/helpers";
|
||||||
|
|
||||||
|
export const localSendTestEmail = async () => {
|
||||||
|
sendTestEmail('hello@compassmeet.com')
|
||||||
|
.then(() => console.debug('Email sent successfully!'))
|
||||||
|
.catch((error) => console.error('Failed to send email:', error))
|
||||||
|
return { message: 'Email sent successfully!'}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import React from 'react';
|
||||||
import {PrivateUser, User} from 'common/user'
|
import {PrivateUser, User} from 'common/user'
|
||||||
import {getNotificationDestinationsForUser, UNSUBSCRIBE_URL} from 'common/user-notification-preferences'
|
import {getNotificationDestinationsForUser, UNSUBSCRIBE_URL} from 'common/user-notification-preferences'
|
||||||
import {sendEmail} from './send-email'
|
import {sendEmail} from './send-email'
|
||||||
@@ -5,12 +6,13 @@ import {NewMessageEmail} from '../new-message'
|
|||||||
import {NewEndorsementEmail} from '../new-endorsement'
|
import {NewEndorsementEmail} from '../new-endorsement'
|
||||||
import {Test} from '../test'
|
import {Test} from '../test'
|
||||||
import {getProfile} from 'shared/profiles/supabase'
|
import {getProfile} from 'shared/profiles/supabase'
|
||||||
import { render } from "@react-email/render"
|
import {render} from "@react-email/render"
|
||||||
import {MatchesType} from "common/profiles/bookmarked_searches";
|
import {MatchesType} from "common/profiles/bookmarked_searches";
|
||||||
import NewSearchAlertsEmail from "email/new-search_alerts";
|
import NewSearchAlertsEmail from "email/new-search_alerts";
|
||||||
import WelcomeEmail from "email/welcome";
|
import WelcomeEmail from "email/welcome";
|
||||||
|
import * as admin from "firebase-admin";
|
||||||
|
|
||||||
const from = 'Compass <compass@compassmeet.com>'
|
export const fromEmail = 'Compass <compass@compassmeet.com>'
|
||||||
|
|
||||||
// export const sendNewMatchEmail = async (
|
// export const sendNewMatchEmail = async (
|
||||||
// privateUser: PrivateUser,
|
// privateUser: PrivateUser,
|
||||||
@@ -60,7 +62,7 @@ export const sendNewMessageEmail = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
return await sendEmail({
|
return await sendEmail({
|
||||||
from,
|
from: fromEmail,
|
||||||
subject: `${fromUser.name} sent you a message!`,
|
subject: `${fromUser.name} sent you a message!`,
|
||||||
to: privateUser.email,
|
to: privateUser.email,
|
||||||
html: await render(
|
html: await render(
|
||||||
@@ -81,8 +83,9 @@ export const sendWelcomeEmail = async (
|
|||||||
privateUser: PrivateUser,
|
privateUser: PrivateUser,
|
||||||
) => {
|
) => {
|
||||||
if (!privateUser.email) return
|
if (!privateUser.email) return
|
||||||
|
const verificationLink = await admin.auth().generateEmailVerificationLink(privateUser.email);
|
||||||
return await sendEmail({
|
return await sendEmail({
|
||||||
from,
|
from: fromEmail,
|
||||||
subject: `Welcome to Compass!`,
|
subject: `Welcome to Compass!`,
|
||||||
to: privateUser.email,
|
to: privateUser.email,
|
||||||
html: await render(
|
html: await render(
|
||||||
@@ -90,6 +93,7 @@ export const sendWelcomeEmail = async (
|
|||||||
toUser={toUser}
|
toUser={toUser}
|
||||||
unsubscribeUrl={UNSUBSCRIBE_URL}
|
unsubscribeUrl={UNSUBSCRIBE_URL}
|
||||||
email={privateUser.email}
|
email={privateUser.email}
|
||||||
|
verificationLink={verificationLink}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
@@ -108,7 +112,7 @@ export const sendSearchAlertsEmail = async (
|
|||||||
if (!email || !sendToEmail) return
|
if (!email || !sendToEmail) return
|
||||||
|
|
||||||
return await sendEmail({
|
return await sendEmail({
|
||||||
from,
|
from: fromEmail,
|
||||||
subject: `People aligned with your values just joined`,
|
subject: `People aligned with your values just joined`,
|
||||||
to: email,
|
to: email,
|
||||||
html: await render(
|
html: await render(
|
||||||
@@ -135,7 +139,7 @@ export const sendNewEndorsementEmail = async (
|
|||||||
if (!privateUser.email || !sendToEmail) return
|
if (!privateUser.email || !sendToEmail) return
|
||||||
|
|
||||||
return await sendEmail({
|
return await sendEmail({
|
||||||
from,
|
from: fromEmail,
|
||||||
subject: `${fromUser.name} just endorsed you!`,
|
subject: `${fromUser.name} just endorsed you!`,
|
||||||
to: privateUser.email,
|
to: privateUser.email,
|
||||||
html: await render(
|
html: await render(
|
||||||
@@ -152,7 +156,7 @@ export const sendNewEndorsementEmail = async (
|
|||||||
|
|
||||||
export const sendTestEmail = async (toEmail: string) => {
|
export const sendTestEmail = async (toEmail: string) => {
|
||||||
return await sendEmail({
|
return await sendEmail({
|
||||||
from,
|
from: fromEmail,
|
||||||
subject: 'Test email from Compass',
|
subject: 'Test email from Compass',
|
||||||
to: toEmail,
|
to: toEmail,
|
||||||
html: await render(<Test name="Test User"/>),
|
html: await render(<Test name="Test User"/>),
|
||||||
|
|||||||
@@ -36,8 +36,10 @@ export const sinclairProfile: ProfileRow = {
|
|||||||
pref_gender: ['female', 'trans-female'],
|
pref_gender: ['female', 'trans-female'],
|
||||||
pref_age_min: 18,
|
pref_age_min: 18,
|
||||||
pref_age_max: 21,
|
pref_age_max: 21,
|
||||||
|
religion: [],
|
||||||
pref_relation_styles: ['friendship'],
|
pref_relation_styles: ['friendship'],
|
||||||
pref_romantic_styles: ['poly', 'open', 'mono'],
|
pref_romantic_styles: ['poly', 'open', 'mono'],
|
||||||
|
disabled: false,
|
||||||
wants_kids_strength: 3,
|
wants_kids_strength: 3,
|
||||||
looking_for_matches: true,
|
looking_for_matches: true,
|
||||||
visibility: 'public',
|
visibility: 'public',
|
||||||
@@ -50,6 +52,7 @@ export const sinclairProfile: ProfileRow = {
|
|||||||
political_beliefs: ['e/acc', 'libertarian'],
|
political_beliefs: ['e/acc', 'libertarian'],
|
||||||
religious_belief_strength: null,
|
religious_belief_strength: null,
|
||||||
religious_beliefs: null,
|
religious_beliefs: null,
|
||||||
|
political_details: '',
|
||||||
photo_urls: [
|
photo_urls: [
|
||||||
'https://firebasestorage.googleapis.com/v0/b/mantic-markets.appspot.com/o/user-images%2FSinclair%2Flove-images%2FnJz22lr3Bl.jpg?alt=media&token=f1e99ba3-39cc-4637-8702-16a3a8dd49db',
|
'https://firebasestorage.googleapis.com/v0/b/mantic-markets.appspot.com/o/user-images%2FSinclair%2Flove-images%2FnJz22lr3Bl.jpg?alt=media&token=f1e99ba3-39cc-4637-8702-16a3a8dd49db',
|
||||||
'https://firebasestorage.googleapis.com/v0/b/mantic-markets.appspot.com/o/user-images%2FSinclair%2Flove-images%2FygM0mGgP_j.HEIC?alt=media&token=573b23d9-693c-4d6e-919b-097309f370e1',
|
'https://firebasestorage.googleapis.com/v0/b/mantic-markets.appspot.com/o/user-images%2FSinclair%2Flove-images%2FygM0mGgP_j.HEIC?alt=media&token=573b23d9-693c-4d6e-919b-097309f370e1',
|
||||||
@@ -135,8 +138,10 @@ export const jamesProfile: ProfileRow = {
|
|||||||
city: 'San Francisco',
|
city: 'San Francisco',
|
||||||
gender: 'male',
|
gender: 'male',
|
||||||
pref_gender: ['female'],
|
pref_gender: ['female'],
|
||||||
|
disabled: false,
|
||||||
pref_age_min: 22,
|
pref_age_min: 22,
|
||||||
pref_age_max: 32,
|
pref_age_max: 32,
|
||||||
|
religion: [],
|
||||||
pref_relation_styles: ['friendship'],
|
pref_relation_styles: ['friendship'],
|
||||||
pref_romantic_styles: ['poly', 'open', 'mono'],
|
pref_romantic_styles: ['poly', 'open', 'mono'],
|
||||||
wants_kids_strength: 4,
|
wants_kids_strength: 4,
|
||||||
@@ -151,6 +156,7 @@ export const jamesProfile: ProfileRow = {
|
|||||||
political_beliefs: ['libertarian'],
|
political_beliefs: ['libertarian'],
|
||||||
religious_belief_strength: null,
|
religious_belief_strength: null,
|
||||||
religious_beliefs: '',
|
religious_beliefs: '',
|
||||||
|
political_details: '',
|
||||||
photo_urls: [
|
photo_urls: [
|
||||||
'https://firebasestorage.googleapis.com/v0/b/mantic-markets.appspot.com/o/user-images%2FJamesGrugett%2Flove-images%2FKl0WtbZsZW.jpg?alt=media&token=c928604f-e5ff-4406-a229-152864a4aa48',
|
'https://firebasestorage.googleapis.com/v0/b/mantic-markets.appspot.com/o/user-images%2FJamesGrugett%2Flove-images%2FKl0WtbZsZW.jpg?alt=media&token=c928604f-e5ff-4406-a229-152864a4aa48',
|
||||||
'https://firebasestorage.googleapis.com/v0/b/mantic-markets.appspot.com/o/user-images%2FJamesGrugett%2Flove-images%2Fsii17zOItz.jpg?alt=media&token=474034b9-0d23-4005-97ad-5864abfd85fe',
|
'https://firebasestorage.googleapis.com/v0/b/mantic-markets.appspot.com/o/user-images%2FJamesGrugett%2Flove-images%2Fsii17zOItz.jpg?alt=media&token=474034b9-0d23-4005-97ad-5864abfd85fe',
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import React from 'react';
|
||||||
import {Body, Button, Column, Container, Head, Html, Preview, Row, Section, Text,} from '@react-email/components'
|
import {Body, Button, Column, Container, Head, Html, Preview, Row, Section, Text,} from '@react-email/components'
|
||||||
import {type User} from 'common/user'
|
import {type User} from 'common/user'
|
||||||
import {DOMAIN} from 'common/envs/constants'
|
import {DOMAIN} from 'common/envs/constants'
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import React from 'react';
|
||||||
import {Body, Button, Container, Head, Html, Preview, Section, Text,} from '@react-email/components'
|
import {Body, Button, Container, Head, Html, Preview, Section, Text,} from '@react-email/components'
|
||||||
import {DOMAIN} from 'common/envs/constants'
|
import {DOMAIN} from 'common/envs/constants'
|
||||||
import {type ProfileRow} from 'common/profiles/profile'
|
import {type ProfileRow} from 'common/profiles/profile'
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import React from 'react';
|
||||||
import {Body, Button, Container, Head, Html, Preview, Section, Text,} from '@react-email/components'
|
import {Body, Button, Container, Head, Html, Preview, Section, Text,} from '@react-email/components'
|
||||||
import {type User} from 'common/user'
|
import {type User} from 'common/user'
|
||||||
import {type ProfileRow} from 'common/profiles/profile'
|
import {type ProfileRow} from 'common/profiles/profile'
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import React from 'react';
|
||||||
import {Body, Container, Head, Html, Link, Preview, Section, Text,} from '@react-email/components'
|
import {Body, Container, Head, Html, Link, Preview, Section, Text,} from '@react-email/components'
|
||||||
import {type User} from 'common/user'
|
import {type User} from 'common/user'
|
||||||
import {mockUser,} from './functions/mock'
|
import {mockUser,} from './functions/mock'
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import React from 'react';
|
||||||
import {Column, Img, Link, Row, Section, Text} from "@react-email/components";
|
import {Column, Img, Link, Row, Section, Text} from "@react-email/components";
|
||||||
import {DOMAIN} from "common/envs/constants";
|
import {DOMAIN} from "common/envs/constants";
|
||||||
|
|
||||||
|
|||||||
@@ -1,29 +1,36 @@
|
|||||||
|
import React from 'react';
|
||||||
import {Body, Button, Container, Head, Html, Preview, Section, Text,} from '@react-email/components'
|
import {Body, Button, Container, Head, Html, Preview, Section, Text,} from '@react-email/components'
|
||||||
import {type User} from 'common/user'
|
import {type User} from 'common/user'
|
||||||
import {mockUser,} from './functions/mock'
|
import {mockUser,} from './functions/mock'
|
||||||
import {button, container, content, Footer, main, paragraph} from "email/utils";
|
import {button, container, content, Footer, main, paragraph} from "email/utils";
|
||||||
|
|
||||||
function randomHex(length: number) {
|
// function randomHex(length: number) {
|
||||||
const bytes = new Uint8Array(Math.ceil(length / 2));
|
// const bytes = new Uint8Array(Math.ceil(length / 2));
|
||||||
crypto.getRandomValues(bytes);
|
// crypto.getRandomValues(bytes);
|
||||||
return Array.from(bytes, b => b.toString(16).padStart(2, "0"))
|
// return Array.from(bytes, b => b.toString(16).padStart(2, "0"))
|
||||||
.join("")
|
// .join("")
|
||||||
.slice(0, length);
|
// .slice(0, length);
|
||||||
}
|
// }
|
||||||
|
|
||||||
interface WelcomeEmailProps {
|
interface WelcomeEmailProps {
|
||||||
toUser: User
|
toUser: User
|
||||||
unsubscribeUrl: string
|
unsubscribeUrl: string
|
||||||
email?: string
|
email?: string
|
||||||
|
verificationLink?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WelcomeEmail = ({
|
export const WelcomeEmail = ({
|
||||||
toUser,
|
toUser,
|
||||||
unsubscribeUrl,
|
unsubscribeUrl,
|
||||||
email,
|
email,
|
||||||
|
verificationLink,
|
||||||
}: WelcomeEmailProps) => {
|
}: WelcomeEmailProps) => {
|
||||||
const name = toUser.name.split(' ')[0]
|
const name = toUser.name.split(' ')[0]
|
||||||
const confirmUrl = `https://compassmeet.com/confirm-email/${randomHex(16)}`
|
|
||||||
|
// Some users may already have a verified email (e.g., signed it with Googl), but we send them a link anyway so that
|
||||||
|
// their email provider marks Compass as spam-free once they click the link.
|
||||||
|
// We can remove the verif link for them if we ask the user to click on another link in the email (which would not be related to email verification)
|
||||||
|
// const verificationLink = `https://compassmeet.com/confirm-email/${randomHex(16)}`
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Html>
|
<Html>
|
||||||
@@ -48,14 +55,14 @@ export const WelcomeEmail = ({
|
|||||||
|
|
||||||
<Button
|
<Button
|
||||||
style={button}
|
style={button}
|
||||||
href={confirmUrl}
|
href={verificationLink}
|
||||||
>
|
>
|
||||||
Confirm My Email
|
Confirm My Email
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Text style={{marginTop: "40px", fontSize: "10px", color: "#555"}}>
|
<Text style={{marginTop: "40px", fontSize: "10px", color: "#555"}}>
|
||||||
Or copy and paste this link into your browser: <br/>
|
Or copy and paste this link into your browser: <br/>
|
||||||
<a href={confirmUrl}>{confirmUrl}</a>
|
<a href={verificationLink}>{verificationLink}</a>
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Text style={{marginTop: "40px", fontSize: "12px", color: "#555"}}>
|
<Text style={{marginTop: "40px", fontSize: "12px", color: "#555"}}>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import * as admin from 'firebase-admin'
|
|||||||
|
|
||||||
|
|
||||||
import {getServiceAccountCredentials} from "shared/firebase-utils";
|
import {getServiceAccountCredentials} from "shared/firebase-utils";
|
||||||
import {IS_LOCAL} from "common/envs/constants";
|
import {IS_LOCAL} from "common/hosting/constants";
|
||||||
|
|
||||||
// Locally initialize Firebase Admin.
|
// Locally initialize Firebase Admin.
|
||||||
export const initAdmin = () => {
|
export const initAdmin = () => {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { format } from 'node:util'
|
|||||||
import { isError, pick, omit } from 'lodash'
|
import { isError, pick, omit } from 'lodash'
|
||||||
import { dim, red, yellow } from 'colors/safe'
|
import { dim, red, yellow } from 'colors/safe'
|
||||||
import { getMonitoringContext } from './context'
|
import { getMonitoringContext } from './context'
|
||||||
import {IS_GOOGLE_CLOUD} from "common/envs/constants";
|
import {IS_GOOGLE_CLOUD} from "common/hosting/constants";
|
||||||
|
|
||||||
// mapping JS log levels (e.g. functions on console object) to GCP log levels
|
// mapping JS log levels (e.g. functions on console object) to GCP log levels
|
||||||
const JS_TO_GCP_LEVELS = {
|
const JS_TO_GCP_LEVELS = {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import {log} from './log'
|
|||||||
import {getInstanceInfo, InstanceInfo} from './instance-info'
|
import {getInstanceInfo, InstanceInfo} from './instance-info'
|
||||||
import {chunk} from 'lodash'
|
import {chunk} from 'lodash'
|
||||||
import {CUSTOM_METRICS, metrics, MetricStore, MetricStoreEntry,} from './metrics'
|
import {CUSTOM_METRICS, metrics, MetricStore, MetricStoreEntry,} from './metrics'
|
||||||
import {IS_GOOGLE_CLOUD} from "common/envs/constants";
|
import {IS_GOOGLE_CLOUD} from "common/hosting/constants";
|
||||||
|
|
||||||
// how often metrics are written. GCP says don't write for a single time series
|
// how often metrics are written. GCP says don't write for a single time series
|
||||||
// more than once per 5 seconds.
|
// more than once per 5 seconds.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export const removePinnedUrlFromPhotoUrls = async (parsedBody: {
|
export const removePinnedUrlFromPhotoUrls = async (parsedBody: {
|
||||||
pinned_url?: string
|
pinned_url?: string
|
||||||
photo_urls?: string[]
|
photo_urls?: string[] | null
|
||||||
}) => {
|
}) => {
|
||||||
if (parsedBody.photo_urls && parsedBody.pinned_url) {
|
if (parsedBody.photo_urls && parsedBody.pinned_url) {
|
||||||
parsedBody.photo_urls = parsedBody.photo_urls.filter(
|
parsedBody.photo_urls = parsedBody.photo_urls.filter(
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
ServerMessage,
|
ServerMessage,
|
||||||
CLIENT_MESSAGE_SCHEMA,
|
CLIENT_MESSAGE_SCHEMA,
|
||||||
} from 'common/api/websockets'
|
} from 'common/api/websockets'
|
||||||
import {IS_LOCAL} from "common/envs/constants";
|
import {IS_LOCAL} from "common/hosting/constants";
|
||||||
import {getWebsocketUrl} from "common/api/utils";
|
import {getWebsocketUrl} from "common/api/utils";
|
||||||
|
|
||||||
// Extend the type definition locally
|
// Extend the type definition locally
|
||||||
|
|||||||
55
backend/supabase/migrations/20251106_add_message_actions.sql
Normal file
55
backend/supabase/migrations/20251106_add_message_actions.sql
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
-- Add columns to support message actions
|
||||||
|
ALTER TABLE private_user_messages
|
||||||
|
ADD COLUMN IF NOT EXISTS is_edited BOOLEAN DEFAULT FALSE,
|
||||||
|
ADD COLUMN IF NOT EXISTS reactions JSONB DEFAULT '{}'::jsonb,
|
||||||
|
ADD COLUMN IF NOT EXISTS deleted BOOLEAN DEFAULT FALSE,
|
||||||
|
ADD COLUMN IF NOT EXISTS edited_at TIMESTAMPTZ;
|
||||||
|
|
||||||
|
-- Create a function to update edited_at timestamp
|
||||||
|
-- CREATE OR REPLACE FUNCTION update_edited_at()
|
||||||
|
-- RETURNS TRIGGER AS $$
|
||||||
|
-- BEGIN
|
||||||
|
-- IF NEW.content <> OLD.content THEN
|
||||||
|
-- NEW.is_edited := TRUE;
|
||||||
|
-- NEW.edited_at := NOW();
|
||||||
|
-- END IF;
|
||||||
|
-- RETURN NEW;
|
||||||
|
-- END;
|
||||||
|
-- $$ LANGUAGE plpgsql;
|
||||||
|
--
|
||||||
|
-- -- Create a trigger to update edited_at when content changes
|
||||||
|
-- DROP TRIGGER IF EXISTS update_private_message_edited_at ON private_user_messages;
|
||||||
|
-- CREATE TRIGGER update_private_message_edited_at
|
||||||
|
-- BEFORE UPDATE ON private_user_messages
|
||||||
|
-- FOR EACH ROW
|
||||||
|
-- WHEN (OLD.content IS DISTINCT FROM NEW.content)
|
||||||
|
-- EXECUTE FUNCTION update_edited_at();
|
||||||
|
|
||||||
|
-- Update RLS policies to allow message owners to update their messages
|
||||||
|
-- DROP POLICY IF EXISTS "private message update" ON private_user_messages;
|
||||||
|
-- CREATE POLICY "private message update" ON private_user_messages
|
||||||
|
-- FOR UPDATE USING (
|
||||||
|
-- user_id = firebase_uid()
|
||||||
|
-- AND created_time > NOW() - INTERVAL '1 day' -- Only allow editing for 24 hours
|
||||||
|
-- AND deleted = FALSE
|
||||||
|
-- );
|
||||||
|
|
||||||
|
-- Add policy for soft delete
|
||||||
|
-- DROP POLICY IF EXISTS "private message delete" ON private_user_messages;
|
||||||
|
-- CREATE POLICY "private message delete" ON private_user_messages
|
||||||
|
-- FOR UPDATE USING (
|
||||||
|
-- user_id = firebase_uid()
|
||||||
|
-- );
|
||||||
|
|
||||||
|
-- Add policy for reactions
|
||||||
|
-- DROP POLICY IF EXISTS "private message react" ON private_user_messages;
|
||||||
|
-- CREATE POLICY "private message react" ON private_user_messages
|
||||||
|
-- FOR UPDATE USING (
|
||||||
|
-- EXISTS (
|
||||||
|
-- SELECT 1
|
||||||
|
-- FROM private_user_message_channels ch
|
||||||
|
-- JOIN private_user_message_channel_members m ON ch.id = m.channel_id
|
||||||
|
-- WHERE m.user_id = firebase_uid()
|
||||||
|
-- AND ch.id = private_user_messages.channel_id
|
||||||
|
-- )
|
||||||
|
-- );
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
|
|
||||||
DO $$
|
DO $$
|
||||||
BEGIN
|
BEGIN
|
||||||
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'profile_visibility') THEN
|
IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'profile_visibility') THEN
|
||||||
@@ -35,6 +34,7 @@ CREATE TABLE IF NOT EXISTS profiles (
|
|||||||
occupation_title TEXT,
|
occupation_title TEXT,
|
||||||
photo_urls TEXT[],
|
photo_urls TEXT[],
|
||||||
pinned_url TEXT,
|
pinned_url TEXT,
|
||||||
|
political_details TEXT,
|
||||||
political_beliefs TEXT[],
|
political_beliefs TEXT[],
|
||||||
pref_age_max INTEGER NULL,
|
pref_age_max INTEGER NULL,
|
||||||
pref_age_min INTEGER NULL,
|
pref_age_min INTEGER NULL,
|
||||||
@@ -45,12 +45,14 @@ CREATE TABLE IF NOT EXISTS profiles (
|
|||||||
region_code TEXT,
|
region_code TEXT,
|
||||||
religious_belief_strength INTEGER,
|
religious_belief_strength INTEGER,
|
||||||
religious_beliefs TEXT,
|
religious_beliefs TEXT,
|
||||||
|
religion TEXT[],
|
||||||
twitter TEXT,
|
twitter TEXT,
|
||||||
university TEXT,
|
university TEXT,
|
||||||
user_id TEXT NOT NULL,
|
user_id TEXT NOT NULL,
|
||||||
visibility profile_visibility DEFAULT 'member'::profile_visibility NOT NULL,
|
visibility profile_visibility DEFAULT 'member'::profile_visibility NOT NULL,
|
||||||
wants_kids_strength INTEGER DEFAULT 0 NOT NULL,
|
wants_kids_strength INTEGER DEFAULT 0 NOT NULL,
|
||||||
website TEXT,
|
website TEXT,
|
||||||
|
disabled BOOLEAN DEFAULT FALSE NOT NULL,
|
||||||
CONSTRAINT profiles_pkey PRIMARY KEY (id)
|
CONSTRAINT profiles_pkey PRIMARY KEY (id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
18
backend/supabase/push_subscriptions_mobile.sql
Normal file
18
backend/supabase/push_subscriptions_mobile.sql
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
create table push_subscriptions_mobile (
|
||||||
|
id serial primary key,
|
||||||
|
user_id text not null,
|
||||||
|
token text not null unique,
|
||||||
|
platform text not null, -- 'android' or 'ios'
|
||||||
|
created_at timestamptz default now(),
|
||||||
|
constraint push_subscriptions_mobile_user_id_fkey foreign KEY (user_id) references users (id) on delete CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Row Level Security
|
||||||
|
ALTER TABLE push_subscriptions_mobile ENABLE ROW LEVEL SECURITY;
|
||||||
|
|
||||||
|
-- Indexes
|
||||||
|
CREATE INDEX IF not exists user_id_idx ON push_subscriptions_mobile (user_id);
|
||||||
|
|
||||||
|
CREATE INDEX IF not exists platform_idx ON push_subscriptions_mobile (platform);
|
||||||
|
|
||||||
|
CREATE INDEX IF not exists platform_user_id_idx ON push_subscriptions_mobile (platform, user_id);
|
||||||
@@ -44,6 +44,7 @@ create or replace function get_votes_with_results(order_by text default 'recent'
|
|||||||
created_time timestamptz,
|
created_time timestamptz,
|
||||||
creator_id TEXT,
|
creator_id TEXT,
|
||||||
is_anonymous boolean,
|
is_anonymous boolean,
|
||||||
|
status text,
|
||||||
votes_for int,
|
votes_for int,
|
||||||
votes_against int,
|
votes_against int,
|
||||||
votes_abstain int,
|
votes_abstain int,
|
||||||
@@ -58,6 +59,7 @@ with results as (
|
|||||||
v.created_time,
|
v.created_time,
|
||||||
v.creator_id,
|
v.creator_id,
|
||||||
v.is_anonymous,
|
v.is_anonymous,
|
||||||
|
v.status,
|
||||||
COALESCE(SUM(CASE WHEN r.choice = 1 THEN 1 ELSE 0 END), 0) AS votes_for,
|
COALESCE(SUM(CASE WHEN r.choice = 1 THEN 1 ELSE 0 END), 0) AS votes_for,
|
||||||
COALESCE(SUM(CASE WHEN r.choice = -1 THEN 1 ELSE 0 END), 0) AS votes_against,
|
COALESCE(SUM(CASE WHEN r.choice = -1 THEN 1 ELSE 0 END), 0) AS votes_against,
|
||||||
COALESCE(SUM(CASE WHEN r.choice = 0 THEN 1 ELSE 0 END), 0) AS votes_abstain,
|
COALESCE(SUM(CASE WHEN r.choice = 0 THEN 1 ELSE 0 END), 0) AS votes_abstain,
|
||||||
@@ -73,6 +75,7 @@ SELECT
|
|||||||
created_time,
|
created_time,
|
||||||
creator_id,
|
creator_id,
|
||||||
is_anonymous,
|
is_anonymous,
|
||||||
|
status,
|
||||||
votes_for,
|
votes_for,
|
||||||
votes_against,
|
votes_against,
|
||||||
votes_abstain,
|
votes_abstain,
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ CREATE TABLE IF NOT EXISTS votes (
|
|||||||
creator_id TEXT NOT NULL,
|
creator_id TEXT NOT NULL,
|
||||||
title TEXT NOT NULL,
|
title TEXT NOT NULL,
|
||||||
is_anonymous BOOLEAN NOT NULL,
|
is_anonymous BOOLEAN NOT NULL,
|
||||||
description JSONB
|
description JSONB,
|
||||||
|
status TEXT
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Foreign Keys
|
-- Foreign Keys
|
||||||
|
|||||||
15
capacitor.config.ts
Normal file
15
capacitor.config.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import type { CapacitorConfig } from '@capacitor/cli';
|
||||||
|
|
||||||
|
const WEBVIEW_DEV_PHONE = process.env.NEXT_PUBLIC_WEBVIEW_DEV_PHONE === '1'
|
||||||
|
const LOCAL_ANDROID = WEBVIEW_DEV_PHONE || process.env.NEXT_PUBLIC_LOCAL_ANDROID === '1'
|
||||||
|
const LOCAL_URL = WEBVIEW_DEV_PHONE ? '192.168.1.3' : '10.0.2.2'
|
||||||
|
console.log("CapacitorConfig", {LOCAL_ANDROID, WEBVIEW_DEV_PHONE})
|
||||||
|
|
||||||
|
const config: CapacitorConfig = {
|
||||||
|
appId: 'com.compassconnections.app',
|
||||||
|
appName: 'Compass',
|
||||||
|
webDir: 'web/out',
|
||||||
|
server: LOCAL_ANDROID ? { url: `http://${LOCAL_URL}:3000`, cleartext: true } : {}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
@@ -238,9 +238,7 @@ export const API = (_apiTypeCheck = {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
authed: true,
|
authed: true,
|
||||||
rateLimited: true,
|
rateLimited: true,
|
||||||
props: z.object({
|
props: z.object({}),
|
||||||
username: z.string(), // just so you're sure
|
|
||||||
}),
|
|
||||||
summary: 'Delete the authenticated user account',
|
summary: 'Delete the authenticated user account',
|
||||||
tag: 'Users',
|
tag: 'Users',
|
||||||
},
|
},
|
||||||
@@ -402,6 +400,7 @@ export const API = (_apiTypeCheck = {
|
|||||||
pref_age_max: z.coerce.number().optional(),
|
pref_age_max: z.coerce.number().optional(),
|
||||||
drinks_min: z.coerce.number().optional(),
|
drinks_min: z.coerce.number().optional(),
|
||||||
drinks_max: z.coerce.number().optional(),
|
drinks_max: z.coerce.number().optional(),
|
||||||
|
religion: arraybeSchema.optional(),
|
||||||
pref_relation_styles: arraybeSchema.optional(),
|
pref_relation_styles: arraybeSchema.optional(),
|
||||||
pref_romantic_styles: arraybeSchema.optional(),
|
pref_romantic_styles: arraybeSchema.optional(),
|
||||||
diet: arraybeSchema.optional(),
|
diet: arraybeSchema.optional(),
|
||||||
@@ -575,6 +574,55 @@ export const API = (_apiTypeCheck = {
|
|||||||
summary: 'Leave a private message channel',
|
summary: 'Leave a private message channel',
|
||||||
tag: 'Messages',
|
tag: 'Messages',
|
||||||
},
|
},
|
||||||
|
'edit-message': {
|
||||||
|
method: 'POST',
|
||||||
|
authed: true,
|
||||||
|
rateLimited: true,
|
||||||
|
returns: {} as any,
|
||||||
|
props: z.object({
|
||||||
|
messageId: z.number(),
|
||||||
|
content: contentSchema,
|
||||||
|
}),
|
||||||
|
summary: 'Edit a private message',
|
||||||
|
tag: 'Messages',
|
||||||
|
},
|
||||||
|
'delete-message': {
|
||||||
|
method: 'POST',
|
||||||
|
authed: true,
|
||||||
|
rateLimited: true,
|
||||||
|
returns: {} as any,
|
||||||
|
props: z.object({
|
||||||
|
messageId: z.number(),
|
||||||
|
}),
|
||||||
|
summary: 'Delete a private message',
|
||||||
|
tag: 'Messages',
|
||||||
|
},
|
||||||
|
'react-to-message': {
|
||||||
|
method: 'POST',
|
||||||
|
authed: true,
|
||||||
|
rateLimited: true,
|
||||||
|
returns: {} as any,
|
||||||
|
props: z.object({
|
||||||
|
messageId: z.number(),
|
||||||
|
reaction: z.string(),
|
||||||
|
toDelete: z.boolean().optional(),
|
||||||
|
}),
|
||||||
|
summary: 'Add or remove a reaction to a message',
|
||||||
|
tag: 'Messages',
|
||||||
|
},
|
||||||
|
// 'get-message-reactions': {
|
||||||
|
// method: 'GET',
|
||||||
|
// authed: true,
|
||||||
|
// rateLimited: false,
|
||||||
|
// returns: {} as {
|
||||||
|
// reactions: Record<string, number>
|
||||||
|
// },
|
||||||
|
// props: z.object({
|
||||||
|
// messageId: z.string(),
|
||||||
|
// }),
|
||||||
|
// summary: 'Get reactions for a message',
|
||||||
|
// tag: 'Messages',
|
||||||
|
// },
|
||||||
'create-compatibility-question': {
|
'create-compatibility-question': {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
authed: true,
|
authed: true,
|
||||||
@@ -698,6 +746,17 @@ export const API = (_apiTypeCheck = {
|
|||||||
summary: 'Save a push/browser subscription for the user',
|
summary: 'Save a push/browser subscription for the user',
|
||||||
tag: 'Notifications',
|
tag: 'Notifications',
|
||||||
},
|
},
|
||||||
|
'save-subscription-mobile': {
|
||||||
|
method: 'POST',
|
||||||
|
authed: true,
|
||||||
|
rateLimited: true,
|
||||||
|
returns: {} as any,
|
||||||
|
props: z.object({
|
||||||
|
token: z.string(),
|
||||||
|
}),
|
||||||
|
summary: 'Save a mobile push subscription for the user',
|
||||||
|
tag: 'Notifications',
|
||||||
|
},
|
||||||
'create-bookmarked-search': {
|
'create-bookmarked-search': {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
authed: true,
|
authed: true,
|
||||||
@@ -723,6 +782,17 @@ export const API = (_apiTypeCheck = {
|
|||||||
summary: 'Delete a bookmarked search by ID',
|
summary: 'Delete a bookmarked search by ID',
|
||||||
tag: 'Searches',
|
tag: 'Searches',
|
||||||
},
|
},
|
||||||
|
// 'auth-google': {
|
||||||
|
// method: 'GET',
|
||||||
|
// authed: false,
|
||||||
|
// rateLimited: true,
|
||||||
|
// returns: {} as any,
|
||||||
|
// props: z.object({
|
||||||
|
// code: z.string(),
|
||||||
|
// }),
|
||||||
|
// summary: 'Google Auth',
|
||||||
|
// tag: 'Tokens',
|
||||||
|
// },
|
||||||
} as const)
|
} as const)
|
||||||
|
|
||||||
export type APIPath = keyof typeof API
|
export type APIPath = keyof typeof API
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import {BACKEND_DOMAIN, IS_LOCAL} from 'common/envs/constants'
|
import {BACKEND_DOMAIN} from 'common/envs/constants'
|
||||||
|
import {IS_LOCAL} from "common/hosting/constants";
|
||||||
|
|
||||||
type ErrorCode =
|
type ErrorCode =
|
||||||
| 400 // your input is bad (like zod is mad)
|
| 400 // your input is bad (like zod is mad)
|
||||||
|
|||||||
@@ -47,57 +47,56 @@ export const zBoolean = z
|
|||||||
.transform((val) => val === true || val === "true");
|
.transform((val) => val === true || val === "true");
|
||||||
|
|
||||||
export const baseProfilesSchema = z.object({
|
export const baseProfilesSchema = z.object({
|
||||||
// Required fields
|
age: z.number().min(18).max(100).optional().nullable(),
|
||||||
age: z.number().min(18).max(100).optional(),
|
|
||||||
gender: genderType,
|
|
||||||
pref_gender: genderTypes,
|
|
||||||
pref_age_min: z.number().min(18).max(100).optional(),
|
|
||||||
pref_age_max: z.number().min(18).max(100).optional(),
|
|
||||||
pref_relation_styles: z.array(z.string()),
|
|
||||||
wants_kids_strength: z.number(),
|
|
||||||
looking_for_matches: zBoolean,
|
|
||||||
photo_urls: z.array(z.string()),
|
|
||||||
visibility: z.union([z.literal('public'), z.literal('member')]),
|
|
||||||
|
|
||||||
bio: contentSchema.optional().nullable(),
|
bio: contentSchema.optional().nullable(),
|
||||||
bio_length: z.number().optional().nullable(),
|
bio_length: z.number().optional().nullable(),
|
||||||
|
|
||||||
geodb_city_id: z.string().optional(),
|
|
||||||
city: z.string(),
|
city: z.string(),
|
||||||
region_code: z.string().optional(),
|
city_latitude: z.number().optional().nullable(),
|
||||||
country: z.string().optional(),
|
city_longitude: z.number().optional().nullable(),
|
||||||
city_latitude: z.number().optional(),
|
country: z.string().optional().nullable(),
|
||||||
city_longitude: z.number().optional(),
|
gender: genderType,
|
||||||
|
geodb_city_id: z.string().optional().nullable(),
|
||||||
|
looking_for_matches: zBoolean,
|
||||||
|
photo_urls: z.array(z.string()).nullable(),
|
||||||
pinned_url: z.string(),
|
pinned_url: z.string(),
|
||||||
referred_by_username: z.string().optional(),
|
pref_age_max: z.number().min(18).max(100).optional().nullable(),
|
||||||
|
pref_age_min: z.number().min(18).max(100).optional().nullable(),
|
||||||
|
pref_gender: genderTypes.nullable(),
|
||||||
|
pref_relation_styles: z.array(z.string()).nullable(),
|
||||||
|
referred_by_username: z.string().optional().nullable(),
|
||||||
|
region_code: z.string().optional().nullable(),
|
||||||
|
visibility: z.union([z.literal('public'), z.literal('member')]),
|
||||||
|
wants_kids_strength: z.number().nullable(),
|
||||||
})
|
})
|
||||||
|
|
||||||
const optionalProfilesSchema = z.object({
|
const optionalProfilesSchema = z.object({
|
||||||
political_beliefs: z.array(z.string()).optional(),
|
avatar_url: z.string().optional().nullable(),
|
||||||
religious_belief_strength: z.number().optional(),
|
|
||||||
religious_beliefs: z.string().optional(),
|
|
||||||
ethnicity: z.array(z.string()).optional(),
|
|
||||||
born_in_location: z.string().optional(),
|
|
||||||
height_in_inches: z.number().optional(),
|
|
||||||
has_pets: zBoolean.optional().optional(),
|
|
||||||
education_level: z.string().optional(),
|
|
||||||
is_smoker: zBoolean.optional().optional(),
|
|
||||||
drinks_per_month: z.number().min(0).optional(),
|
|
||||||
diet: z.array(z.string()).optional(),
|
|
||||||
has_kids: z.number().min(0).optional(),
|
|
||||||
university: z.string().optional(),
|
|
||||||
occupation_title: z.string().optional(),
|
|
||||||
occupation: z.string().optional(),
|
|
||||||
company: z.string().optional(),
|
|
||||||
comments_enabled: zBoolean.optional().optional(),
|
|
||||||
website: z.string().optional(),
|
|
||||||
bio: contentSchema.optional().nullable(),
|
bio: contentSchema.optional().nullable(),
|
||||||
twitter: z.string().optional(),
|
born_in_location: z.string().optional().nullable(),
|
||||||
avatar_url: z.string().optional(),
|
comments_enabled: zBoolean.optional(),
|
||||||
pref_romantic_styles: z.array(z.string()),
|
company: z.string().optional().nullable(),
|
||||||
drinks_min: z.number().min(0).optional(),
|
diet: z.array(z.string()).optional().nullable(),
|
||||||
drinks_max: z.number().min(0).optional(),
|
disabled: zBoolean.optional(),
|
||||||
|
drinks_max: z.number().min(0).optional().nullable(),
|
||||||
|
drinks_min: z.number().min(0).optional().nullable(),
|
||||||
|
drinks_per_month: z.number().min(0).optional().nullable(),
|
||||||
|
education_level: z.string().optional().nullable(),
|
||||||
|
ethnicity: z.array(z.string()).optional().nullable(),
|
||||||
|
has_kids: z.number().min(0).optional().nullable(),
|
||||||
|
has_pets: zBoolean.optional().nullable(),
|
||||||
|
height_in_inches: z.number().optional().nullable(),
|
||||||
|
is_smoker: zBoolean.optional().nullable(),
|
||||||
|
occupation: z.string().optional().nullable(),
|
||||||
|
occupation_title: z.string().optional().nullable(),
|
||||||
|
political_beliefs: z.array(z.string()).optional().nullable(),
|
||||||
|
political_details: z.string().optional().nullable(),
|
||||||
|
pref_romantic_styles: z.array(z.string()).nullable(),
|
||||||
|
religion: z.array(z.string()).optional().nullable(),
|
||||||
|
religious_belief_strength: z.number().optional().nullable(),
|
||||||
|
religious_beliefs: z.string().optional().nullable(),
|
||||||
|
twitter: z.string().optional().nullable(),
|
||||||
|
university: z.string().optional().nullable(),
|
||||||
|
website: z.string().optional().nullable(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const combinedProfileSchema =
|
export const combinedProfileSchema =
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ import { type JSONContent } from '@tiptap/core'
|
|||||||
export type ChatVisibility = 'private' | 'system_status' | 'introduction'
|
export type ChatVisibility = 'private' | 'system_status' | 'introduction'
|
||||||
|
|
||||||
export type ChatMessage = {
|
export type ChatMessage = {
|
||||||
id: string
|
id: number
|
||||||
userId: string
|
userId: string
|
||||||
channelId: string
|
channelId: string
|
||||||
content: JSONContent
|
content: JSONContent
|
||||||
createdTime: number
|
createdTime: number
|
||||||
visibility: ChatVisibility
|
visibility: ChatVisibility
|
||||||
|
isEdited: boolean
|
||||||
|
reactions: any
|
||||||
}
|
}
|
||||||
export type PrivateChatMessage = Omit<ChatMessage, 'id'> & {
|
export type PrivateChatMessage = Omit<ChatMessage, 'id'> & {
|
||||||
id: number
|
id: number
|
||||||
|
|||||||
@@ -1,14 +1,16 @@
|
|||||||
export const MIN_INT = Number.MIN_SAFE_INTEGER
|
export const MIN_INT = Number.MIN_SAFE_INTEGER
|
||||||
export const MAX_INT = Number.MAX_SAFE_INTEGER
|
export const MAX_INT = Number.MAX_SAFE_INTEGER
|
||||||
|
|
||||||
export const supportEmail = 'hello@compassmeet.com';
|
export const supportEmail = 'hello@compassmeet.com'
|
||||||
// export const marketingEmail = 'hello@compassmeet.com';
|
// export const marketingEmail = 'hello@compassmeet.com'
|
||||||
|
|
||||||
export const githubRepo = "https://github.com/CompassConnections/Compass";
|
export const githubRepoSlug = "CompassConnections/Compass"
|
||||||
|
export const githubRepo = `https://github.com/${githubRepoSlug}`
|
||||||
export const githubIssues = `${githubRepo}/issues`
|
export const githubIssues = `${githubRepo}/issues`
|
||||||
|
|
||||||
export const paypalLink = "https://www.paypal.com/paypalme/CompassConnections"
|
export const paypalLink = "https://www.paypal.com/paypalme/CompassConnections"
|
||||||
export const openCollectiveLink = "https://opencollective.com/compass-connection"
|
export const openCollectiveLink = "https://opencollective.com/compass-connection"
|
||||||
|
export const liberapayLink = "https://liberapay.com/CompassConnections"
|
||||||
export const patreonLink = "https://patreon.com/CompassMeet"
|
export const patreonLink = "https://patreon.com/CompassMeet"
|
||||||
export const discordLink = "https://discord.gg/8Vd7jzqjun"
|
export const discordLink = "https://discord.gg/8Vd7jzqjun"
|
||||||
export const stoatLink = "https://stt.gg/YKQp81yA"
|
export const stoatLink = "https://stt.gg/YKQp81yA"
|
||||||
@@ -16,9 +18,11 @@ export const redditLink = "https://www.reddit.com/r/CompassConnect"
|
|||||||
export const xLink = "https://x.com/compassmeet"
|
export const xLink = "https://x.com/compassmeet"
|
||||||
export const formLink = "https://forms.gle/tKnXUMAbEreMK6FC6"
|
export const formLink = "https://forms.gle/tKnXUMAbEreMK6FC6"
|
||||||
|
|
||||||
export const pStyle = "mt-1 text-gray-800 dark:text-white whitespace-pre-line";
|
export const IS_MAINTENANCE = false // set to true to enable the maintenance mode banner
|
||||||
|
|
||||||
export const IS_MAINTENANCE = false; // set to true to enable maintenance mode banner
|
export const MIN_BIO_LENGTH = 250
|
||||||
|
|
||||||
export const MIN_BIO_LENGTH = 250;
|
export const WEB_GOOGLE_CLIENT_ID = '253367029065-khkj31qt22l0vc3v754h09vhpg6t33ad.apps.googleusercontent.com'
|
||||||
|
// export const ANDROID_GOOGLE_CLIENT_ID = '253367029065-s9sr5vqgkhc8f7p5s6ti6a4chqsrqgc4.apps.googleusercontent.com'
|
||||||
|
export const GOOGLE_CLIENT_ID = WEB_GOOGLE_CLIENT_ID
|
||||||
|
|
||||||
|
|||||||
@@ -1,24 +1,11 @@
|
|||||||
import {DEV_CONFIG} from './dev'
|
import {DEV_CONFIG} from './dev'
|
||||||
import {PROD_CONFIG} from './prod'
|
import {PROD_CONFIG} from './prod'
|
||||||
import {isProd} from "common/envs/is-prod";
|
import {isProd} from "common/envs/is-prod";
|
||||||
|
import {HOSTING_ENV, IS_LOCAL, IS_LOCAL_ANDROID, IS_WEBVIEW_DEV_PHONE} from "common/hosting/constants";
|
||||||
|
|
||||||
export const MAX_DESCRIPTION_LENGTH = 100000
|
export const MAX_DESCRIPTION_LENGTH = 100000
|
||||||
export const MAX_ANSWER_LENGTH = 240
|
export const MAX_ANSWER_LENGTH = 240
|
||||||
|
|
||||||
export const LOCAL_WEB_DOMAIN = 'localhost:3000';
|
|
||||||
export const LOCAL_BACKEND_DOMAIN = 'localhost:8088';
|
|
||||||
|
|
||||||
export const IS_GOOGLE_CLOUD = !!process.env.GOOGLE_CLOUD_PROJECT
|
|
||||||
export const IS_VERCEL = !!process.env.NEXT_PUBLIC_VERCEL
|
|
||||||
export const IS_DEPLOYED = IS_GOOGLE_CLOUD || IS_VERCEL
|
|
||||||
export const IS_LOCAL = !IS_DEPLOYED
|
|
||||||
export const HOSTING_ENV = IS_GOOGLE_CLOUD ? 'Google Cloud' : IS_VERCEL ? 'Vercel' : IS_LOCAL ? 'local' : 'unknown'
|
|
||||||
|
|
||||||
if (IS_LOCAL && !process.env.ENVIRONMENT && !process.env.NEXT_PUBLIC_FIREBASE_ENV) {
|
|
||||||
console.warn("No ENVIRONMENT set, defaulting to DEV")
|
|
||||||
process.env.ENVIRONMENT = 'DEV'
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ENV_CONFIG = isProd() ? PROD_CONFIG : DEV_CONFIG
|
export const ENV_CONFIG = isProd() ? PROD_CONFIG : DEV_CONFIG
|
||||||
|
|
||||||
export function isAdminId(id: string) {
|
export function isAdminId(id: string) {
|
||||||
@@ -30,7 +17,7 @@ export function isModId(id: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ENV = isProd() ? 'prod' : 'dev'
|
export const ENV = isProd() ? 'prod' : 'dev'
|
||||||
export const IS_PROD = ENV === 'prod'
|
// export const IS_PROD = ENV === 'prod'
|
||||||
export const IS_DEV = ENV === 'dev'
|
export const IS_DEV = ENV === 'dev'
|
||||||
|
|
||||||
console.debug(`Running in ${HOSTING_ENV} (${ENV})`,);
|
console.debug(`Running in ${HOSTING_ENV} (${ENV})`,);
|
||||||
@@ -54,11 +41,18 @@ console.debug(`Running in ${HOSTING_ENV} (${ENV})`,);
|
|||||||
// throw new MissingKeyError('firebaseConfig.apiKey')
|
// throw new MissingKeyError('firebaseConfig.apiKey')
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
export const LOCAL_WEB_DOMAIN = `localhost:3000`
|
||||||
|
export const LOCAL_BACKEND_DOMAIN = `${IS_WEBVIEW_DEV_PHONE ? '192.168.1.3' : IS_LOCAL_ANDROID ? '10.0.2.2' : 'localhost'}:8088`
|
||||||
|
|
||||||
export const DOMAIN = IS_LOCAL ? LOCAL_WEB_DOMAIN : ENV_CONFIG.domain
|
export const DOMAIN = IS_LOCAL ? LOCAL_WEB_DOMAIN : ENV_CONFIG.domain
|
||||||
|
export const DEPLOYED_WEB_URL = `https://www.${ENV_CONFIG.domain}`
|
||||||
|
export const WEB_URL = IS_LOCAL ? `http://${LOCAL_WEB_DOMAIN}` : `https://${DOMAIN}`
|
||||||
export const BACKEND_DOMAIN = IS_LOCAL ? LOCAL_BACKEND_DOMAIN : ENV_CONFIG.backendDomain
|
export const BACKEND_DOMAIN = IS_LOCAL ? LOCAL_BACKEND_DOMAIN : ENV_CONFIG.backendDomain
|
||||||
export const FIREBASE_CONFIG = ENV_CONFIG.firebaseConfig
|
export const FIREBASE_CONFIG = ENV_CONFIG.firebaseConfig
|
||||||
export const PROJECT_ID = ENV_CONFIG.firebaseConfig.projectId
|
export const PROJECT_ID = ENV_CONFIG.firebaseConfig.projectId
|
||||||
|
|
||||||
|
export const REDIRECT_URI = `${WEB_URL}/auth/callback`
|
||||||
|
|
||||||
export const AUTH_COOKIE_NAME = `FBUSER_${PROJECT_ID.toUpperCase().replace(
|
export const AUTH_COOKIE_NAME = `FBUSER_${PROJECT_ID.toUpperCase().replace(
|
||||||
/-/g,
|
/-/g,
|
||||||
'_'
|
'_'
|
||||||
@@ -75,6 +69,7 @@ export const VERIFIED_USERNAMES = [
|
|||||||
export const TEN_YEARS_SECS = 60 * 60 * 24 * 365 * 10
|
export const TEN_YEARS_SECS = 60 * 60 * 24 * 365 * 10
|
||||||
|
|
||||||
export const RESERVED_PATHS = [
|
export const RESERVED_PATHS = [
|
||||||
|
'',
|
||||||
'404',
|
'404',
|
||||||
'_app',
|
'_app',
|
||||||
'_document',
|
'_document',
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export type FilterFields = {
|
|||||||
| 'pref_gender'
|
| 'pref_gender'
|
||||||
| 'pref_age_min'
|
| 'pref_age_min'
|
||||||
| 'pref_age_max'
|
| 'pref_age_max'
|
||||||
|
| 'religion'
|
||||||
>
|
>
|
||||||
|
|
||||||
export const orderProfiles = (
|
export const orderProfiles = (
|
||||||
@@ -70,6 +71,7 @@ export const initialFilters: Partial<FilterFields> = {
|
|||||||
pref_romantic_styles: undefined,
|
pref_romantic_styles: undefined,
|
||||||
diet: undefined,
|
diet: undefined,
|
||||||
political_beliefs: undefined,
|
political_beliefs: undefined,
|
||||||
|
religion: undefined,
|
||||||
pref_gender: undefined,
|
pref_gender: undefined,
|
||||||
shortBio: undefined,
|
shortBio: undefined,
|
||||||
drinks_min: undefined,
|
drinks_min: undefined,
|
||||||
@@ -80,4 +82,4 @@ export const initialFilters: Partial<FilterFields> = {
|
|||||||
|
|
||||||
export const FilterKeys = Object.keys(initialFilters) as (keyof FilterFields)[]
|
export const FilterKeys = Object.keys(initialFilters) as (keyof FilterFields)[]
|
||||||
|
|
||||||
export type OriginLocation = { id: string; name: string, lat: number, lon: number }
|
export type OriginLocation = { id: string; name: string | null, lat: number, lon: number }
|
||||||
|
|||||||
15
common/src/hosting/constants.ts
Normal file
15
common/src/hosting/constants.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
export const IS_WEBVIEW_DEV_PHONE = process.env.NEXT_PUBLIC_WEBVIEW_DEV_PHONE === '1'
|
||||||
|
export const IS_LOCAL_ANDROID = IS_WEBVIEW_DEV_PHONE || process.env.NEXT_PUBLIC_LOCAL_ANDROID === '1'
|
||||||
|
export const IS_WEBVIEW = !!process.env.NEXT_PUBLIC_WEBVIEW
|
||||||
|
export const IS_GOOGLE_CLOUD = !!process.env.GOOGLE_CLOUD_PROJECT
|
||||||
|
export const IS_VERCEL = !!process.env.NEXT_PUBLIC_VERCEL
|
||||||
|
export const IS_DEPLOYED = IS_GOOGLE_CLOUD || IS_VERCEL || IS_WEBVIEW
|
||||||
|
export const IS_LOCAL = !IS_DEPLOYED
|
||||||
|
export const HOSTING_ENV = IS_GOOGLE_CLOUD ? 'Google Cloud' : IS_VERCEL ? 'Vercel' : IS_WEBVIEW ? 'WebView' : IS_LOCAL ? 'local' : 'unknown'
|
||||||
|
|
||||||
|
if (IS_LOCAL && !process.env.ENVIRONMENT && !process.env.NEXT_PUBLIC_FIREBASE_ENV) {
|
||||||
|
console.warn("No ENVIRONMENT set, defaulting to DEV")
|
||||||
|
process.env.ENVIRONMENT = 'DEV'
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log('IS_LOCAL_ANDROID', IS_LOCAL_ANDROID)
|
||||||
@@ -2,15 +2,15 @@ import { ProfileRow } from 'common/profiles/profile'
|
|||||||
import {MAX_INT, MIN_INT} from "common/constants";
|
import {MAX_INT, MIN_INT} from "common/constants";
|
||||||
|
|
||||||
const isPreferredGender = (
|
const isPreferredGender = (
|
||||||
preferredGenders: string[] | undefined,
|
preferredGenders: string[] | undefined | null,
|
||||||
gender: string | undefined
|
gender: string | undefined | null,
|
||||||
) => {
|
) => {
|
||||||
// console.debug('isPreferredGender', preferredGenders, gender)
|
// console.debug('isPreferredGender', preferredGenders, gender)
|
||||||
if (preferredGenders === undefined || preferredGenders.length === 0 || gender === undefined) return true
|
if (!preferredGenders?.length || !gender) return true
|
||||||
|
|
||||||
// If simple gender preference, don't include non-binary.
|
// If simple gender preference, don't include non-binary.
|
||||||
if (
|
if (
|
||||||
preferredGenders.length === 1 &&
|
preferredGenders?.length === 1 &&
|
||||||
(preferredGenders[0] === 'male' || preferredGenders[0] === 'female')
|
(preferredGenders[0] === 'male' || preferredGenders[0] === 'female')
|
||||||
) {
|
) {
|
||||||
return preferredGenders.includes(gender)
|
return preferredGenders.includes(gender)
|
||||||
@@ -43,13 +43,15 @@ export const areLocationCompatible = (profile1: ProfileRow, profile2: ProfileRow
|
|||||||
!profile2.city_latitude ||
|
!profile2.city_latitude ||
|
||||||
!profile1.city_longitude ||
|
!profile1.city_longitude ||
|
||||||
!profile2.city_longitude
|
!profile2.city_longitude
|
||||||
)
|
) {
|
||||||
|
if (!profile1.city || !profile2.city) return true
|
||||||
return profile1.city.trim().toLowerCase() === profile2.city.trim().toLowerCase()
|
return profile1.city.trim().toLowerCase() === profile2.city.trim().toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
const latitudeDiff = Math.abs(profile1.city_latitude - profile2.city_latitude)
|
const latitudeDiff = Math.abs(profile1.city_latitude - profile2.city_latitude)
|
||||||
const longigudeDiff = Math.abs(profile1.city_longitude - profile2.city_longitude)
|
const longitudeDiff = Math.abs(profile1.city_longitude - profile2.city_longitude)
|
||||||
|
|
||||||
const root = (latitudeDiff ** 2 + longigudeDiff ** 2) ** 0.5
|
const root = (latitudeDiff ** 2 + longitudeDiff ** 2) ** 0.5
|
||||||
return root < 2.5
|
return root < 2.5
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,8 +59,9 @@ export const areRelationshipStyleCompatible = (
|
|||||||
profile1: ProfileRow,
|
profile1: ProfileRow,
|
||||||
profile2: ProfileRow
|
profile2: ProfileRow
|
||||||
) => {
|
) => {
|
||||||
|
if (!profile1.pref_relation_styles?.length || !profile2.pref_relation_styles) return true
|
||||||
return profile1.pref_relation_styles.some((style) =>
|
return profile1.pref_relation_styles.some((style) =>
|
||||||
profile2.pref_relation_styles.includes(style)
|
profile2.pref_relation_styles?.includes(style)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,7 +69,7 @@ export const areWantKidsCompatible = (profile1: ProfileRow, profile2: ProfileRow
|
|||||||
const { wants_kids_strength: kids1 } = profile1
|
const { wants_kids_strength: kids1 } = profile1
|
||||||
const { wants_kids_strength: kids2 } = profile2
|
const { wants_kids_strength: kids2 } = profile2
|
||||||
|
|
||||||
if (kids1 === undefined || kids2 === undefined) return true
|
if (kids1 == null || kids2 == null) return true
|
||||||
|
|
||||||
const diff = Math.abs(kids1 - kids2)
|
const diff = Math.abs(kids1 - kids2)
|
||||||
return diff <= 2
|
return diff <= 2
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { User } from 'common/user'
|
|||||||
export type ProfileRow = Row<'profiles'>
|
export type ProfileRow = Row<'profiles'>
|
||||||
export type Profile = ProfileRow & { user: User }
|
export type Profile = ProfileRow & { user: User }
|
||||||
export const getProfileRow = async (userId: string, db: SupabaseClient) => {
|
export const getProfileRow = async (userId: string, db: SupabaseClient) => {
|
||||||
console.debug('getProfileRow', userId)
|
// console.debug('getProfileRow', userId)
|
||||||
const res = await run(db.from('profiles').select('*').eq('user_id', userId))
|
const res = await run(db.from('profiles').select('*').eq('user_id', userId))
|
||||||
return res.data[0]
|
return res.data[0]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const filterLabels: Record<string, string> = {
|
|||||||
wants_kids_strength: "Kids",
|
wants_kids_strength: "Kids",
|
||||||
is_smoker: "",
|
is_smoker: "",
|
||||||
pref_relation_styles: "Seeking",
|
pref_relation_styles: "Seeking",
|
||||||
|
religion: "",
|
||||||
pref_gender: "",
|
pref_gender: "",
|
||||||
orderBy: "",
|
orderBy: "",
|
||||||
diet: "Diet",
|
diet: "Diet",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {SecretManagerServiceClient} from '@google-cloud/secret-manager'
|
import {SecretManagerServiceClient} from '@google-cloud/secret-manager'
|
||||||
import {zip} from 'lodash'
|
import {zip} from 'lodash'
|
||||||
import {IS_LOCAL} from "common/envs/constants";
|
|
||||||
import {refreshConfig} from "common/envs/prod";
|
import {refreshConfig} from "common/envs/prod";
|
||||||
|
import {IS_LOCAL} from "common/hosting/constants";
|
||||||
|
|
||||||
// List of secrets that are available to backend (api, functions, scripts, etc.)
|
// List of secrets that are available to backend (api, functions, scripts, etc.)
|
||||||
// Edit them at:
|
// Edit them at:
|
||||||
@@ -26,6 +26,7 @@ export const secrets = (
|
|||||||
'VAPID_PUBLIC_KEY',
|
'VAPID_PUBLIC_KEY',
|
||||||
'VAPID_PRIVATE_KEY',
|
'VAPID_PRIVATE_KEY',
|
||||||
'DB_ENC_MASTER_KEY_BASE64',
|
'DB_ENC_MASTER_KEY_BASE64',
|
||||||
|
'GOOGLE_CLIENT_SECRET',
|
||||||
// Some typescript voodoo to keep the string literal types while being not readonly.
|
// Some typescript voodoo to keep the string literal types while being not readonly.
|
||||||
] as const
|
] as const
|
||||||
).concat()
|
).concat()
|
||||||
|
|||||||
@@ -270,10 +270,14 @@ export type Database = {
|
|||||||
ciphertext: string | null
|
ciphertext: string | null
|
||||||
content: Json | null
|
content: Json | null
|
||||||
created_time: string
|
created_time: string
|
||||||
|
deleted: boolean | null
|
||||||
|
edited_at: string | null
|
||||||
id: number
|
id: number
|
||||||
|
is_edited: boolean | null
|
||||||
iv: string | null
|
iv: string | null
|
||||||
|
reactions: Json | null
|
||||||
tag: string | null
|
tag: string | null
|
||||||
user_id: string
|
user_id: string | null
|
||||||
visibility: string
|
visibility: string
|
||||||
}
|
}
|
||||||
Insert: {
|
Insert: {
|
||||||
@@ -281,10 +285,14 @@ export type Database = {
|
|||||||
ciphertext?: string | null
|
ciphertext?: string | null
|
||||||
content?: Json | null
|
content?: Json | null
|
||||||
created_time?: string
|
created_time?: string
|
||||||
|
deleted?: boolean | null
|
||||||
|
edited_at?: string | null
|
||||||
id?: never
|
id?: never
|
||||||
|
is_edited?: boolean | null
|
||||||
iv?: string | null
|
iv?: string | null
|
||||||
|
reactions?: Json | null
|
||||||
tag?: string | null
|
tag?: string | null
|
||||||
user_id: string
|
user_id?: string | null
|
||||||
visibility?: string
|
visibility?: string
|
||||||
}
|
}
|
||||||
Update: {
|
Update: {
|
||||||
@@ -292,10 +300,14 @@ export type Database = {
|
|||||||
ciphertext?: string | null
|
ciphertext?: string | null
|
||||||
content?: Json | null
|
content?: Json | null
|
||||||
created_time?: string
|
created_time?: string
|
||||||
|
deleted?: boolean | null
|
||||||
|
edited_at?: string | null
|
||||||
id?: never
|
id?: never
|
||||||
|
is_edited?: boolean | null
|
||||||
iv?: string | null
|
iv?: string | null
|
||||||
|
reactions?: Json | null
|
||||||
tag?: string | null
|
tag?: string | null
|
||||||
user_id?: string
|
user_id?: string | null
|
||||||
visibility?: string
|
visibility?: string
|
||||||
}
|
}
|
||||||
Relationships: [
|
Relationships: [
|
||||||
@@ -533,7 +545,7 @@ export type Database = {
|
|||||||
bio_text: string | null
|
bio_text: string | null
|
||||||
bio_tsv: unknown
|
bio_tsv: unknown
|
||||||
born_in_location: string | null
|
born_in_location: string | null
|
||||||
city: string
|
city: string | null
|
||||||
city_latitude: number | null
|
city_latitude: number | null
|
||||||
city_longitude: number | null
|
city_longitude: number | null
|
||||||
comments_enabled: boolean
|
comments_enabled: boolean
|
||||||
@@ -541,10 +553,11 @@ export type Database = {
|
|||||||
country: string | null
|
country: string | null
|
||||||
created_time: string
|
created_time: string
|
||||||
diet: string[] | null
|
diet: string[] | null
|
||||||
|
disabled: boolean
|
||||||
drinks_per_month: number | null
|
drinks_per_month: number | null
|
||||||
education_level: string | null
|
education_level: string | null
|
||||||
ethnicity: string[] | null
|
ethnicity: string[] | null
|
||||||
gender: string
|
gender: string | null
|
||||||
geodb_city_id: string | null
|
geodb_city_id: string | null
|
||||||
has_kids: number | null
|
has_kids: number | null
|
||||||
height_in_inches: number | null
|
height_in_inches: number | null
|
||||||
@@ -558,20 +571,22 @@ export type Database = {
|
|||||||
photo_urls: string[] | null
|
photo_urls: string[] | null
|
||||||
pinned_url: string | null
|
pinned_url: string | null
|
||||||
political_beliefs: string[] | null
|
political_beliefs: string[] | null
|
||||||
|
political_details: string | null
|
||||||
pref_age_max: number | null
|
pref_age_max: number | null
|
||||||
pref_age_min: number | null
|
pref_age_min: number | null
|
||||||
pref_gender: string[]
|
pref_gender: string[] | null
|
||||||
pref_relation_styles: string[]
|
pref_relation_styles: string[] | null
|
||||||
pref_romantic_styles: string[] | null
|
pref_romantic_styles: string[] | null
|
||||||
referred_by_username: string | null
|
referred_by_username: string | null
|
||||||
region_code: string | null
|
region_code: string | null
|
||||||
|
religion: string[] | null
|
||||||
religious_belief_strength: number | null
|
religious_belief_strength: number | null
|
||||||
religious_beliefs: string | null
|
religious_beliefs: string | null
|
||||||
twitter: string | null
|
twitter: string | null
|
||||||
university: string | null
|
university: string | null
|
||||||
user_id: string
|
user_id: string
|
||||||
visibility: Database['public']['Enums']['lover_visibility']
|
visibility: Database['public']['Enums']['lover_visibility']
|
||||||
wants_kids_strength: number
|
wants_kids_strength: number | null
|
||||||
website: string | null
|
website: string | null
|
||||||
}
|
}
|
||||||
Insert: {
|
Insert: {
|
||||||
@@ -581,7 +596,7 @@ export type Database = {
|
|||||||
bio_text?: string | null
|
bio_text?: string | null
|
||||||
bio_tsv?: unknown
|
bio_tsv?: unknown
|
||||||
born_in_location?: string | null
|
born_in_location?: string | null
|
||||||
city: string
|
city?: string | null
|
||||||
city_latitude?: number | null
|
city_latitude?: number | null
|
||||||
city_longitude?: number | null
|
city_longitude?: number | null
|
||||||
comments_enabled?: boolean
|
comments_enabled?: boolean
|
||||||
@@ -589,10 +604,11 @@ export type Database = {
|
|||||||
country?: string | null
|
country?: string | null
|
||||||
created_time?: string
|
created_time?: string
|
||||||
diet?: string[] | null
|
diet?: string[] | null
|
||||||
|
disabled?: boolean
|
||||||
drinks_per_month?: number | null
|
drinks_per_month?: number | null
|
||||||
education_level?: string | null
|
education_level?: string | null
|
||||||
ethnicity?: string[] | null
|
ethnicity?: string[] | null
|
||||||
gender: string
|
gender?: string | null
|
||||||
geodb_city_id?: string | null
|
geodb_city_id?: string | null
|
||||||
has_kids?: number | null
|
has_kids?: number | null
|
||||||
height_in_inches?: number | null
|
height_in_inches?: number | null
|
||||||
@@ -606,20 +622,22 @@ export type Database = {
|
|||||||
photo_urls?: string[] | null
|
photo_urls?: string[] | null
|
||||||
pinned_url?: string | null
|
pinned_url?: string | null
|
||||||
political_beliefs?: string[] | null
|
political_beliefs?: string[] | null
|
||||||
|
political_details?: string | null
|
||||||
pref_age_max?: number | null
|
pref_age_max?: number | null
|
||||||
pref_age_min?: number | null
|
pref_age_min?: number | null
|
||||||
pref_gender: string[]
|
pref_gender?: string[] | null
|
||||||
pref_relation_styles: string[]
|
pref_relation_styles?: string[] | null
|
||||||
pref_romantic_styles?: string[] | null
|
pref_romantic_styles?: string[] | null
|
||||||
referred_by_username?: string | null
|
referred_by_username?: string | null
|
||||||
region_code?: string | null
|
region_code?: string | null
|
||||||
|
religion?: string[] | null
|
||||||
religious_belief_strength?: number | null
|
religious_belief_strength?: number | null
|
||||||
religious_beliefs?: string | null
|
religious_beliefs?: string | null
|
||||||
twitter?: string | null
|
twitter?: string | null
|
||||||
university?: string | null
|
university?: string | null
|
||||||
user_id: string
|
user_id: string
|
||||||
visibility?: Database['public']['Enums']['lover_visibility']
|
visibility?: Database['public']['Enums']['lover_visibility']
|
||||||
wants_kids_strength?: number
|
wants_kids_strength?: number | null
|
||||||
website?: string | null
|
website?: string | null
|
||||||
}
|
}
|
||||||
Update: {
|
Update: {
|
||||||
@@ -629,7 +647,7 @@ export type Database = {
|
|||||||
bio_text?: string | null
|
bio_text?: string | null
|
||||||
bio_tsv?: unknown
|
bio_tsv?: unknown
|
||||||
born_in_location?: string | null
|
born_in_location?: string | null
|
||||||
city?: string
|
city?: string | null
|
||||||
city_latitude?: number | null
|
city_latitude?: number | null
|
||||||
city_longitude?: number | null
|
city_longitude?: number | null
|
||||||
comments_enabled?: boolean
|
comments_enabled?: boolean
|
||||||
@@ -637,10 +655,11 @@ export type Database = {
|
|||||||
country?: string | null
|
country?: string | null
|
||||||
created_time?: string
|
created_time?: string
|
||||||
diet?: string[] | null
|
diet?: string[] | null
|
||||||
|
disabled?: boolean
|
||||||
drinks_per_month?: number | null
|
drinks_per_month?: number | null
|
||||||
education_level?: string | null
|
education_level?: string | null
|
||||||
ethnicity?: string[] | null
|
ethnicity?: string[] | null
|
||||||
gender?: string
|
gender?: string | null
|
||||||
geodb_city_id?: string | null
|
geodb_city_id?: string | null
|
||||||
has_kids?: number | null
|
has_kids?: number | null
|
||||||
height_in_inches?: number | null
|
height_in_inches?: number | null
|
||||||
@@ -654,20 +673,22 @@ export type Database = {
|
|||||||
photo_urls?: string[] | null
|
photo_urls?: string[] | null
|
||||||
pinned_url?: string | null
|
pinned_url?: string | null
|
||||||
political_beliefs?: string[] | null
|
political_beliefs?: string[] | null
|
||||||
|
political_details?: string | null
|
||||||
pref_age_max?: number | null
|
pref_age_max?: number | null
|
||||||
pref_age_min?: number | null
|
pref_age_min?: number | null
|
||||||
pref_gender?: string[]
|
pref_gender?: string[] | null
|
||||||
pref_relation_styles?: string[]
|
pref_relation_styles?: string[] | null
|
||||||
pref_romantic_styles?: string[] | null
|
pref_romantic_styles?: string[] | null
|
||||||
referred_by_username?: string | null
|
referred_by_username?: string | null
|
||||||
region_code?: string | null
|
region_code?: string | null
|
||||||
|
religion?: string[] | null
|
||||||
religious_belief_strength?: number | null
|
religious_belief_strength?: number | null
|
||||||
religious_beliefs?: string | null
|
religious_beliefs?: string | null
|
||||||
twitter?: string | null
|
twitter?: string | null
|
||||||
university?: string | null
|
university?: string | null
|
||||||
user_id?: string
|
user_id?: string
|
||||||
visibility?: Database['public']['Enums']['lover_visibility']
|
visibility?: Database['public']['Enums']['lover_visibility']
|
||||||
wants_kids_strength?: number
|
wants_kids_strength?: number | null
|
||||||
website?: string | null
|
website?: string | null
|
||||||
}
|
}
|
||||||
Relationships: [
|
Relationships: [
|
||||||
@@ -712,6 +733,38 @@ export type Database = {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
push_subscriptions_mobile: {
|
||||||
|
Row: {
|
||||||
|
created_at: string | null
|
||||||
|
id: number
|
||||||
|
platform: string
|
||||||
|
token: string
|
||||||
|
user_id: string
|
||||||
|
}
|
||||||
|
Insert: {
|
||||||
|
created_at?: string | null
|
||||||
|
id?: number
|
||||||
|
platform: string
|
||||||
|
token: string
|
||||||
|
user_id: string
|
||||||
|
}
|
||||||
|
Update: {
|
||||||
|
created_at?: string | null
|
||||||
|
id?: number
|
||||||
|
platform?: string
|
||||||
|
token?: string
|
||||||
|
user_id?: string
|
||||||
|
}
|
||||||
|
Relationships: [
|
||||||
|
{
|
||||||
|
foreignKeyName: 'push_subscriptions_mobile_user_id_fkey'
|
||||||
|
columns: ['user_id']
|
||||||
|
isOneToOne: false
|
||||||
|
referencedRelation: 'users'
|
||||||
|
referencedColumns: ['id']
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
reports: {
|
reports: {
|
||||||
Row: {
|
Row: {
|
||||||
content_id: string
|
content_id: string
|
||||||
@@ -939,6 +992,7 @@ export type Database = {
|
|||||||
description: Json | null
|
description: Json | null
|
||||||
id: number
|
id: number
|
||||||
is_anonymous: boolean | null
|
is_anonymous: boolean | null
|
||||||
|
status: string | null
|
||||||
title: string
|
title: string
|
||||||
}
|
}
|
||||||
Insert: {
|
Insert: {
|
||||||
@@ -947,6 +1001,7 @@ export type Database = {
|
|||||||
description?: Json | null
|
description?: Json | null
|
||||||
id?: never
|
id?: never
|
||||||
is_anonymous?: boolean | null
|
is_anonymous?: boolean | null
|
||||||
|
status?: string | null
|
||||||
title: string
|
title: string
|
||||||
}
|
}
|
||||||
Update: {
|
Update: {
|
||||||
@@ -955,6 +1010,7 @@ export type Database = {
|
|||||||
description?: Json | null
|
description?: Json | null
|
||||||
id?: never
|
id?: never
|
||||||
is_anonymous?: boolean | null
|
is_anonymous?: boolean | null
|
||||||
|
status?: string | null
|
||||||
title?: string
|
title?: string
|
||||||
}
|
}
|
||||||
Relationships: [
|
Relationships: [
|
||||||
@@ -1003,6 +1059,7 @@ export type Database = {
|
|||||||
id: number
|
id: number
|
||||||
is_anonymous: boolean
|
is_anonymous: boolean
|
||||||
priority: number
|
priority: number
|
||||||
|
status: string
|
||||||
title: string
|
title: string
|
||||||
votes_abstain: number
|
votes_abstain: number
|
||||||
votes_against: number
|
votes_against: number
|
||||||
|
|||||||
20
common/src/util/sorting.ts
Normal file
20
common/src/util/sorting.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import {MAX_INT} from "common/constants";
|
||||||
|
|
||||||
|
export function getSortedOptions(options: string[], order: string[] | Record<string, string>) {
|
||||||
|
let parsedOrder: string[]
|
||||||
|
if (Array.isArray(order)) {
|
||||||
|
parsedOrder = order
|
||||||
|
} else {
|
||||||
|
parsedOrder = Object.keys(order)
|
||||||
|
}
|
||||||
|
return options
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => {
|
||||||
|
const ia = parsedOrder.indexOf(a as any)
|
||||||
|
const ib = parsedOrder.indexOf(b as any)
|
||||||
|
const sa = ia === -1 ? MAX_INT : ia
|
||||||
|
const sb = ib === -1 ? MAX_INT : ib
|
||||||
|
if (sa !== sb) return sa - sb
|
||||||
|
return String(a).localeCompare(String(b))
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,7 +1,26 @@
|
|||||||
export const ORDER_BY = ['recent', 'mostVoted', 'priority'] as const
|
export const ORDER_BY = ['recent', 'mostVoted', 'priority'] as const
|
||||||
export type OrderBy = typeof ORDER_BY[number]
|
export type OrderBy = typeof ORDER_BY[number]
|
||||||
export const Constants: Record<OrderBy, string> = {
|
export const ORDER_BY_CHOICES: Record<OrderBy, string> = {
|
||||||
recent: 'Most recent',
|
recent: 'Most recent',
|
||||||
mostVoted: 'Most voted',
|
mostVoted: 'Most voted',
|
||||||
priority: 'Highest Priority',
|
priority: 'Highest Priority',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const STATUS_CHOICES: Record<string, string> = {
|
||||||
|
draft: "Draft",
|
||||||
|
under_review: "Under Review",
|
||||||
|
voting_open: "Voting Open",
|
||||||
|
voting_closed: "Voting Closed",
|
||||||
|
accepted: "Accepted",
|
||||||
|
pending: "Pending Implementation",
|
||||||
|
implemented: "Implemented ✔️",
|
||||||
|
rejected: "Rejected ❌",
|
||||||
|
cancelled: "Cancelled 🚫",
|
||||||
|
superseded: "Superseded",
|
||||||
|
expired: "Expired ⌛",
|
||||||
|
archived: "Archived",
|
||||||
|
}
|
||||||
|
|
||||||
|
export const REVERSED_STATUS_CHOICES: Record<string, string> = Object.fromEntries(
|
||||||
|
Object.entries(STATUS_CHOICES).map(([key, value]) => [value, key])
|
||||||
|
)
|
||||||
@@ -13,9 +13,7 @@ See those other useful documents as well:
|
|||||||
|
|
||||||
A profile field is any variable associated with a user profile, such as age, politics, diet, etc. You may want to add a new profile field if it helps people find better matches.
|
A profile field is any variable associated with a user profile, such as age, politics, diet, etc. You may want to add a new profile field if it helps people find better matches.
|
||||||
|
|
||||||
To do so, you can add code in a similar way as in [this commit](https://github.com/CompassConnections/Compass/commit/b94cdba5af377b06c31cebb97c0a772ad6324690) for the `diet` field.
|
To do so, you can add code in a similar way as in [this commit](https://github.com/CompassConnections/Compass/commit/940c1f5692f63bf72ddccd4ec3b00b1443801682) for the `religion` field. If you also want people to filter by that profile field, you'll also need to add it to the search filters, as done in [this commit](https://github.com/CompassConnections/Compass/commit/a4bb184e95553184a4c8773d7896e4b570508fe5) (for the `religion` field as well).
|
||||||
|
|
||||||
[//]: # (If you also want people to filter by that profile field, you'll also need to add it to the search filters, as done in [this commit](https://github.com/CompassConnections/Compass/commit/591798e98c51144fe257e28cf463707be748c2aa) for the education level. )
|
|
||||||
|
|
||||||
Note that you will also need to add a column to the `profiles` table in the dev database before running the code; you can do so via this SQL command (change the type if not `TEXT`):
|
Note that you will also need to add a column to the `profiles` table in the dev database before running the code; you can do so via this SQL command (change the type if not `TEXT`):
|
||||||
```sql
|
```sql
|
||||||
|
|||||||
180
docs/webview_oauth_signin.md
Normal file
180
docs/webview_oauth_signin.md
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
# WebView OAuth Sign-in
|
||||||
|
|
||||||
|
How to let a WebView-based app safely complete OAuth, even though Google blocks sign-in *inside* WebViews.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. The problem
|
||||||
|
|
||||||
|
Google OAuth refuses to complete inside a WebView.
|
||||||
|
You’ll get errors like:
|
||||||
|
|
||||||
|
```
|
||||||
|
403 disallowed_useragent
|
||||||
|
or
|
||||||
|
This browser or app may not be secure
|
||||||
|
```
|
||||||
|
|
||||||
|
This is because embedded WebViews can intercept credentials, and Google requires that the sign-in happen in a **real browser** (like Chrome, Safari, Firefox).
|
||||||
|
|
||||||
|
So we must:
|
||||||
|
|
||||||
|
1. Start the login **from inside** the WebView app.
|
||||||
|
2. Open the Google login page in the **system browser**.
|
||||||
|
3. After the user finishes signing in, Google redirects to a **custom URL** (deep link or universal link).
|
||||||
|
4. The app intercepts that redirect, extracts the `code` from it, and injects it back into the WebView.
|
||||||
|
|
||||||
|
That’s the “catch the redirect with a custom scheme or deep link” part.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. What a deep link / custom scheme is
|
||||||
|
|
||||||
|
A **custom scheme** is a URL protocol that your app owns.
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```
|
||||||
|
com.compassmeet://auth
|
||||||
|
```
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
```
|
||||||
|
compassmeet://auth
|
||||||
|
```
|
||||||
|
|
||||||
|
When Android (or iOS) sees a redirect to one of these URLs, it **launches your app** and passes it the URL data.
|
||||||
|
|
||||||
|
You register this scheme in your `AndroidManifest.xml` so Android knows which app handles it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3.
|
||||||
|
|
||||||
|
### Step 1 — Start flow inside the WebView
|
||||||
|
|
||||||
|
Your web code (running inside WebView) does:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
client_id: GOOGLE_CLIENT_ID,
|
||||||
|
redirect_uri: 'com.compassmeet://auth', // your deep link
|
||||||
|
response_type: 'code',
|
||||||
|
scope: 'openid email profile',
|
||||||
|
});
|
||||||
|
|
||||||
|
window.open(`https://accounts.google.com/o/oauth2/v2/auth?${params}`, '_system');
|
||||||
|
```
|
||||||
|
|
||||||
|
Here, `_system` (or using Capacitor Browser plugin) opens the **system browser**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 2 — User signs in (in the browser)
|
||||||
|
|
||||||
|
After login, Google redirects to your registered `redirect_uri`, e.g.:
|
||||||
|
|
||||||
|
```
|
||||||
|
com.compassmeet://auth?code=4/0AfJohXyZ...
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 3 — The app intercepts that deep link
|
||||||
|
|
||||||
|
In your **Android app code**, you register an intent filter in `AndroidManifest.xml`:
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<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.compassmeet" android:host="auth" />
|
||||||
|
</intent-filter>
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, in your app’s main activity, you listen for deep links.
|
||||||
|
In java:
|
||||||
|
```java
|
||||||
|
@Override
|
||||||
|
protected void onNewIntent(Intent intent) {
|
||||||
|
super.onNewIntent(intent);
|
||||||
|
|
||||||
|
String data = intent.getDataString();
|
||||||
|
String payload = new JSONObject().put("data", data).toString();
|
||||||
|
bridge.getWebView().post(() -> bridge.getWebView().evaluateJavascript("oauthRedirect(" + payload + ");", null));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
That line emits a custom JavaScript event inside the WebView so your web app can pick it up.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Step 4 — WebView catches redirect event and exchanges the code in backend
|
||||||
|
|
||||||
|
In your web app (TypeScript side):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
window.addEventListener('oauthRedirect', async (event: any) => {
|
||||||
|
const url = new URL(event.detail);
|
||||||
|
const code = url.searchParams.get('code');
|
||||||
|
|
||||||
|
// fetch backend API
|
||||||
|
const tokens = await api('...', {code})
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Backend endpoint
|
||||||
|
```ts
|
||||||
|
const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
body: new URLSearchParams({
|
||||||
|
client_id: GOOGLE_CLIENT_ID,
|
||||||
|
code,
|
||||||
|
redirect_uri: 'com.compassmeet://auth',
|
||||||
|
grant_type: 'authorization_code',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const tokens = await tokenResponse.json();
|
||||||
|
console.log('Tokens:', tokens);
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
At this point:
|
||||||
|
|
||||||
|
* You have your `access_token` and `id_token`.
|
||||||
|
* You can sign into Firebase or use them directly.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Why this works and what makes it safe
|
||||||
|
|
||||||
|
* The login itself happens in Google’s **system browser**, not in your WebView.
|
||||||
|
* The deep link ensures the token is delivered **only** to your app.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Universal links alternative
|
||||||
|
|
||||||
|
If you want to use a normal HTTPS redirect (e.g. `https://www.compassmeet.com/auth/callback`), you can register it as a **universal link**:
|
||||||
|
|
||||||
|
* User finishes login → redirected to your HTTPS domain.
|
||||||
|
* That URL is also registered to open your app (via Digital Asset Links JSON).
|
||||||
|
* Android recognizes it and launches your app instead of loading the page in the browser.
|
||||||
|
* The rest of the flow is the same.
|
||||||
|
|
||||||
|
However, universal links are more setup-heavy (require hosting a `.well-known/assetlinks.json` file).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Summary
|
||||||
|
|
||||||
|
| Step | What happens | Where |
|
||||||
|
| ---- | -------------------------------------------------------------- | ----------------- |
|
||||||
|
| 1 | Open Google OAuth URL | WebView |
|
||||||
|
| 2 | User signs in | System browser |
|
||||||
|
| 3 | Browser redirects to deep link (e.g. `com.compassmeet://auth`) | OS → App |
|
||||||
|
| 4 | App intercepts deep link and injects it into WebView | Native layer |
|
||||||
|
| 5 | WebView exchanges `code` with backend for tokens | Web app + backend |
|
||||||
18
package.json
18
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "compass",
|
"name": "compass",
|
||||||
"version": "1.5.0",
|
"version": "1.6.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"common",
|
"common",
|
||||||
@@ -16,14 +16,27 @@
|
|||||||
"dev": "./scripts/run_local.sh dev",
|
"dev": "./scripts/run_local.sh dev",
|
||||||
"prod": "./scripts/run_local.sh prod",
|
"prod": "./scripts/run_local.sh prod",
|
||||||
"clean-install": "./scripts/install.sh",
|
"clean-install": "./scripts/install.sh",
|
||||||
|
"build-web": "./scripts/build_web.sh",
|
||||||
|
"build-sync-android": "./scripts/build_sync_android.sh",
|
||||||
|
"sync-android": "./scripts/sync_android.sh",
|
||||||
"migrate": "./scripts/migrate.sh",
|
"migrate": "./scripts/migrate.sh",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
|
"playwright": "playwright test",
|
||||||
|
"playwright:ui": "playwright test --ui",
|
||||||
|
"playwright:debug": "playwright test --debug",
|
||||||
|
"playwright:report": "npx playwright show-report tests/reports/playwright-report",
|
||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
"test:coverage": "jest --coverage",
|
"test:coverage": "jest --coverage",
|
||||||
"test:update": "jest --updateSnapshot",
|
"test:update": "jest --updateSnapshot",
|
||||||
"postinstall": "./scripts/post_install.sh"
|
"postinstall": "./scripts/post_install.sh"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@capacitor/app": "7.1.0",
|
||||||
|
"@capacitor/core": "7.4.4",
|
||||||
|
"@capacitor/keyboard": "7.0.3",
|
||||||
|
"@capacitor/push-notifications": "7.0.3",
|
||||||
|
"@capacitor/status-bar": "7.0.3",
|
||||||
|
"@capgo/capacitor-social-login": "7.14.9",
|
||||||
"@playwright/test": "^1.54.2",
|
"@playwright/test": "^1.54.2",
|
||||||
"colorette": "^2.0.20",
|
"colorette": "^2.0.20",
|
||||||
"prismjs": "^1.30.0",
|
"prismjs": "^1.30.0",
|
||||||
@@ -32,6 +45,9 @@
|
|||||||
"react-markdown": "*"
|
"react-markdown": "*"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@capacitor/android": "7.4.4",
|
||||||
|
"@capacitor/assets": "3.0.5",
|
||||||
|
"@capacitor/cli": "7.4.4",
|
||||||
"@testing-library/dom": "^10.0.0",
|
"@testing-library/dom": "^10.0.0",
|
||||||
"@testing-library/jest-dom": "^6.6.4",
|
"@testing-library/jest-dom": "^6.6.4",
|
||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
|
|||||||
28
playwright.config.ts
Normal file
28
playwright.config.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { defineConfig, devices } from '@playwright/test';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
testDir: './tests/e2e',
|
||||||
|
fullyParallel: true,
|
||||||
|
forbidOnly: !!process.env.CI,
|
||||||
|
retries: process.env.CI ? 2 : 0,
|
||||||
|
workers: process.env.CI ? 1 : undefined,
|
||||||
|
reporter: [['html', {outputFolder: `tests/reports/playwright-report`, open: 'on-falure'}]],
|
||||||
|
use: {
|
||||||
|
baseURL: 'http://localhost:3000',
|
||||||
|
trace: 'on-first-retry',
|
||||||
|
},
|
||||||
|
projects: [
|
||||||
|
{
|
||||||
|
name: 'chromium',
|
||||||
|
use: { ...devices['Desktop Chrome'] },
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// name: 'firefox',
|
||||||
|
// use: { ...devices['Desktop Firefox'] },
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'webkit',
|
||||||
|
// use: { ...devices['Desktop Safari'] },
|
||||||
|
// },
|
||||||
|
],
|
||||||
|
});
|
||||||
23
scripts/build_android.sh
Executable file
23
scripts/build_android.sh
Executable file
@@ -0,0 +1,23 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
cd "$(dirname "$0")"/..
|
||||||
|
|
||||||
|
# keytool -genkeypair -v -keystore my-release-key.keystore -alias compass -keyalg RSA -keysize 2048 -validity 10000
|
||||||
|
|
||||||
|
# npx cap sync android
|
||||||
|
# npx cap run android
|
||||||
|
# npx cap open android
|
||||||
|
|
||||||
|
cd android
|
||||||
|
|
||||||
|
./gradlew --stop
|
||||||
|
./gradlew clean
|
||||||
|
./gradlew assembleRelease
|
||||||
|
./gradlew bundleRelease
|
||||||
|
|
||||||
|
adb install -r app-release.apk
|
||||||
|
|
||||||
|
adb logcat | grep FirebaseMessaging
|
||||||
|
|
||||||
18
scripts/build_sync_android.sh
Executable file
18
scripts/build_sync_android.sh
Executable file
@@ -0,0 +1,18 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
cd "$(dirname "$0")"/..
|
||||||
|
|
||||||
|
export NEXT_PUBLIC_WEBVIEW=1
|
||||||
|
|
||||||
|
yarn build-web
|
||||||
|
|
||||||
|
source web/.env
|
||||||
|
|
||||||
|
npx cap sync android
|
||||||
|
|
||||||
|
# To generate icons
|
||||||
|
# npx capacitor-assets generate --android
|
||||||
|
|
||||||
|
# Then go to android studio, build, generate signed APK in android/app/release, adb install -r app-release.apk
|
||||||
45
scripts/build_web.sh
Executable file
45
scripts/build_web.sh
Executable file
@@ -0,0 +1,45 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
cd "$(dirname "$0")"/..
|
||||||
|
|
||||||
|
# Paths
|
||||||
|
ROOT_ENV=".env" # your root .env
|
||||||
|
WEB_ENV="web/.env" # target for frontend
|
||||||
|
|
||||||
|
# Backup existing web/.env if it exists
|
||||||
|
if [ -f "$WEB_ENV" ]; then
|
||||||
|
cp "$WEB_ENV" "${WEB_ENV}.bak"
|
||||||
|
echo "Backed up existing $WEB_ENV to ${WEB_ENV}.bak"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Filter NEXT_PUBLIC_* lines
|
||||||
|
grep '^NEXT_PUBLIC_' "$ROOT_ENV" > "$WEB_ENV"
|
||||||
|
|
||||||
|
echo "Copied NEXT_PUBLIC_ variables to $WEB_ENV:"
|
||||||
|
|
||||||
|
echo "NEXT_PUBLIC_FIREBASE_ENV=prod" >> "$WEB_ENV"
|
||||||
|
|
||||||
|
cat "$WEB_ENV"
|
||||||
|
|
||||||
|
cd web
|
||||||
|
|
||||||
|
rm -rf .next
|
||||||
|
|
||||||
|
# Hack to ignore getStaticProps and getStaticPaths for mobile webview build
|
||||||
|
# as Next.js doesn't support SSR / ISR on mobile
|
||||||
|
USERNAME_PAGE=pages/[username]/index.tsx
|
||||||
|
|
||||||
|
# rename getStaticProps to _getStaticProps
|
||||||
|
sed -i.bak 's/\bgetStaticProps\b/_getStaticProps/g' $USERNAME_PAGE
|
||||||
|
|
||||||
|
# rename getStaticPaths to _getStaticPaths
|
||||||
|
sed -i.bak 's/\bgetStaticPaths\b/_getStaticPaths/g' $USERNAME_PAGE
|
||||||
|
|
||||||
|
yarn build
|
||||||
|
|
||||||
|
sed -i.bak 's/\b_getStaticProps\b/getStaticProps/g' $USERNAME_PAGE
|
||||||
|
|
||||||
|
# rename getStaticPaths to _getStaticPaths
|
||||||
|
sed -i.bak 's/\b_getStaticPaths\b/getStaticPaths/g' $USERNAME_PAGE
|
||||||
11
scripts/sync_android.sh
Executable file
11
scripts/sync_android.sh
Executable file
@@ -0,0 +1,11 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
cd "$(dirname "$0")"/..
|
||||||
|
|
||||||
|
export $(grep -E '^NEXT_PUBLIC_LOCAL_ANDROID=' .env)
|
||||||
|
|
||||||
|
npx cap sync android
|
||||||
|
|
||||||
|
# Then go to android studio, build, generate signed APK in android/app/release, adb install -r app-release.apk
|
||||||
0
tests/e2e/backend/.keep
Normal file
0
tests/e2e/backend/.keep
Normal file
0
tests/e2e/mobile/.keep
Normal file
0
tests/e2e/mobile/.keep
Normal file
0
tests/e2e/utils/.keep
Normal file
0
tests/e2e/utils/.keep
Normal file
0
tests/e2e/web/.auth/.keep
Normal file
0
tests/e2e/web/.auth/.keep
Normal file
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user