hypercall_api/handlers/
push.rs1use axum::extract::State;
11use serde::{Deserialize, Serialize};
12use utoipa::ToSchema;
13
14use super::AppState;
15use crate::error::ApiError;
16use crate::middleware::SignerContext;
17use crate::sonic_json::SonicJson;
18
19#[derive(Debug, Deserialize, ToSchema)]
24pub struct PushSubscribeRequest {
25 pub endpoint: String,
26 pub auth_key: String,
27 pub p256dh_key: String,
28 pub preferences: Option<serde_json::Value>,
30}
31
32#[derive(Debug, Serialize, ToSchema)]
33pub struct PushSubscribeResponse {
34 pub ok: bool,
35}
36
37#[derive(Debug, Deserialize, ToSchema)]
38pub struct PushUnsubscribeRequest {
39 pub endpoint: String,
40}
41
42#[derive(Debug, Serialize, ToSchema)]
43pub struct PushUnsubscribeResponse {
44 pub deleted: bool,
45}
46
47#[derive(Debug, Deserialize, ToSchema)]
48pub struct PushPreferencesRequest {
49 pub endpoint: String,
50 pub preferences: serde_json::Value,
52}
53
54#[derive(Debug, Serialize, ToSchema)]
55pub struct PushPreferencesResponse {
56 pub updated: bool,
57}
58
59#[utoipa::path(
65 post,
66 path = "/push/subscribe",
67 request_body = PushSubscribeRequest,
68 responses(
69 (status = 200, description = "Subscription registered", body = PushSubscribeResponse),
70 (status = 400, description = "Invalid request"),
71 (status = 500, description = "Internal server error"),
72 ),
73 security(("eip712_signature" = [])),
74 tag = "Push Notifications"
75)]
76pub async fn push_subscribe(
77 State(state): State<AppState>,
78 signer_ctx: SignerContext,
79 SonicJson(request): SonicJson<PushSubscribeRequest>,
80) -> Result<SonicJson<PushSubscribeResponse>, ApiError> {
81 let wallet = format!("{:#x}", signer_ctx.wallet_address.0);
82
83 let Some(ref push_service) = state.push_service else {
84 return Err(ApiError::internal_error(
85 "Push notifications not configured",
86 ));
87 };
88
89 push_service
90 .subscribe(
91 &wallet,
92 &request.endpoint,
93 &request.auth_key,
94 &request.p256dh_key,
95 request.preferences,
96 )
97 .await
98 .map_err(|e| {
99 let msg = e.to_string();
100 if msg.contains("not from a recognized push service")
102 || msg.contains("Maximum push subscriptions")
103 {
104 return ApiError::bad_request(msg);
105 }
106 tracing::error!("push_subscribe failed: {e}");
107 ApiError::internal_error("Failed to register push subscription")
108 })?;
109
110 Ok(SonicJson(PushSubscribeResponse { ok: true }))
111}
112
113#[utoipa::path(
115 post,
116 path = "/push/unsubscribe",
117 request_body = PushUnsubscribeRequest,
118 responses(
119 (status = 200, description = "Subscription removed", body = PushUnsubscribeResponse),
120 (status = 500, description = "Internal server error"),
121 ),
122 security(("eip712_signature" = [])),
123 tag = "Push Notifications"
124)]
125pub async fn push_unsubscribe(
126 State(state): State<AppState>,
127 signer_ctx: SignerContext,
128 SonicJson(request): SonicJson<PushUnsubscribeRequest>,
129) -> Result<SonicJson<PushUnsubscribeResponse>, ApiError> {
130 let wallet = format!("{:#x}", signer_ctx.wallet_address.0);
131
132 let Some(ref push_service) = state.push_service else {
133 return Err(ApiError::internal_error(
134 "Push notifications not configured",
135 ));
136 };
137
138 let deleted = push_service
139 .unsubscribe(&wallet, &request.endpoint)
140 .await
141 .map_err(|e| {
142 tracing::error!("push_unsubscribe failed: {e}");
143 ApiError::internal_error("Failed to remove push subscription")
144 })?;
145
146 Ok(SonicJson(PushUnsubscribeResponse { deleted }))
147}
148
149#[utoipa::path(
151 post,
152 path = "/push/preferences",
153 request_body = PushPreferencesRequest,
154 responses(
155 (status = 200, description = "Preferences updated", body = PushPreferencesResponse),
156 (status = 500, description = "Internal server error"),
157 ),
158 security(("eip712_signature" = [])),
159 tag = "Push Notifications"
160)]
161pub async fn push_preferences(
162 State(state): State<AppState>,
163 signer_ctx: SignerContext,
164 SonicJson(request): SonicJson<PushPreferencesRequest>,
165) -> Result<SonicJson<PushPreferencesResponse>, ApiError> {
166 let wallet = format!("{:#x}", signer_ctx.wallet_address.0);
167
168 let Some(ref push_service) = state.push_service else {
169 return Err(ApiError::internal_error(
170 "Push notifications not configured",
171 ));
172 };
173
174 let updated = push_service
175 .update_preferences(&wallet, &request.endpoint, request.preferences)
176 .await
177 .map_err(|e| {
178 tracing::error!("push_preferences failed: {e}");
179 ApiError::internal_error("Failed to update notification preferences")
180 })?;
181
182 Ok(SonicJson(PushPreferencesResponse { updated }))
183}