@iamgame/wallet-sdk 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -80,6 +80,23 @@ var SolvenClient = class {
80
80
  this.opts.storage.set(session);
81
81
  return session;
82
82
  }
83
+ /** Email + OTP: send a one-time code to the address. */
84
+ async initiateEmail(email) {
85
+ return this.publicCall(
86
+ "POST",
87
+ "/auth/email/initiate",
88
+ { email }
89
+ );
90
+ }
91
+ /** Email + OTP: exchange the code for a session. */
92
+ async verifyEmail(email, code) {
93
+ const session = await this.publicCall("POST", "/auth/email/verify", {
94
+ email,
95
+ code
96
+ });
97
+ this.opts.storage.set(session);
98
+ return session;
99
+ }
83
100
  async refreshSession() {
84
101
  const current = this.opts.storage.get();
85
102
  if (!current) return null;
@@ -254,7 +271,7 @@ function useSolvenContext() {
254
271
  if (!ctx) throw new Error("useSolvenContext must be inside a <SolvenProvider />");
255
272
  return ctx;
256
273
  }
257
- function SolvenProvider({ children, ...opts }) {
274
+ function SolvenProvider({ children, autoTelegram = true, ...opts }) {
258
275
  const client = react.useMemo(() => new SolvenClient(opts), [
259
276
  opts.publishableKey,
260
277
  opts.baseUrl,
@@ -295,6 +312,32 @@ function SolvenProvider({ children, ...opts }) {
295
312
  cancelled = true;
296
313
  };
297
314
  }, [client]);
315
+ const triedTelegram = react.useRef(false);
316
+ react.useEffect(() => {
317
+ if (!autoTelegram || status !== "anonymous" || triedTelegram.current) return;
318
+ triedTelegram.current = true;
319
+ let cancelled = false;
320
+ (async () => {
321
+ let initData = getTelegramInitData();
322
+ for (let i = 0; i < 4 && !initData; i++) {
323
+ await new Promise((r) => setTimeout(r, 400));
324
+ if (cancelled) return;
325
+ initData = getTelegramInitData();
326
+ }
327
+ if (!initData) return;
328
+ try {
329
+ notifyTelegramReady();
330
+ const next = await client.verifyTelegram({ initData });
331
+ if (cancelled) return;
332
+ setSession(next);
333
+ setStatus("authenticated");
334
+ } catch {
335
+ }
336
+ })();
337
+ return () => {
338
+ cancelled = true;
339
+ };
340
+ }, [autoTelegram, status, client]);
298
341
  const value = {
299
342
  client,
300
343
  session,
@@ -327,6 +370,20 @@ function useSolvenAuth() {
327
370
  const session2 = await client.verifyTelegram({ initData });
328
371
  setSession(session2);
329
372
  }, [client, setSession]);
373
+ const requestEmailOtp = react.useCallback(
374
+ async (email) => {
375
+ const { expiresAt } = await client.initiateEmail(email);
376
+ return { expiresAt };
377
+ },
378
+ [client]
379
+ );
380
+ const connectEmail = react.useCallback(
381
+ async (email, code) => {
382
+ const session2 = await client.verifyEmail(email, code);
383
+ setSession(session2);
384
+ },
385
+ [client, setSession]
386
+ );
330
387
  const logout = react.useCallback(async () => {
331
388
  await client.logout();
332
389
  setSession(null);
@@ -336,6 +393,8 @@ function useSolvenAuth() {
336
393
  status,
337
394
  connectExternal,
338
395
  connectTelegram,
396
+ requestEmailOtp,
397
+ connectEmail,
339
398
  logout
340
399
  };
341
400
  }
@@ -527,9 +586,13 @@ var defaultTheme = {
527
586
  fontFamily: 'system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif'
528
587
  };
529
588
  function SolvenLoginContent(props) {
530
- const { connectExternal, connectTelegram, status, user } = useSolvenAuth();
589
+ const { connectExternal, connectTelegram, requestEmailOtp, connectEmail, status, user } = useSolvenAuth();
531
590
  const [busy, setBusy] = react.useState(null);
532
591
  const [error, setError] = react.useState(null);
592
+ const [email, setEmail] = react.useState("");
593
+ const [otp, setOtp] = react.useState("");
594
+ const [otpSent, setOtpSent] = react.useState(false);
595
+ const showEmail = props.showEmail ?? true;
533
596
  const theme = react.useMemo(
534
597
  () => ({ ...defaultTheme, ...props.theme ?? {} }),
535
598
  [props.theme]
@@ -596,6 +659,31 @@ function SolvenLoginContent(props) {
596
659
  setBusy(null);
597
660
  }
598
661
  };
662
+ const handleSendOtp = async () => {
663
+ setBusy("email");
664
+ setError(null);
665
+ try {
666
+ await requestEmailOtp(email.trim());
667
+ setOtpSent(true);
668
+ } catch (e) {
669
+ setError(e instanceof Error ? e.message : "Couldn't send the code");
670
+ } finally {
671
+ setBusy(null);
672
+ }
673
+ };
674
+ const handleVerifyOtp = async () => {
675
+ setBusy("email");
676
+ setError(null);
677
+ try {
678
+ await connectEmail(email.trim(), otp.trim());
679
+ props.onSignIn?.();
680
+ props.onCloseAfterSignIn?.();
681
+ } catch (e) {
682
+ setError(e instanceof Error ? e.message : "Couldn't verify the code");
683
+ } finally {
684
+ setBusy(null);
685
+ }
686
+ };
599
687
  const buttonBase = {
600
688
  width: "100%",
601
689
  padding: "0.75rem 0.875rem",
@@ -681,6 +769,135 @@ function SolvenLoginContent(props) {
681
769
  w.id
682
770
  );
683
771
  }) }),
772
+ !inTma && showEmail && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { marginTop: "1rem" }, children: [
773
+ /* @__PURE__ */ jsxRuntime.jsxs(
774
+ "div",
775
+ {
776
+ style: {
777
+ display: "flex",
778
+ alignItems: "center",
779
+ gap: "0.625rem",
780
+ margin: "0.25rem 0 0.875rem",
781
+ color: theme.muted,
782
+ fontSize: "0.8125rem"
783
+ },
784
+ children: [
785
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { flex: 1, height: 1, background: theme.border } }),
786
+ "or",
787
+ /* @__PURE__ */ jsxRuntime.jsx("span", { style: { flex: 1, height: 1, background: theme.border } })
788
+ ]
789
+ }
790
+ ),
791
+ !otpSent ? /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "0.5rem" }, children: [
792
+ /* @__PURE__ */ jsxRuntime.jsx(
793
+ "input",
794
+ {
795
+ type: "email",
796
+ inputMode: "email",
797
+ autoComplete: "email",
798
+ placeholder: "you@email.com",
799
+ value: email,
800
+ onChange: (e) => setEmail(e.target.value),
801
+ style: {
802
+ width: "100%",
803
+ padding: "0.75rem 0.875rem",
804
+ borderRadius: theme.radius,
805
+ border: `1px solid ${theme.border}`,
806
+ background: theme.surface,
807
+ color: theme.foreground,
808
+ fontSize: "0.9375rem",
809
+ outline: "none",
810
+ boxSizing: "border-box"
811
+ }
812
+ }
813
+ ),
814
+ /* @__PURE__ */ jsxRuntime.jsx(
815
+ "button",
816
+ {
817
+ type: "button",
818
+ onClick: handleSendOtp,
819
+ disabled: busy !== null || !email.includes("@"),
820
+ style: {
821
+ ...buttonBase,
822
+ justifyContent: "center",
823
+ background: theme.primary,
824
+ color: "#ffffff",
825
+ border: "none",
826
+ opacity: busy === "email" || !email.includes("@") ? 0.6 : 1
827
+ },
828
+ children: busy === "email" ? "Sending\u2026" : "Email me a code"
829
+ }
830
+ )
831
+ ] }) : /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "0.5rem" }, children: [
832
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: { color: theme.muted, fontSize: "0.8125rem" }, children: [
833
+ "Enter the 6-digit code sent to ",
834
+ /* @__PURE__ */ jsxRuntime.jsx("strong", { children: email.trim() }),
835
+ "."
836
+ ] }),
837
+ /* @__PURE__ */ jsxRuntime.jsx(
838
+ "input",
839
+ {
840
+ type: "text",
841
+ inputMode: "numeric",
842
+ autoComplete: "one-time-code",
843
+ maxLength: 6,
844
+ placeholder: "123456",
845
+ value: otp,
846
+ onChange: (e) => setOtp(e.target.value.replace(/\D/g, "")),
847
+ style: {
848
+ width: "100%",
849
+ padding: "0.75rem 0.875rem",
850
+ borderRadius: theme.radius,
851
+ border: `1px solid ${theme.border}`,
852
+ background: theme.surface,
853
+ color: theme.foreground,
854
+ fontSize: "1.125rem",
855
+ letterSpacing: "0.3em",
856
+ textAlign: "center",
857
+ outline: "none",
858
+ boxSizing: "border-box"
859
+ }
860
+ }
861
+ ),
862
+ /* @__PURE__ */ jsxRuntime.jsx(
863
+ "button",
864
+ {
865
+ type: "button",
866
+ onClick: handleVerifyOtp,
867
+ disabled: busy !== null || otp.length !== 6,
868
+ style: {
869
+ ...buttonBase,
870
+ justifyContent: "center",
871
+ background: theme.primary,
872
+ color: "#ffffff",
873
+ border: "none",
874
+ opacity: busy === "email" || otp.length !== 6 ? 0.6 : 1
875
+ },
876
+ children: busy === "email" ? "Verifying\u2026" : "Verify & sign in"
877
+ }
878
+ ),
879
+ /* @__PURE__ */ jsxRuntime.jsx(
880
+ "button",
881
+ {
882
+ type: "button",
883
+ onClick: () => {
884
+ setOtpSent(false);
885
+ setOtp("");
886
+ setError(null);
887
+ },
888
+ style: {
889
+ background: "none",
890
+ border: "none",
891
+ color: theme.muted,
892
+ fontSize: "0.8125rem",
893
+ cursor: "pointer",
894
+ padding: "0.25rem"
895
+ },
896
+ children: "Use a different email"
897
+ }
898
+ )
899
+ ] })
900
+ ] }),
684
901
  error && /* @__PURE__ */ jsxRuntime.jsx(
685
902
  "div",
686
903
  {
package/dist/index.d.cts CHANGED
@@ -1,12 +1,23 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import React$1 from 'react';
3
3
 
4
- type AuthMethod = "siws" | "telegram";
4
+ type AuthMethod = "siws" | "telegram" | "email";
5
5
  interface IUserIdentity {
6
6
  id: string;
7
7
  type: AuthMethod;
8
- /** For SIWS: base58 Solana pubkey. For Telegram: numeric user id as string. */
8
+ /** For SIWS: base58 Solana pubkey. For Telegram: numeric user id as string. For email: the address. */
9
9
  externalId: string;
10
+ /** Provider-specific profile captured at login (e.g. Telegram { username, firstName }).
11
+ * Display/attribution only — null when none was captured. */
12
+ profile?: Record<string, unknown> | null;
13
+ }
14
+ interface IUserWallet {
15
+ /** base58 Solana pubkey */
16
+ address: string;
17
+ /** test (devnet) or live (mainnet) — wallets are isolated per environment. */
18
+ environment: "test" | "live";
19
+ custody: "self" | "operator";
20
+ status: "active" | "exported" | "archived";
10
21
  }
11
22
  interface IUser {
12
23
  id: string;
@@ -14,6 +25,8 @@ interface IUser {
14
25
  studioUserId?: string;
15
26
  primaryIdentity: IUserIdentity;
16
27
  identities: IUserIdentity[];
28
+ /** Managed wallets owned by this user. Lets a relying server confirm a claimed address. */
29
+ wallets: IUserWallet[];
17
30
  createdAt: string;
18
31
  }
19
32
  /** SIWS challenge issued by the signer; the client signs it with its external wallet. */
@@ -146,7 +159,7 @@ interface IWebhookSignatureHeader {
146
159
  signature: string;
147
160
  }
148
161
 
149
- type IAMGameErrorCode = "auth/invalid_signature" | "auth/challenge_expired" | "auth/challenge_already_redeemed" | "auth/invalid_init_data" | "auth/init_data_stale" | "auth/init_data_replayed" | "auth/missing_token" | "auth/invalid_token" | "auth/expired_token" | "wallet/not_found" | "wallet/insufficient_balance" | "wallet/export_blocked" | "wallet/already_exported" | "user/not_found" | "user/suspended" | "compliance/blocked" | "sign/invalid_transaction" | "sign/wallet_archived" | "sign/internal_failure" | "withdrawal/insufficient_balance" | "withdrawal/limit_exceeded" | "ratelimit/exceeded" | "idempotency/key_in_use" | "validation/bad_request" | "server/internal";
162
+ type IAMGameErrorCode = "auth/invalid_signature" | "auth/challenge_expired" | "auth/challenge_already_redeemed" | "auth/invalid_init_data" | "auth/init_data_stale" | "auth/init_data_replayed" | "auth/missing_token" | "auth/invalid_token" | "auth/expired_token" | "auth/method_not_allowed" | "auth/invalid_otp" | "auth/otp_not_found" | "auth/otp_locked" | "wallet/not_found" | "wallet/insufficient_balance" | "wallet/export_blocked" | "wallet/already_exported" | "user/not_found" | "user/suspended" | "compliance/blocked" | "sign/invalid_transaction" | "sign/wallet_archived" | "sign/internal_failure" | "withdrawal/insufficient_balance" | "withdrawal/limit_exceeded" | "ledger/insufficient_balance" | "ledger/session_not_found" | "ledger/session_closed" | "ledger/bet_not_found" | "ledger/bet_rolled_back" | "ledger/currency_mismatch" | "ratelimit/exceeded" | "idempotency/key_in_use" | "validation/bad_request" | "server/internal";
150
163
  interface IIAMGameErrorEnvelope {
151
164
  code: IAMGameErrorCode;
152
165
  message: string;
@@ -184,6 +197,13 @@ declare class IAMGameClient {
184
197
  requestSiwsChallenge(publicKey: string): Promise<ISiwsChallenge>;
185
198
  verifySiws(req: ISiwsVerifyRequest): Promise<ISession>;
186
199
  verifyTelegram(req: ITelegramVerifyRequest): Promise<ISession>;
200
+ /** Email + OTP: send a one-time code to the address. */
201
+ initiateEmail(email: string): Promise<{
202
+ sent: boolean;
203
+ expiresAt: string;
204
+ }>;
205
+ /** Email + OTP: exchange the code for a session. */
206
+ verifyEmail(email: string, code: string): Promise<ISession>;
187
207
  refreshSession(): Promise<ISession | null>;
188
208
  logout(): Promise<void>;
189
209
  getSession(): ISession | null;
@@ -241,6 +261,12 @@ interface IUseIAMGameAuth {
241
261
  connectExternal: (adapter: IExternalWalletAdapter) => Promise<void>;
242
262
  /** Run the Telegram TMA flow. Reads `window.Telegram.WebApp.initData` and posts to IAMGame. */
243
263
  connectTelegram: () => Promise<void>;
264
+ /** Email + OTP step 1: email a one-time code to the address. */
265
+ requestEmailOtp: (email: string) => Promise<{
266
+ expiresAt: string;
267
+ }>;
268
+ /** Email + OTP step 2: exchange the code for a session. */
269
+ connectEmail: (email: string, code: string) => Promise<void>;
244
270
  logout: () => Promise<void>;
245
271
  }
246
272
  declare function useIAMGameAuth(): IUseIAMGameAuth;
@@ -297,6 +323,12 @@ interface CommonProps {
297
323
  * no clicks. Defaults to true.
298
324
  */
299
325
  autoTelegram?: boolean;
326
+ /**
327
+ * Show the email + one-time-code login option (web / non-TMA). Defaults to true.
328
+ * The app must also enable `email` in its allowed auth methods, or the send-code
329
+ * call is rejected.
330
+ */
331
+ showEmail?: boolean;
300
332
  }
301
333
  type IAMGameLoginProps = CommonProps;
302
334
  interface IAMGameLoginModalProps extends CommonProps {
package/dist/index.d.ts CHANGED
@@ -1,12 +1,23 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
  import React$1 from 'react';
3
3
 
4
- type AuthMethod = "siws" | "telegram";
4
+ type AuthMethod = "siws" | "telegram" | "email";
5
5
  interface IUserIdentity {
6
6
  id: string;
7
7
  type: AuthMethod;
8
- /** For SIWS: base58 Solana pubkey. For Telegram: numeric user id as string. */
8
+ /** For SIWS: base58 Solana pubkey. For Telegram: numeric user id as string. For email: the address. */
9
9
  externalId: string;
10
+ /** Provider-specific profile captured at login (e.g. Telegram { username, firstName }).
11
+ * Display/attribution only — null when none was captured. */
12
+ profile?: Record<string, unknown> | null;
13
+ }
14
+ interface IUserWallet {
15
+ /** base58 Solana pubkey */
16
+ address: string;
17
+ /** test (devnet) or live (mainnet) — wallets are isolated per environment. */
18
+ environment: "test" | "live";
19
+ custody: "self" | "operator";
20
+ status: "active" | "exported" | "archived";
10
21
  }
11
22
  interface IUser {
12
23
  id: string;
@@ -14,6 +25,8 @@ interface IUser {
14
25
  studioUserId?: string;
15
26
  primaryIdentity: IUserIdentity;
16
27
  identities: IUserIdentity[];
28
+ /** Managed wallets owned by this user. Lets a relying server confirm a claimed address. */
29
+ wallets: IUserWallet[];
17
30
  createdAt: string;
18
31
  }
19
32
  /** SIWS challenge issued by the signer; the client signs it with its external wallet. */
@@ -146,7 +159,7 @@ interface IWebhookSignatureHeader {
146
159
  signature: string;
147
160
  }
148
161
 
149
- type IAMGameErrorCode = "auth/invalid_signature" | "auth/challenge_expired" | "auth/challenge_already_redeemed" | "auth/invalid_init_data" | "auth/init_data_stale" | "auth/init_data_replayed" | "auth/missing_token" | "auth/invalid_token" | "auth/expired_token" | "wallet/not_found" | "wallet/insufficient_balance" | "wallet/export_blocked" | "wallet/already_exported" | "user/not_found" | "user/suspended" | "compliance/blocked" | "sign/invalid_transaction" | "sign/wallet_archived" | "sign/internal_failure" | "withdrawal/insufficient_balance" | "withdrawal/limit_exceeded" | "ratelimit/exceeded" | "idempotency/key_in_use" | "validation/bad_request" | "server/internal";
162
+ type IAMGameErrorCode = "auth/invalid_signature" | "auth/challenge_expired" | "auth/challenge_already_redeemed" | "auth/invalid_init_data" | "auth/init_data_stale" | "auth/init_data_replayed" | "auth/missing_token" | "auth/invalid_token" | "auth/expired_token" | "auth/method_not_allowed" | "auth/invalid_otp" | "auth/otp_not_found" | "auth/otp_locked" | "wallet/not_found" | "wallet/insufficient_balance" | "wallet/export_blocked" | "wallet/already_exported" | "user/not_found" | "user/suspended" | "compliance/blocked" | "sign/invalid_transaction" | "sign/wallet_archived" | "sign/internal_failure" | "withdrawal/insufficient_balance" | "withdrawal/limit_exceeded" | "ledger/insufficient_balance" | "ledger/session_not_found" | "ledger/session_closed" | "ledger/bet_not_found" | "ledger/bet_rolled_back" | "ledger/currency_mismatch" | "ratelimit/exceeded" | "idempotency/key_in_use" | "validation/bad_request" | "server/internal";
150
163
  interface IIAMGameErrorEnvelope {
151
164
  code: IAMGameErrorCode;
152
165
  message: string;
@@ -184,6 +197,13 @@ declare class IAMGameClient {
184
197
  requestSiwsChallenge(publicKey: string): Promise<ISiwsChallenge>;
185
198
  verifySiws(req: ISiwsVerifyRequest): Promise<ISession>;
186
199
  verifyTelegram(req: ITelegramVerifyRequest): Promise<ISession>;
200
+ /** Email + OTP: send a one-time code to the address. */
201
+ initiateEmail(email: string): Promise<{
202
+ sent: boolean;
203
+ expiresAt: string;
204
+ }>;
205
+ /** Email + OTP: exchange the code for a session. */
206
+ verifyEmail(email: string, code: string): Promise<ISession>;
187
207
  refreshSession(): Promise<ISession | null>;
188
208
  logout(): Promise<void>;
189
209
  getSession(): ISession | null;
@@ -241,6 +261,12 @@ interface IUseIAMGameAuth {
241
261
  connectExternal: (adapter: IExternalWalletAdapter) => Promise<void>;
242
262
  /** Run the Telegram TMA flow. Reads `window.Telegram.WebApp.initData` and posts to IAMGame. */
243
263
  connectTelegram: () => Promise<void>;
264
+ /** Email + OTP step 1: email a one-time code to the address. */
265
+ requestEmailOtp: (email: string) => Promise<{
266
+ expiresAt: string;
267
+ }>;
268
+ /** Email + OTP step 2: exchange the code for a session. */
269
+ connectEmail: (email: string, code: string) => Promise<void>;
244
270
  logout: () => Promise<void>;
245
271
  }
246
272
  declare function useIAMGameAuth(): IUseIAMGameAuth;
@@ -297,6 +323,12 @@ interface CommonProps {
297
323
  * no clicks. Defaults to true.
298
324
  */
299
325
  autoTelegram?: boolean;
326
+ /**
327
+ * Show the email + one-time-code login option (web / non-TMA). Defaults to true.
328
+ * The app must also enable `email` in its allowed auth methods, or the send-code
329
+ * call is rejected.
330
+ */
331
+ showEmail?: boolean;
300
332
  }
301
333
  type IAMGameLoginProps = CommonProps;
302
334
  interface IAMGameLoginModalProps extends CommonProps {
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { createContext, useMemo, useState, useEffect, useCallback, useRef, useContext } from 'react';
1
+ import { createContext, useMemo, useState, useEffect, useRef, useCallback, useContext } from 'react';
2
2
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
3
3
  import { createPortal } from 'react-dom';
4
4
 
@@ -78,6 +78,23 @@ var SolvenClient = class {
78
78
  this.opts.storage.set(session);
79
79
  return session;
80
80
  }
81
+ /** Email + OTP: send a one-time code to the address. */
82
+ async initiateEmail(email) {
83
+ return this.publicCall(
84
+ "POST",
85
+ "/auth/email/initiate",
86
+ { email }
87
+ );
88
+ }
89
+ /** Email + OTP: exchange the code for a session. */
90
+ async verifyEmail(email, code) {
91
+ const session = await this.publicCall("POST", "/auth/email/verify", {
92
+ email,
93
+ code
94
+ });
95
+ this.opts.storage.set(session);
96
+ return session;
97
+ }
81
98
  async refreshSession() {
82
99
  const current = this.opts.storage.get();
83
100
  if (!current) return null;
@@ -252,7 +269,7 @@ function useSolvenContext() {
252
269
  if (!ctx) throw new Error("useSolvenContext must be inside a <SolvenProvider />");
253
270
  return ctx;
254
271
  }
255
- function SolvenProvider({ children, ...opts }) {
272
+ function SolvenProvider({ children, autoTelegram = true, ...opts }) {
256
273
  const client = useMemo(() => new SolvenClient(opts), [
257
274
  opts.publishableKey,
258
275
  opts.baseUrl,
@@ -293,6 +310,32 @@ function SolvenProvider({ children, ...opts }) {
293
310
  cancelled = true;
294
311
  };
295
312
  }, [client]);
313
+ const triedTelegram = useRef(false);
314
+ useEffect(() => {
315
+ if (!autoTelegram || status !== "anonymous" || triedTelegram.current) return;
316
+ triedTelegram.current = true;
317
+ let cancelled = false;
318
+ (async () => {
319
+ let initData = getTelegramInitData();
320
+ for (let i = 0; i < 4 && !initData; i++) {
321
+ await new Promise((r) => setTimeout(r, 400));
322
+ if (cancelled) return;
323
+ initData = getTelegramInitData();
324
+ }
325
+ if (!initData) return;
326
+ try {
327
+ notifyTelegramReady();
328
+ const next = await client.verifyTelegram({ initData });
329
+ if (cancelled) return;
330
+ setSession(next);
331
+ setStatus("authenticated");
332
+ } catch {
333
+ }
334
+ })();
335
+ return () => {
336
+ cancelled = true;
337
+ };
338
+ }, [autoTelegram, status, client]);
296
339
  const value = {
297
340
  client,
298
341
  session,
@@ -325,6 +368,20 @@ function useSolvenAuth() {
325
368
  const session2 = await client.verifyTelegram({ initData });
326
369
  setSession(session2);
327
370
  }, [client, setSession]);
371
+ const requestEmailOtp = useCallback(
372
+ async (email) => {
373
+ const { expiresAt } = await client.initiateEmail(email);
374
+ return { expiresAt };
375
+ },
376
+ [client]
377
+ );
378
+ const connectEmail = useCallback(
379
+ async (email, code) => {
380
+ const session2 = await client.verifyEmail(email, code);
381
+ setSession(session2);
382
+ },
383
+ [client, setSession]
384
+ );
328
385
  const logout = useCallback(async () => {
329
386
  await client.logout();
330
387
  setSession(null);
@@ -334,6 +391,8 @@ function useSolvenAuth() {
334
391
  status,
335
392
  connectExternal,
336
393
  connectTelegram,
394
+ requestEmailOtp,
395
+ connectEmail,
337
396
  logout
338
397
  };
339
398
  }
@@ -525,9 +584,13 @@ var defaultTheme = {
525
584
  fontFamily: 'system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif'
526
585
  };
527
586
  function SolvenLoginContent(props) {
528
- const { connectExternal, connectTelegram, status, user } = useSolvenAuth();
587
+ const { connectExternal, connectTelegram, requestEmailOtp, connectEmail, status, user } = useSolvenAuth();
529
588
  const [busy, setBusy] = useState(null);
530
589
  const [error, setError] = useState(null);
590
+ const [email, setEmail] = useState("");
591
+ const [otp, setOtp] = useState("");
592
+ const [otpSent, setOtpSent] = useState(false);
593
+ const showEmail = props.showEmail ?? true;
531
594
  const theme = useMemo(
532
595
  () => ({ ...defaultTheme, ...props.theme ?? {} }),
533
596
  [props.theme]
@@ -594,6 +657,31 @@ function SolvenLoginContent(props) {
594
657
  setBusy(null);
595
658
  }
596
659
  };
660
+ const handleSendOtp = async () => {
661
+ setBusy("email");
662
+ setError(null);
663
+ try {
664
+ await requestEmailOtp(email.trim());
665
+ setOtpSent(true);
666
+ } catch (e) {
667
+ setError(e instanceof Error ? e.message : "Couldn't send the code");
668
+ } finally {
669
+ setBusy(null);
670
+ }
671
+ };
672
+ const handleVerifyOtp = async () => {
673
+ setBusy("email");
674
+ setError(null);
675
+ try {
676
+ await connectEmail(email.trim(), otp.trim());
677
+ props.onSignIn?.();
678
+ props.onCloseAfterSignIn?.();
679
+ } catch (e) {
680
+ setError(e instanceof Error ? e.message : "Couldn't verify the code");
681
+ } finally {
682
+ setBusy(null);
683
+ }
684
+ };
597
685
  const buttonBase = {
598
686
  width: "100%",
599
687
  padding: "0.75rem 0.875rem",
@@ -679,6 +767,135 @@ function SolvenLoginContent(props) {
679
767
  w.id
680
768
  );
681
769
  }) }),
770
+ !inTma && showEmail && /* @__PURE__ */ jsxs("div", { style: { marginTop: "1rem" }, children: [
771
+ /* @__PURE__ */ jsxs(
772
+ "div",
773
+ {
774
+ style: {
775
+ display: "flex",
776
+ alignItems: "center",
777
+ gap: "0.625rem",
778
+ margin: "0.25rem 0 0.875rem",
779
+ color: theme.muted,
780
+ fontSize: "0.8125rem"
781
+ },
782
+ children: [
783
+ /* @__PURE__ */ jsx("span", { style: { flex: 1, height: 1, background: theme.border } }),
784
+ "or",
785
+ /* @__PURE__ */ jsx("span", { style: { flex: 1, height: 1, background: theme.border } })
786
+ ]
787
+ }
788
+ ),
789
+ !otpSent ? /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "0.5rem" }, children: [
790
+ /* @__PURE__ */ jsx(
791
+ "input",
792
+ {
793
+ type: "email",
794
+ inputMode: "email",
795
+ autoComplete: "email",
796
+ placeholder: "you@email.com",
797
+ value: email,
798
+ onChange: (e) => setEmail(e.target.value),
799
+ style: {
800
+ width: "100%",
801
+ padding: "0.75rem 0.875rem",
802
+ borderRadius: theme.radius,
803
+ border: `1px solid ${theme.border}`,
804
+ background: theme.surface,
805
+ color: theme.foreground,
806
+ fontSize: "0.9375rem",
807
+ outline: "none",
808
+ boxSizing: "border-box"
809
+ }
810
+ }
811
+ ),
812
+ /* @__PURE__ */ jsx(
813
+ "button",
814
+ {
815
+ type: "button",
816
+ onClick: handleSendOtp,
817
+ disabled: busy !== null || !email.includes("@"),
818
+ style: {
819
+ ...buttonBase,
820
+ justifyContent: "center",
821
+ background: theme.primary,
822
+ color: "#ffffff",
823
+ border: "none",
824
+ opacity: busy === "email" || !email.includes("@") ? 0.6 : 1
825
+ },
826
+ children: busy === "email" ? "Sending\u2026" : "Email me a code"
827
+ }
828
+ )
829
+ ] }) : /* @__PURE__ */ jsxs("div", { style: { display: "flex", flexDirection: "column", gap: "0.5rem" }, children: [
830
+ /* @__PURE__ */ jsxs("div", { style: { color: theme.muted, fontSize: "0.8125rem" }, children: [
831
+ "Enter the 6-digit code sent to ",
832
+ /* @__PURE__ */ jsx("strong", { children: email.trim() }),
833
+ "."
834
+ ] }),
835
+ /* @__PURE__ */ jsx(
836
+ "input",
837
+ {
838
+ type: "text",
839
+ inputMode: "numeric",
840
+ autoComplete: "one-time-code",
841
+ maxLength: 6,
842
+ placeholder: "123456",
843
+ value: otp,
844
+ onChange: (e) => setOtp(e.target.value.replace(/\D/g, "")),
845
+ style: {
846
+ width: "100%",
847
+ padding: "0.75rem 0.875rem",
848
+ borderRadius: theme.radius,
849
+ border: `1px solid ${theme.border}`,
850
+ background: theme.surface,
851
+ color: theme.foreground,
852
+ fontSize: "1.125rem",
853
+ letterSpacing: "0.3em",
854
+ textAlign: "center",
855
+ outline: "none",
856
+ boxSizing: "border-box"
857
+ }
858
+ }
859
+ ),
860
+ /* @__PURE__ */ jsx(
861
+ "button",
862
+ {
863
+ type: "button",
864
+ onClick: handleVerifyOtp,
865
+ disabled: busy !== null || otp.length !== 6,
866
+ style: {
867
+ ...buttonBase,
868
+ justifyContent: "center",
869
+ background: theme.primary,
870
+ color: "#ffffff",
871
+ border: "none",
872
+ opacity: busy === "email" || otp.length !== 6 ? 0.6 : 1
873
+ },
874
+ children: busy === "email" ? "Verifying\u2026" : "Verify & sign in"
875
+ }
876
+ ),
877
+ /* @__PURE__ */ jsx(
878
+ "button",
879
+ {
880
+ type: "button",
881
+ onClick: () => {
882
+ setOtpSent(false);
883
+ setOtp("");
884
+ setError(null);
885
+ },
886
+ style: {
887
+ background: "none",
888
+ border: "none",
889
+ color: theme.muted,
890
+ fontSize: "0.8125rem",
891
+ cursor: "pointer",
892
+ padding: "0.25rem"
893
+ },
894
+ children: "Use a different email"
895
+ }
896
+ )
897
+ ] })
898
+ ] }),
682
899
  error && /* @__PURE__ */ jsx(
683
900
  "div",
684
901
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iamgame/wallet-sdk",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "IAMGame Wallet browser SDK — Telegram & Solana wallet auth, balances, server-side signing, and key export. Drop-in React provider for game frontends.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://wallet.iamgame.com",