Skip to main content

better_auth_api/plugins/phone_number/
callbacks.rs

1use super::{PhoneNumberPlugin, PhoneOtp, PhoneVerification};
2use crate::plugins::endpoint_context::{EndpointContext, WithCallbacks};
3use better_auth_core::{AuthPlugin, AuthResult, AuthSchema, background::BackgroundFuture};
4use std::{future::Future, pin::Pin, sync::Arc};
5
6/// Borrowing future for phone callbacks with access to the active typed store.
7pub type PhoneCallbackFuture<'a, T> = Pin<Box<dyn Future<Output = AuthResult<T>> + Send + 'a>>;
8type OtpCallback<S, T> = dyn for<'a> Fn(&'a PhoneOtp, &'a EndpointContext<'_, S>) -> PhoneCallbackFuture<'a, T>
9    + Send
10    + Sync;
11type Sender<S> = dyn Fn(&PhoneOtp, &EndpointContext<'_, S>) -> AuthResult<Option<BackgroundFuture>>
12    + Send
13    + Sync;
14type VerifiedCallback<S> = dyn for<'a> Fn(&'a PhoneVerification, &'a EndpointContext<'_, S>) -> PhoneCallbackFuture<'a, ()>
15    + Send
16    + Sync;
17
18/// Phone callbacks with parsed endpoint input and the complete authentication runtime.
19pub struct PhoneNumberCallbacks<S: AuthSchema> {
20    pub(super) send: Option<Arc<Sender<S>>>,
21    pub(super) reset: Option<Arc<Sender<S>>>,
22    pub(super) verify: Option<Arc<OtpCallback<S, bool>>>,
23    pub(super) verified: Option<Arc<VerifiedCallback<S>>>,
24}
25impl<S: AuthSchema> Default for PhoneNumberCallbacks<S> {
26    fn default() -> Self {
27        Self {
28            send: None,
29            reset: None,
30            verify: None,
31            verified: None,
32        }
33    }
34}
35impl<S: AuthSchema> PhoneNumberCallbacks<S> {
36    /// Start verification delivery. Return `None` for synchronous completion.
37    /// Retain the endpoint with `to_owned` when asynchronous delivery needs its context.
38    pub fn send_otp<F>(mut self, callback: F) -> Self
39    where
40        F: Fn(&PhoneOtp, &EndpointContext<'_, S>) -> AuthResult<Option<BackgroundFuture>>
41            + Send
42            + Sync
43            + 'static,
44    {
45        self.send = Some(Arc::new(callback));
46        self
47    }
48    /// Start password reset delivery. Return `None` for synchronous completion.
49    pub fn send_password_reset_otp<F>(mut self, callback: F) -> Self
50    where
51        F: Fn(&PhoneOtp, &EndpointContext<'_, S>) -> AuthResult<Option<BackgroundFuture>>
52            + Send
53            + Sync
54            + 'static,
55    {
56        self.reset = Some(Arc::new(callback));
57        self
58    }
59    /// Verify a code using an external provider.
60    pub fn verify_otp<F>(mut self, callback: F) -> Self
61    where
62        F: for<'a> Fn(&'a PhoneOtp, &'a EndpointContext<'_, S>) -> PhoneCallbackFuture<'a, bool>
63            + Send
64            + Sync
65            + 'static,
66    {
67        self.verify = Some(Arc::new(callback));
68        self
69    }
70    /// Observe the persisted user before a session is created.
71    pub fn on_verification<F>(mut self, callback: F) -> Self
72    where
73        F: for<'a> Fn(
74                &'a PhoneVerification,
75                &'a EndpointContext<'_, S>,
76            ) -> PhoneCallbackFuture<'a, ()>
77            + Send
78            + Sync
79            + 'static,
80    {
81        self.verified = Some(Arc::new(callback));
82        self
83    }
84}
85#[derive(Clone, Copy)]
86pub(super) enum Delivery {
87    Verification,
88    PasswordReset,
89}
90impl PhoneNumberPlugin {
91    /// Attach schema-aware callbacks after configuring the plugin options.
92    pub fn callbacks<S: AuthSchema>(
93        self,
94        callbacks: PhoneNumberCallbacks<S>,
95    ) -> impl AuthPlugin<S> {
96        WithCallbacks {
97            plugin: self,
98            callbacks: Arc::new(callbacks),
99        }
100    }
101    pub(super) fn has_sender<S: AuthSchema>(
102        &self,
103        ctx: &better_auth_core::AuthContext<S>,
104        kind: Delivery,
105    ) -> bool {
106        let callbacks = ctx.extensions.get::<Arc<PhoneNumberCallbacks<S>>>();
107        match kind {
108            Delivery::Verification => {
109                self.send_otp.is_some()
110                    || callbacks.is_some_and(|callbacks| callbacks.send.is_some())
111            }
112            Delivery::PasswordReset => {
113                self.send_password_reset_otp.is_some()
114                    || callbacks.is_some_and(|callbacks| callbacks.reset.is_some())
115            }
116        }
117    }
118    pub(super) fn delivery<S: AuthSchema>(
119        &self,
120        otp: PhoneOtp,
121        endpoint: &EndpointContext<'_, S>,
122        kind: Delivery,
123    ) -> AuthResult<Option<BackgroundFuture>> {
124        let callbacks = endpoint
125            .auth
126            .extensions
127            .get::<Arc<PhoneNumberCallbacks<S>>>();
128        let (typed, legacy) = match kind {
129            Delivery::Verification => (
130                callbacks.and_then(|callbacks| callbacks.send.as_ref()),
131                self.send_otp.as_ref(),
132            ),
133            Delivery::PasswordReset => (
134                callbacks.and_then(|callbacks| callbacks.reset.as_ref()),
135                self.send_password_reset_otp.as_ref(),
136            ),
137        };
138        if let Some(callback) = typed {
139            callback(&otp, endpoint)
140        } else if let Some(callback) = legacy {
141            let request = endpoint.request.ok_or_else(|| {
142                better_auth_core::AuthError::config("Legacy phone callbacks require a request")
143            })?;
144            Ok(Some(callback(otp, request.clone())))
145        } else {
146            Err(super::error(
147                501,
148                "SEND_OTP_NOT_IMPLEMENTED",
149                "sendOTP not implemented",
150            ))
151        }
152    }
153    pub(super) async fn notify_verified<S: AuthSchema>(
154        &self,
155        data: PhoneVerification,
156        endpoint: &EndpointContext<'_, S>,
157    ) -> AuthResult<()> {
158        if let Some(callback) = endpoint
159            .auth
160            .extensions
161            .get::<Arc<PhoneNumberCallbacks<S>>>()
162            .and_then(|callbacks| callbacks.verified.as_ref())
163        {
164            callback(&data, endpoint).await?;
165        } else if let Some(callback) = &self.callback_on_verification {
166            let request = endpoint.request.ok_or_else(|| {
167                better_auth_core::AuthError::config("Legacy phone callbacks require a request")
168            })?;
169            callback(data, request.clone()).await?;
170        }
171        Ok(())
172    }
173}