diff --git a/api/drizzle/20260617013849_flippant_peter_parker/migration.sql b/api/drizzle/20260617013849_flippant_peter_parker/migration.sql
new file mode 100644
index 00000000..20ec2f42
--- /dev/null
+++ b/api/drizzle/20260617013849_flippant_peter_parker/migration.sql
@@ -0,0 +1 @@
+ALTER TABLE "beep" ADD COLUMN "rider_live_activity_token" varchar(255);
\ No newline at end of file
diff --git a/api/drizzle/20260617013849_flippant_peter_parker/snapshot.json b/api/drizzle/20260617013849_flippant_peter_parker/snapshot.json
new file mode 100644
index 00000000..1b1e7b90
--- /dev/null
+++ b/api/drizzle/20260617013849_flippant_peter_parker/snapshot.json
@@ -0,0 +1,1707 @@
+{
+ "version": "8",
+ "dialect": "postgres",
+ "id": "721eac3e-185b-4bec-80b2-241c9c707b16",
+ "prevIds": [
+ "927a46d5-918f-49f9-ae18-0a4ea2ccf091"
+ ],
+ "ddl": [
+ {
+ "values": [
+ "canceled",
+ "denied",
+ "waiting",
+ "accepted",
+ "on_the_way",
+ "here",
+ "in_progress",
+ "complete"
+ ],
+ "name": "beep_status",
+ "entityType": "enums",
+ "schema": "public"
+ },
+ {
+ "values": [
+ "top_of_beeper_list_1_hour",
+ "top_of_beeper_list_2_hours",
+ "top_of_beeper_list_3_hours"
+ ],
+ "name": "payment_product",
+ "entityType": "enums",
+ "schema": "public"
+ },
+ {
+ "values": [
+ "play_store",
+ "app_store"
+ ],
+ "name": "payment_store",
+ "entityType": "enums",
+ "schema": "public"
+ },
+ {
+ "values": [
+ "sha256",
+ "bcrypt"
+ ],
+ "name": "user_password_type",
+ "entityType": "enums",
+ "schema": "public"
+ },
+ {
+ "values": [
+ "user",
+ "admin"
+ ],
+ "name": "user_role",
+ "entityType": "enums",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "beep",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "car",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "feedback",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "forgot_password",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "payment",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "rating",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "report",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "token",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "user",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "isRlsEnabled": false,
+ "name": "verify_email",
+ "entityType": "tables",
+ "schema": "public"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "beep"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "beeper_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "beep"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "rider_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "beep"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "origin",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "beep"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "destination",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "beep"
+ },
+ {
+ "type": "integer",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "group_size",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "beep"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "start",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "beep"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "end",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "beep"
+ },
+ {
+ "type": "beep_status",
+ "typeSchema": "public",
+ "notNull": true,
+ "dimensions": 0,
+ "default": "'waiting'",
+ "generated": null,
+ "identity": null,
+ "name": "status",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "beep"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "rider_live_activity_token",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "beep"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "car"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "user_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "car"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "make",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "car"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "model",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "car"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "color",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "car"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "photo",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "car"
+ },
+ {
+ "type": "integer",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "year",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "car"
+ },
+ {
+ "type": "boolean",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "false",
+ "generated": null,
+ "identity": null,
+ "name": "default",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "car"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "created",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "car"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "updated",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "car"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "feedback"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "user_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "feedback"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "message",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "feedback"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "created",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "feedback"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "forgot_password"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "user_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "forgot_password"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "time",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "forgot_password"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "payment"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "user_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "payment"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "store_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "payment"
+ },
+ {
+ "type": "payment_product",
+ "typeSchema": "public",
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "product_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "payment"
+ },
+ {
+ "type": "numeric",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "price",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "payment"
+ },
+ {
+ "type": "payment_store",
+ "typeSchema": "public",
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "store",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "payment"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "created",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "payment"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "expires",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "payment"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "rating"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "rater_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "rating"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "rated_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "rating"
+ },
+ {
+ "type": "integer",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "stars",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "rating"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "message",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "rating"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "timestamp",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "rating"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "beep_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "rating"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "report"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "reporter_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "report"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "reported_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "report"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "handled_by_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "report"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "reason",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "report"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "notes",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "report"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "timestamp",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "report"
+ },
+ {
+ "type": "boolean",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "false",
+ "generated": null,
+ "identity": null,
+ "name": "handled",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "report"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "beep_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "report"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "rating_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "report"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "token"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "tokenid",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "token"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "user_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "token"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "user"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "first",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "user"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "last",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "user"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "username",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "user"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "email",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "user"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "phone",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "user"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "venmo",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "user"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "cashapp",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "user"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "password",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "user"
+ },
+ {
+ "type": "user_password_type",
+ "typeSchema": "public",
+ "notNull": true,
+ "dimensions": 0,
+ "default": "'bcrypt'",
+ "generated": null,
+ "identity": null,
+ "name": "password_type",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "user"
+ },
+ {
+ "type": "boolean",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "false",
+ "generated": null,
+ "identity": null,
+ "name": "is_beeping",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "user"
+ },
+ {
+ "type": "boolean",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "false",
+ "generated": null,
+ "identity": null,
+ "name": "is_email_verified",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "user"
+ },
+ {
+ "type": "boolean",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "false",
+ "generated": null,
+ "identity": null,
+ "name": "is_student",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "user"
+ },
+ {
+ "type": "integer",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "4",
+ "generated": null,
+ "identity": null,
+ "name": "group_rate",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "user"
+ },
+ {
+ "type": "integer",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "3",
+ "generated": null,
+ "identity": null,
+ "name": "singles_rate",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "user"
+ },
+ {
+ "type": "integer",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "4",
+ "generated": null,
+ "identity": null,
+ "name": "capacity",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "user"
+ },
+ {
+ "type": "integer",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": "0",
+ "generated": null,
+ "identity": null,
+ "name": "queue_size",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "user"
+ },
+ {
+ "type": "numeric",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "rating",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "user"
+ },
+ {
+ "type": "user_role",
+ "typeSchema": "public",
+ "notNull": true,
+ "dimensions": 0,
+ "default": "'user'",
+ "generated": null,
+ "identity": null,
+ "name": "role",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "user"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "push_token",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "user"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "photo",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "user"
+ },
+ {
+ "type": "geometry",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "location",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "user"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": false,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "created",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "user"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "verify_email"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "user_id",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "verify_email"
+ },
+ {
+ "type": "timestamp with time zone",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "time",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "verify_email"
+ },
+ {
+ "type": "varchar(255)",
+ "typeSchema": null,
+ "notNull": true,
+ "dimensions": 0,
+ "default": null,
+ "generated": null,
+ "identity": null,
+ "name": "email",
+ "entityType": "columns",
+ "schema": "public",
+ "table": "verify_email"
+ },
+ {
+ "nameExplicit": true,
+ "columns": [
+ {
+ "value": "beeper_id",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "with": "",
+ "method": "btree",
+ "concurrently": false,
+ "name": "beeper_id_idx",
+ "entityType": "indexes",
+ "schema": "public",
+ "table": "beep"
+ },
+ {
+ "nameExplicit": true,
+ "columns": [
+ {
+ "value": "beeper_id",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ },
+ {
+ "value": "rider_id",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "with": "",
+ "method": "btree",
+ "concurrently": false,
+ "name": "beeper_id_rider_id_idx",
+ "entityType": "indexes",
+ "schema": "public",
+ "table": "beep"
+ },
+ {
+ "nameExplicit": true,
+ "columns": [
+ {
+ "value": "rider_id",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "with": "",
+ "method": "btree",
+ "concurrently": false,
+ "name": "rider_id_idx",
+ "entityType": "indexes",
+ "schema": "public",
+ "table": "beep"
+ },
+ {
+ "nameExplicit": true,
+ "columns": [
+ {
+ "value": "start",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "with": "",
+ "method": "btree",
+ "concurrently": false,
+ "name": "start_idx",
+ "entityType": "indexes",
+ "schema": "public",
+ "table": "beep"
+ },
+ {
+ "nameExplicit": true,
+ "columns": [
+ {
+ "value": "status",
+ "isExpression": false,
+ "asc": true,
+ "nullsFirst": false,
+ "opclass": null
+ }
+ ],
+ "isUnique": false,
+ "where": null,
+ "with": "",
+ "method": "btree",
+ "concurrently": false,
+ "name": "status_idx",
+ "entityType": "indexes",
+ "schema": "public",
+ "table": "beep"
+ },
+ {
+ "nameExplicit": false,
+ "columns": [
+ "beeper_id"
+ ],
+ "schemaTo": "public",
+ "tableTo": "user",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "CASCADE",
+ "onDelete": "CASCADE",
+ "name": "beep_beeper_id_user_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "beep"
+ },
+ {
+ "nameExplicit": false,
+ "columns": [
+ "rider_id"
+ ],
+ "schemaTo": "public",
+ "tableTo": "user",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "CASCADE",
+ "onDelete": "CASCADE",
+ "name": "beep_rider_id_user_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "beep"
+ },
+ {
+ "nameExplicit": false,
+ "columns": [
+ "user_id"
+ ],
+ "schemaTo": "public",
+ "tableTo": "user",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "CASCADE",
+ "onDelete": "CASCADE",
+ "name": "car_user_id_user_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "car"
+ },
+ {
+ "nameExplicit": false,
+ "columns": [
+ "user_id"
+ ],
+ "schemaTo": "public",
+ "tableTo": "user",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "CASCADE",
+ "onDelete": "CASCADE",
+ "name": "feedback_user_id_user_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "feedback"
+ },
+ {
+ "nameExplicit": false,
+ "columns": [
+ "user_id"
+ ],
+ "schemaTo": "public",
+ "tableTo": "user",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "CASCADE",
+ "onDelete": "CASCADE",
+ "name": "forgot_password_user_id_user_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "forgot_password"
+ },
+ {
+ "nameExplicit": false,
+ "columns": [
+ "user_id"
+ ],
+ "schemaTo": "public",
+ "tableTo": "user",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "CASCADE",
+ "onDelete": "CASCADE",
+ "name": "payment_user_id_user_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "payment"
+ },
+ {
+ "nameExplicit": false,
+ "columns": [
+ "rater_id"
+ ],
+ "schemaTo": "public",
+ "tableTo": "user",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "CASCADE",
+ "onDelete": "CASCADE",
+ "name": "rating_rater_id_user_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "rating"
+ },
+ {
+ "nameExplicit": false,
+ "columns": [
+ "rated_id"
+ ],
+ "schemaTo": "public",
+ "tableTo": "user",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "CASCADE",
+ "onDelete": "CASCADE",
+ "name": "rating_rated_id_user_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "rating"
+ },
+ {
+ "nameExplicit": false,
+ "columns": [
+ "beep_id"
+ ],
+ "schemaTo": "public",
+ "tableTo": "beep",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "CASCADE",
+ "onDelete": "CASCADE",
+ "name": "rating_beep_id_beep_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "rating"
+ },
+ {
+ "nameExplicit": false,
+ "columns": [
+ "reporter_id"
+ ],
+ "schemaTo": "public",
+ "tableTo": "user",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "CASCADE",
+ "onDelete": "CASCADE",
+ "name": "report_reporter_id_user_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "report"
+ },
+ {
+ "nameExplicit": false,
+ "columns": [
+ "reported_id"
+ ],
+ "schemaTo": "public",
+ "tableTo": "user",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "CASCADE",
+ "onDelete": "CASCADE",
+ "name": "report_reported_id_user_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "report"
+ },
+ {
+ "nameExplicit": false,
+ "columns": [
+ "handled_by_id"
+ ],
+ "schemaTo": "public",
+ "tableTo": "user",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "CASCADE",
+ "onDelete": "SET NULL",
+ "name": "report_handled_by_id_user_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "report"
+ },
+ {
+ "nameExplicit": false,
+ "columns": [
+ "beep_id"
+ ],
+ "schemaTo": "public",
+ "tableTo": "beep",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "CASCADE",
+ "onDelete": "SET NULL",
+ "name": "report_beep_id_beep_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "report"
+ },
+ {
+ "nameExplicit": false,
+ "columns": [
+ "rating_id"
+ ],
+ "schemaTo": "public",
+ "tableTo": "rating",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "CASCADE",
+ "onDelete": "SET NULL",
+ "name": "report_rating_id_rating_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "report"
+ },
+ {
+ "nameExplicit": false,
+ "columns": [
+ "user_id"
+ ],
+ "schemaTo": "public",
+ "tableTo": "user",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "CASCADE",
+ "onDelete": "CASCADE",
+ "name": "token_user_id_user_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "token"
+ },
+ {
+ "nameExplicit": false,
+ "columns": [
+ "user_id"
+ ],
+ "schemaTo": "public",
+ "tableTo": "user",
+ "columnsTo": [
+ "id"
+ ],
+ "onUpdate": "CASCADE",
+ "onDelete": "CASCADE",
+ "name": "verify_email_user_id_user_id_fkey",
+ "entityType": "fks",
+ "schema": "public",
+ "table": "verify_email"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "beep_pkey",
+ "schema": "public",
+ "table": "beep",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "car_pkey",
+ "schema": "public",
+ "table": "car",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "feedback_pkey",
+ "schema": "public",
+ "table": "feedback",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "forgot_password_pkey",
+ "schema": "public",
+ "table": "forgot_password",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "payment_pkey",
+ "schema": "public",
+ "table": "payment",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "rating_pkey",
+ "schema": "public",
+ "table": "rating",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "report_pkey",
+ "schema": "public",
+ "table": "report",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "token_pkey",
+ "schema": "public",
+ "table": "token",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "user_pkey",
+ "schema": "public",
+ "table": "user",
+ "entityType": "pks"
+ },
+ {
+ "columns": [
+ "id"
+ ],
+ "nameExplicit": false,
+ "name": "verify_email_pkey",
+ "schema": "public",
+ "table": "verify_email",
+ "entityType": "pks"
+ },
+ {
+ "nameExplicit": true,
+ "columns": [
+ "rater_id",
+ "beep_id"
+ ],
+ "nullsNotDistinct": false,
+ "name": "rating_beep_id_rater_id_unique",
+ "entityType": "uniques",
+ "schema": "public",
+ "table": "rating"
+ },
+ {
+ "nameExplicit": true,
+ "columns": [
+ "username"
+ ],
+ "nullsNotDistinct": false,
+ "name": "user_username_unique",
+ "entityType": "uniques",
+ "schema": "public",
+ "table": "user"
+ },
+ {
+ "nameExplicit": true,
+ "columns": [
+ "email"
+ ],
+ "nullsNotDistinct": false,
+ "name": "user_email_unique",
+ "entityType": "uniques",
+ "schema": "public",
+ "table": "user"
+ }
+ ],
+ "renames": []
+}
\ No newline at end of file
diff --git a/api/drizzle/schema.ts b/api/drizzle/schema.ts
index 767c3e6f..41e081d6 100644
--- a/api/drizzle/schema.ts
+++ b/api/drizzle/schema.ts
@@ -183,6 +183,9 @@ export const beep = pgTable(
start: timestamp("start", { withTimezone: true, mode: "date" }).notNull(),
end: timestamp("end", { withTimezone: true, mode: "date" }),
status: beepStatusEnum("status").default("waiting").notNull(),
+ rider_live_activity_token: varchar("rider_live_activity_token", {
+ length: 255,
+ }),
},
(table) => [
index("beeper_id_idx").using("btree", table.beeper_id),
diff --git a/api/package.json b/api/package.json
index 62ddd6bc..aa82ab32 100644
--- a/api/package.json
+++ b/api/package.json
@@ -28,6 +28,7 @@
"car-info": "0.2.5",
"drizzle-orm": "1.0.0-rc.3",
"ioredis": "^5.10.1",
+ "jose": "^6.2.3",
"nodemailer": "^8.0.5",
"pg": "^8.21.0",
"redlock-universal": "^0.8.4",
diff --git a/api/src/logic/beep.ts b/api/src/logic/beep.ts
index 3be806fe..7c9a723d 100644
--- a/api/src/logic/beep.ts
+++ b/api/src/logic/beep.ts
@@ -4,9 +4,9 @@ import { db } from "../utils/db";
import { beep } from "../../drizzle/schema";
import { pubSub, User } from "../utils/pubsub";
import { sendNotification } from "../utils/notifications";
+import { updateLiveActivity } from "../utils/live-activities";
type Beep = typeof beep.$inferSelect;
-type BeepStatus = Beep["status"];
export const inProgressBeep = or(
eq(beep.status, "waiting"),
@@ -103,89 +103,182 @@ export function getDerivedRiderFields(beep: Beep, queue: Beep[]) {
}
export async function sendBeepUpdateNotificationToRider(
- riderId: string,
- status: BeepStatus,
+ beep: Beep,
beeper: User,
) {
- const rider = await db.query.user.findFirst({
- columns: {
- pushToken: true,
- },
- where: { id: riderId },
- });
+ switch (beep.status) {
+ case "canceled": {
+ if (beep.rider_live_activity_token) {
+ updateLiveActivity(beep.rider_live_activity_token, {
+ action: "end",
+ name: "RiderActivity",
+ });
+ }
- if (!rider?.pushToken) {
- return;
- }
+ const riderPushToken = await getUsersPushToken(beep.rider_id);
- switch (status) {
- case "canceled":
- sendNotification({
- to: rider.pushToken,
- title: `${beeper.first} ${beeper.last} has canceled your beep 🚫`,
- body: "Open your app to find a new beep",
- });
+ if (riderPushToken) {
+ sendNotification({
+ to: riderPushToken,
+ title: `${beeper.first} ${beeper.last} has canceled your beep`,
+ body: "Open your app to find a new beep",
+ });
+ }
break;
- case "denied":
- sendNotification({
- to: rider.pushToken,
- title: `${beeper.first} ${beeper.last} has denied your beep request 🚫`,
- body: "Open your app to find a different beeper",
- });
+ }
+ case "denied": {
+ if (beep.rider_live_activity_token) {
+ updateLiveActivity(beep.rider_live_activity_token, {
+ action: "end",
+ name: "RiderActivity",
+ });
+ }
+
+ const riderPushToken = await getUsersPushToken(beep.rider_id);
+
+ if (riderPushToken) {
+ sendNotification({
+ to: riderPushToken,
+ title: `${beeper.first} ${beeper.last} has denied your beep`,
+ body: "Open your app to find a different beeper",
+ });
+ }
break;
- case "accepted":
- sendNotification({
- to: rider.pushToken,
- title: `${beeper.first} ${beeper.last} has accepted your beep request ✅`,
+ }
+ case "accepted": {
+ const alert = {
+ title: `${beeper.first} ${beeper.last} has accepted your beep request`,
body: "You will receive another notification when they are on their way to pick you up",
- });
+ };
+
+ if (beep.rider_live_activity_token) {
+ updateLiveActivity(beep.rider_live_activity_token, {
+ action: "update",
+ name: "RiderActivity",
+ props: {
+ name: `${beeper.first} ${beeper.last}`,
+ status: beep.status,
+ },
+ alert,
+ });
+ } else {
+ const riderPushToken = await getUsersPushToken(beep.rider_id);
+
+ if (riderPushToken) {
+ sendNotification({
+ to: riderPushToken,
+ ...alert,
+ });
+ }
+ }
break;
+ }
case "on_the_way": {
+ const alert = {
+ title: `${beeper.first} ${beeper.last} is on their way 🚕`,
+ body: "Your beeper is on their way.",
+ };
+
const c = await db.query.car.findFirst({
where: { user_id: beeper.id, default: true },
});
- let body = "Your beeper is on their way.";
-
if (c) {
- body = `Your beeper is on their way in a ${c.color} ${c.make} ${c.model}`;
+ alert.body = `Your beeper is on their way in a ${c.color} ${c.make} ${c.model}`;
}
- sendNotification({
- to: rider.pushToken,
- title: `${beeper.first} ${beeper.last} is on their way 🚕`,
- body,
- });
+ if (beep.rider_live_activity_token) {
+ updateLiveActivity(beep.rider_live_activity_token, {
+ action: "update",
+ alert,
+ name: "RiderActivity",
+ props: {
+ etaMinutes: 5,
+ name: `${beeper.first} ${beeper.last}`,
+ status: beep.status,
+ },
+ });
+ } else {
+ const riderPushToken = await getUsersPushToken(beep.rider_id);
+
+ if (riderPushToken) {
+ sendNotification({
+ to: riderPushToken,
+ ...alert,
+ });
+ }
+ }
break;
}
case "here": {
+ const alert = {
+ title: `${beeper.first} ${beeper.last} is here`,
+ body: "Your beeper is here to pick you up.",
+ };
const c = await db.query.car.findFirst({
where: { user_id: beeper.id, default: true },
});
- let body = "Your beeper is here to pick you up.";
-
if (c) {
- body = `Look for a ${c.color} ${c.make} ${c.model}`;
+ alert.body = `Look for a ${c.color} ${c.make} ${c.model}`;
}
- sendNotification({
- to: rider.pushToken,
- title: `${beeper.first} ${beeper.last} is here 📍`,
- body,
- });
+ if (beep.rider_live_activity_token) {
+ }
+
+ if (beep.rider_live_activity_token) {
+ updateLiveActivity(beep.rider_live_activity_token, {
+ action: "update",
+ alert,
+ name: "RiderActivity",
+ props: {
+ name: `${beeper.first} ${beeper.last}`,
+ status: beep.status,
+ },
+ });
+ } else {
+ const riderPushToken = await getUsersPushToken(beep.rider_id);
+
+ if (riderPushToken) {
+ sendNotification({
+ to: riderPushToken,
+ ...alert,
+ });
+ }
+ }
break;
}
case "in_progress":
- // Beep is in progress - no notification needed at this stage.
+ if (beep.rider_live_activity_token) {
+ updateLiveActivity(beep.rider_live_activity_token, {
+ action: "update",
+ name: "RiderActivity",
+ props: {
+ name: `${beeper.first} ${beeper.last}`,
+ status: beep.status,
+ },
+ });
+ }
break;
- case "complete":
- sendNotification({
- to: rider.pushToken,
- title: `Your beep with ${beeper.first} ${beeper.last} is complete 🎉`,
- body: "Please rate your beeper in the app.",
- });
+ case "complete": {
+ if (beep.rider_live_activity_token) {
+ updateLiveActivity(beep.rider_live_activity_token, {
+ action: "end",
+ name: "RiderActivity",
+ });
+ }
+
+ const riderPushToken = await getUsersPushToken(beep.rider_id);
+
+ if (riderPushToken) {
+ sendNotification({
+ to: riderPushToken,
+ title: `Your beep with ${beeper.first} ${beeper.last} is complete 🎉`,
+ body: "Please rate your beeper in the app.",
+ });
+ }
break;
+ }
default:
Sentry.captureException(
"Our beeper's state notification switch statement reached a point that is should not have",
@@ -193,6 +286,17 @@ export async function sendBeepUpdateNotificationToRider(
}
}
+async function getUsersPushToken(userId: string) {
+ const rider = await db.query.user.findFirst({
+ columns: {
+ pushToken: true,
+ },
+ where: { id: userId },
+ });
+
+ return rider?.pushToken ?? null;
+}
+
export async function getBeepsCount() {
return await db.$count(beep);
}
diff --git a/api/src/routers/beeper.ts b/api/src/routers/beeper.ts
index 0d293547..aa8cc365 100644
--- a/api/src/routers/beeper.ts
+++ b/api/src/routers/beeper.ts
@@ -134,11 +134,7 @@ export const beeperRouter = router({
pubSub.publish("ride", queueEntry.rider.id, { ride: null });
}
- sendBeepUpdateNotificationToRider(
- queueEntry.rider.id,
- queueEntry.status,
- ctx.user,
- );
+ sendBeepUpdateNotificationToRider(queueEntry, ctx.user);
queue = queue.filter(getIsInProgressBeep);
diff --git a/api/src/routers/rider.ts b/api/src/routers/rider.ts
index 38c8878c..45640d5d 100644
--- a/api/src/routers/rider.ts
+++ b/api/src/routers/rider.ts
@@ -155,6 +155,7 @@ export const riderRouter = router({
start: new Date(),
status: "waiting",
end: null,
+ rider_live_activity_token: null,
} as const;
const currentRide = await db.query.beep.findFirst({
@@ -474,4 +475,31 @@ export const riderRouter = router({
return mostRecentCompletedBeep;
}),
+ setBeepLiveActivityToken: authedProcedure
+ .input(z.object({ beepId: z.string(), token: z.string() }))
+ .mutation(async ({ ctx, input }) => {
+ const b = await db.query.beep.findFirst({
+ where: { id: input.beepId },
+ });
+
+ if (!b) {
+ throw new TRPCError({ code: "NOT_FOUND", message: "Beep not found." });
+ }
+
+ if (ctx.user.id !== b.rider_id) {
+ throw new TRPCError({
+ code: "FORBIDDEN",
+ message:
+ "You must be the rider of the beep to set the rider live activity token",
+ });
+ }
+ console.log("Got token", input.token);
+
+ await db
+ .update(beep)
+ .set({ rider_live_activity_token: input.token })
+ .where(eq(beep.id, b.id));
+
+ return {};
+ }),
});
diff --git a/api/src/utils/constants.ts b/api/src/utils/constants.ts
index 2f1ffced..d2b2e810 100644
--- a/api/src/utils/constants.ts
+++ b/api/src/utils/constants.ts
@@ -88,3 +88,7 @@ export const CAR_COLOR_OPTIONS = [
export const PHOTON_BASE_URL = "http://192.168.0.110:2322";
export const OSRM_BASE_URL = "http://192.168.0.104:5000";
+
+export const APPLE_APN_KEY_ID = process.env.APPLE_APN_KEY_ID;
+export const APPLE_APN_KEY = process.env.APPLE_APN_KEY;
+export const APPLE_TEAM_ID = process.env.APPLE_TEAM_ID;
diff --git a/api/src/utils/live-activities.ts b/api/src/utils/live-activities.ts
new file mode 100644
index 00000000..496182d7
--- /dev/null
+++ b/api/src/utils/live-activities.ts
@@ -0,0 +1,88 @@
+import { importPKCS8, SignJWT } from "jose";
+import http2 from "http2";
+import { APPLE_APN_KEY, APPLE_APN_KEY_ID, APPLE_TEAM_ID } from "./constants";
+
+type ActivityUpdate =
+ | {
+ name: "RiderActivity";
+ action: "update";
+ props: { status: string; etaMinutes?: number; name: string };
+ alert?: { title: string; body: string };
+ }
+ | {
+ name: "RiderActivity";
+ action: "end";
+ props?: never;
+ alert?: never;
+ };
+
+export async function updateLiveActivity(
+ deviceToken: string,
+ options: ActivityUpdate,
+) {
+ const devicePath = `/3/device/${deviceToken}`;
+
+ if (!APPLE_APN_KEY || !APPLE_TEAM_ID) {
+ throw new Error("Apple Keys not setup in the Beep API.");
+ }
+
+ const privateKey = await importPKCS8(APPLE_APN_KEY, "ES256");
+
+ const token = await new SignJWT()
+ .setIssuer(APPLE_TEAM_ID)
+ .setIssuedAt()
+ .setProtectedHeader({
+ alg: "ES256",
+ kid: APPLE_APN_KEY_ID,
+ })
+ .sign(privateKey);
+
+ const body = {
+ aps: {
+ timestamp: Date.now(),
+ event: options.action,
+ "content-state":
+ options.action === "update"
+ ? {
+ name: options.name,
+ props: JSON.stringify(options.props),
+ }
+ : {},
+ alert: options.alert,
+ ...(options.action === "end"
+ ? {
+ "dismissal-date": Date.now() / 1000,
+ }
+ : {}),
+ sound: "default",
+ },
+ };
+
+ const client = http2.connect("https://api.push.apple.com");
+
+ client.on("error", (err) => console.error(err));
+
+ const headers = {
+ ":method": "POST",
+ "apns-push-type": "liveactivity",
+ "apns-topic": "app.ridebeep.App.push-type.liveactivity",
+ "apns-priority": 10,
+ ":scheme": "https",
+ ":path": devicePath,
+ authorization: `bearer ${token}`,
+ };
+
+ const request = client.request(headers);
+
+ request.on("response", (headers, flags) => {
+ console.log("From APN:", headers, flags);
+ });
+ request.on("data", (data) => {
+ console.log(data);
+ });
+
+ request.setEncoding("utf8");
+
+ request.write(JSON.stringify(body));
+ request.end();
+}
diff --git a/app/app.config.ts b/app/app.config.ts
index 243f4e4e..c7376498 100644
--- a/app/app.config.ts
+++ b/app/app.config.ts
@@ -57,6 +57,7 @@ const config: ExpoConfig = {
}),
expoWidgets({
enablePushNotifications: true,
+ frequentUpdates: true,
}),
[
"react-native-maps",
diff --git a/app/src/app/(tabs)/(ride)/ride/index.tsx b/app/src/app/(tabs)/(ride)/ride/index.tsx
index b364c164..0b5ac8af 100644
--- a/app/src/app/(tabs)/(ride)/ride/index.tsx
+++ b/app/src/app/(tabs)/(ride)/ride/index.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect } from "react";
+import { useEffect } from "react";
import { useTRPC } from "@/utils/trpc";
import { skipToken, useQuery } from "@tanstack/react-query";
import { useSubscription } from "@trpc/tanstack-react-query";
@@ -8,7 +8,7 @@ import { BottomSheet } from "@/components/BottomSheet";
import { Pressable, View } from "react-native";
import { RideMap } from "@/components/RideMap";
import { BottomSheetView } from "@gorhom/bottom-sheet";
-import { Controller, useForm, useFormContext } from "react-hook-form";
+import { Controller, useFormContext } from "react-hook-form";
import { KeyboardAwareScrollView } from "react-native-keyboard-controller";
import { Input, TextField, FieldError } from "heroui-native";
import { LocationInput } from "@/components/LocationInput";
@@ -16,13 +16,8 @@ import { Button } from "@/components/Button";
import { BeepersMap } from "@/components/BeepersMap";
import { RideMenu } from "@/components/RideToolbar";
import { Label } from "@/components/Label";
-import {
- Link,
- SplashScreen,
- useLocalSearchParams,
- useRouter,
-} from "expo-router";
-// import { RateLastBeeper } from "@/components/RateLastBeeper";
+import { Link, SplashScreen, useRouter } from "expo-router";
+import { ensureRiderLiveActivity } from "@/live-activities/utils";
export default function MainFindBeepScreen() {
const trpc = useTRPC();
@@ -63,6 +58,10 @@ export default function MainFindBeepScreen() {
SplashScreen.hide();
}, []);
+ useEffect(() => {
+ ensureRiderLiveActivity(beep);
+ }, [beep]);
+
const router = useRouter();
const { control, handleSubmit, setFocus, getValues } = useFormContext();
diff --git a/app/src/app/(tabs)/(ride)/ride/pick.tsx b/app/src/app/(tabs)/(ride)/ride/pick.tsx
index 9d9852a4..27c144ba 100644
--- a/app/src/app/(tabs)/(ride)/ride/pick.tsx
+++ b/app/src/app/(tabs)/(ride)/ride/pick.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect } from "react";
+import { useEffect } from "react";
import { useNavigation } from "expo-router/react-navigation";
import { Avatar } from "@/components/Avatar";
import { useLocation } from "@/utils/location";
@@ -12,7 +12,7 @@ import { useQueryClient } from "@tanstack/react-query";
import { printStars } from "@/components/Stars";
import { useLocalSearchParams } from "expo-router";
import { tryCatch } from "@/utils/errors";
-import { captureException, captureMessage } from "@sentry/react-native";
+import { captureException } from "@sentry/react-native";
import { getContentContainerStyle } from "@/utils/styles";
export default function PickBeepScreen() {
diff --git a/app/src/live-activities/rider-activity.tsx b/app/src/live-activities/rider-activity.tsx
new file mode 100644
index 00000000..6ac5436c
--- /dev/null
+++ b/app/src/live-activities/rider-activity.tsx
@@ -0,0 +1,69 @@
+import { HStack, Spacer, Text, VStack } from "@expo/ui/swift-ui";
+import { font, padding } from "@expo/ui/swift-ui/modifiers";
+import { createLiveActivity, type LiveActivityEnvironment } from "expo-widgets";
+
+export interface RiderActivityProps {
+ name: string;
+ etaMinutes?: number;
+ status: string;
+}
+
+const RiderActivity = (
+ props: RiderActivityProps,
+ environment: LiveActivityEnvironment,
+) => {
+ "widget";
+
+ return {
+ banner: (
+
+ 🚕
+
+ {props.name}
+ {props.status}
+
+
+ {props.etaMinutes !== undefined && (
+
+
+ {props.etaMinutes}
+
+ minutes
+
+ )}
+
+ ),
+ compactLeading: 🚕,
+ compactTrailing: {props.etaMinutes} min,
+ minimal: 🚕,
+ expandedLeading: (
+
+ 🚕
+
+ ),
+ expandedTrailing:
+ props.etaMinutes !== undefined ? (
+
+
+ {props.etaMinutes}
+
+ minutes
+
+ ) : null,
+ expandedBottom: (
+
+
+ {props.name}
+ {props.status}
+
+
+
+ ),
+ };
+};
+
+export default createLiveActivity("RiderActivity", RiderActivity);
diff --git a/app/src/live-activities/rider-activity.web.tsx b/app/src/live-activities/rider-activity.web.tsx
new file mode 100644
index 00000000..db3abfd4
--- /dev/null
+++ b/app/src/live-activities/rider-activity.web.tsx
@@ -0,0 +1,11 @@
+const RiderActivity = {
+ getInstances: () => [],
+ start: () => {
+ return {
+ addPushTokenListener: () => {},
+ end: () => {},
+ };
+ },
+};
+
+export default RiderActivity;
diff --git a/app/src/live-activities/utils.ts b/app/src/live-activities/utils.ts
new file mode 100644
index 00000000..f7bde8ac
--- /dev/null
+++ b/app/src/live-activities/utils.ts
@@ -0,0 +1,67 @@
+import { RouterOutput, trpcClient } from "@/utils/trpc";
+import { getCurrentStatusMessage } from "@/utils/utils";
+import { LiveActivity, PushTokenEvent } from "expo-widgets";
+import RiderActivity, {
+ RiderActivityProps,
+} from "@/live-activities/rider-activity";
+
+type Beep = RouterOutput["rider"]["startBeep"];
+
+let riderActivity: LiveActivity | null =
+ RiderActivity.getInstances()[0] ?? null;
+let riderActivityTokenListener: { remove(): void } | null = null;
+
+async function handleRiderPushTokenUpdate(event: PushTokenEvent, beep: Beep) {
+ trpcClient.rider.setBeepLiveActivityToken
+ .mutate({
+ beepId: beep.id,
+ token: event.pushToken,
+ })
+ .then(() => {
+ alert(
+ `Live Activity Token sent to the API ${event.pushToken} from listener`,
+ );
+ });
+}
+
+export async function ensureRiderLiveActivity(beep: Beep | null | undefined) {
+ if (beep === undefined) {
+ return;
+ }
+
+ // If the user is not in a beep, ensure there is no live activity and listener
+ if (beep === null) {
+ if (riderActivityTokenListener) {
+ riderActivityTokenListener.remove();
+ riderActivityTokenListener = null;
+ }
+ if (riderActivity) {
+ riderActivity.end("immediate");
+ riderActivity = null;
+ }
+ return;
+ }
+
+ if (riderActivity && !riderActivityTokenListener) {
+ // If there is an active live activity, but a listener is not setup, setup one.
+ // This is so that we continue listening for tokens after the app is killed and re-opened.
+ riderActivityTokenListener = riderActivity.addPushTokenListener((event) =>
+ handleRiderPushTokenUpdate(event, beep),
+ );
+ return;
+ }
+
+ if (riderActivity && riderActivityTokenListener) {
+ return;
+ }
+
+ // User is in a beep. Start a live activity and listen for token updates
+ riderActivity = RiderActivity.start({
+ status: getCurrentStatusMessage(beep),
+ name: `${beep.beeper.first} ${beep.beeper.last}`,
+ });
+
+ riderActivityTokenListener = riderActivity.addPushTokenListener((event) =>
+ handleRiderPushTokenUpdate(event, beep),
+ );
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 6ce10fa9..4a4b58e2 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -40,6 +40,9 @@ importers:
ioredis:
specifier: ^5.10.1
version: 5.10.1
+ jose:
+ specifier: ^6.2.3
+ version: 6.2.3
nodemailer:
specifier: ^8.0.5
version: 8.0.5
@@ -4589,6 +4592,9 @@ packages:
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
hasBin: true
+ jose@6.2.3:
+ resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==}
+
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@@ -11300,6 +11306,8 @@ snapshots:
jiti@2.6.1: {}
+ jose@6.2.3: {}
+
js-tokens@4.0.0: {}
js-yaml@3.14.2: