33//! Request Textile to seat this maker, and poll until they do.
44//!
55//! Connect only registers a key. This is the last setup step: the panel
6- //! posts contact details to the venue, which emails ops. Check status
6+ //! posts contact details to the venue, which emails ops. Asked once per
7+ //! maker — approval covers every RFQ corridor on every chain, including
8+ //! pairs listed later. The email is required and the venue mails a confirm
9+ //! link beside the review; the request is reviewed either way. Check status
710//! applies corridors once approved, without rotating the key.
811
912use axum:: extract:: { Path as UrlPath , State } ;
@@ -51,6 +54,37 @@ struct AccessStatusResponse {
5154 corridors : Vec < String > ,
5255 #[ serde( default ) ]
5356 corridor_pairs : Vec < EnrollCorridorPair > ,
57+ #[ serde( default ) ]
58+ contact_email : Option < String > ,
59+ /// False until the operator clicks the confirm link. Older venues omit
60+ /// it, and "omitted" must not read as "not confirmed".
61+ #[ serde( default ) ]
62+ email_verified : Option < bool > ,
63+ }
64+
65+ /// What the venue says back to a filed request.
66+ #[ derive( Debug , Deserialize ) ]
67+ #[ serde( rename_all = "camelCase" ) ]
68+ struct AccessRequestResponse {
69+ #[ serde( default ) ]
70+ contact_email : Option < String > ,
71+ #[ serde( default ) ]
72+ email_verified : Option < bool > ,
73+ }
74+
75+ /// The line under "Request sent" / "still pending": only nags about the
76+ /// confirm link while the venue actually says it is unconfirmed.
77+ fn verify_hint ( contact_email : Option < & str > , email_verified : Option < bool > ) -> String {
78+ match ( email_verified, contact_email) {
79+ ( Some ( false ) , Some ( email) ) => format ! (
80+ " Confirm your email: we sent a link to {email}. Request access again to resend it."
81+ ) ,
82+ ( Some ( false ) , None ) => {
83+ " Confirm your email: we sent you a link. Request access again to resend it."
84+ . to_string ( )
85+ }
86+ _ => String :: new ( ) ,
87+ }
5488}
5589
5690/// Body for POST /v2/maker/access-request. The form leaves most of these
@@ -75,13 +109,13 @@ fn filled(value: Option<&str>) -> Option<&str> {
75109}
76110
77111/// Email is the channel Textile answers a review on, so it is the required
78- /// one. WhatsApp is a bonus number for them to ping .
112+ /// one — and the one they ask you to confirm . WhatsApp is a bonus number.
79113fn require_email ( email : Option < & str > ) -> Result < ( ) , ApiError > {
80114 if filled ( email) . is_some ( ) {
81115 return Ok ( ( ) ) ;
82116 }
83117 Err ( ApiError :: bad_request (
84- "add an email address so Textile can reply to your access request — WhatsApp is optional" ,
118+ "add an email address you own so Textile can reply to your access request — WhatsApp is optional" ,
85119 ) )
86120}
87121
@@ -151,10 +185,25 @@ pub async fn request_access(
151185 . unwrap_or_else ( || format ! ( "Textile access request failed ({status})" ) ) ;
152186 return Err ( ApiError :: bad_request ( message) ) ;
153187 }
188+ let filed: AccessRequestResponse =
189+ serde_json:: from_str ( & text) . unwrap_or ( AccessRequestResponse {
190+ contact_email : None ,
191+ email_verified : None ,
192+ } ) ;
193+ let hint = verify_hint (
194+ filed
195+ . contact_email
196+ . as_deref ( )
197+ . or ( filled ( body. contact_email . as_deref ( ) ) ) ,
198+ filed. email_verified ,
199+ ) ;
154200
155201 Ok ( Json ( json ! ( {
156- "message" : "Request sent. Textile will review it and email you if they need anything." ,
202+ "message" : format!(
203+ "Request sent. Textile reviews it and approves you for every Swap pair at once.{hint}"
204+ ) ,
157205 "accessStatus" : "PENDING" ,
206+ "emailVerified" : filed. email_verified,
158207 } ) )
159208 . into_response ( ) )
160209}
@@ -209,19 +258,28 @@ pub async fn access_status(
209258 } ) ?;
210259
211260 if reported. access_status != "APPROVED" || reported. flagged {
212- let message = if reported. flagged || reported . access_status == "REJECTED" {
261+ let message = if reported. flagged {
213262 format ! (
214- "Textile rejected {}. You will not receive private quotes." ,
263+ "Textile blocked {}. You will not receive private quotes." ,
264+ reported. maker_slug
265+ )
266+ } else if reported. access_status == "REJECTED" {
267+ format ! (
268+ "Textile turned {} down. You can request access again." ,
215269 reported. maker_slug
216270 )
217271 } else if reported. access_status == "PENDING" {
218- "Textile still has your request. Nothing to do until they approve it." . to_string ( )
272+ format ! (
273+ "Textile still has your request. Nothing to do until they approve it.{}" ,
274+ verify_hint( reported. contact_email. as_deref( ) , reported. email_verified)
275+ )
219276 } else {
220277 "No access request yet. Send one so Textile can review this maker." . to_string ( )
221278 } ;
222279 return Ok ( Json ( json ! ( {
223280 "message" : message,
224281 "accessStatus" : reported. access_status,
282+ "emailVerified" : reported. email_verified,
225283 "enrollment" : {
226284 "makerSlug" : reported. maker_slug,
227285 "environment" : reported. environment,
@@ -358,10 +416,7 @@ mod tests {
358416 . and_then ( |v| v. to_str ( ) . ok ( ) )
359417 . unwrap_or ( "" ) ;
360418 assert_eq ! ( auth, format!( "Bearer {expect_key}" ) ) ;
361- assert ! (
362- body[ "contactEmail" ] . as_str( ) . is_some( )
363- || body[ "contactWhatsapp" ] . as_str( ) . is_some( )
364- ) ;
419+ assert ! ( body[ "contactEmail" ] . as_str( ) . is_some( ) ) ;
365420 // The venue validates these as optional strings, so a
366421 // blank field must be absent rather than null.
367422 assert ! (
@@ -372,7 +427,12 @@ mod tests {
372427 . any( Value :: is_null) ,
373428 "sent a null field: {body}"
374429 ) ;
375- Json ( json ! ( { "accessStatus" : "PENDING" , "requestId" : "clreq1" } ) )
430+ Json ( json ! ( {
431+ "accessStatus" : "PENDING" ,
432+ "requestId" : "clreq1" ,
433+ "contactEmail" : body[ "contactEmail" ] ,
434+ "emailVerified" : false ,
435+ } ) )
376436 } ,
377437 ) ,
378438 )
@@ -564,6 +624,20 @@ mod tests {
564624 assert ! ( !body. contains( "tx_live_enroll_secret" ) , "{body}" ) ;
565625 let v = Harness :: parse ( & body) ;
566626 assert_eq ! ( v[ "accessStatus" ] , "PENDING" ) ;
627+ assert_eq ! ( v[ "emailVerified" ] , false ) ;
628+ assert ! (
629+ v[ "message" ] . as_str( ) . unwrap( ) . contains( "desk@example.com" ) ,
630+ "tells them where the confirm link went: {body}"
631+ ) ;
632+ }
633+
634+ #[ test]
635+ fn the_verify_hint_only_nags_while_unconfirmed ( ) {
636+ assert ! ( verify_hint( Some ( "a@b.c" ) , Some ( false ) ) . contains( "a@b.c" ) ) ;
637+ assert_eq ! ( verify_hint( Some ( "a@b.c" ) , Some ( true ) ) , "" ) ;
638+ // An older venue that does not report it must not read as unconfirmed.
639+ assert_eq ! ( verify_hint( Some ( "a@b.c" ) , None ) , "" ) ;
640+ assert ! ( verify_hint( None , Some ( false ) ) . contains( "Confirm your email" ) ) ;
567641 }
568642
569643 #[ tokio:: test]
0 commit comments