@blinkdotnew/sdk 0.19.4 → 0.19.5

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.mjs CHANGED
@@ -1018,6 +1018,142 @@ var HttpClient = class {
1018
1018
  }
1019
1019
  };
1020
1020
 
1021
+ // src/utils/browser-env.ts
1022
+ function hasWindow() {
1023
+ return typeof window !== "undefined";
1024
+ }
1025
+ function hasWindowLocation() {
1026
+ return typeof window !== "undefined" && typeof window.location !== "undefined";
1027
+ }
1028
+ function hasDocument() {
1029
+ return typeof document !== "undefined";
1030
+ }
1031
+ function isReactNative2() {
1032
+ return typeof navigator !== "undefined" && navigator.product === "ReactNative";
1033
+ }
1034
+ function getWindowLocation() {
1035
+ if (!hasWindow()) return null;
1036
+ try {
1037
+ return window.location;
1038
+ } catch {
1039
+ return null;
1040
+ }
1041
+ }
1042
+ function getLocationHref() {
1043
+ const loc = getWindowLocation();
1044
+ if (!loc) return null;
1045
+ try {
1046
+ return loc.href;
1047
+ } catch {
1048
+ return null;
1049
+ }
1050
+ }
1051
+ function getLocationOrigin() {
1052
+ const loc = getWindowLocation();
1053
+ if (!loc) return null;
1054
+ try {
1055
+ return loc.origin;
1056
+ } catch {
1057
+ return null;
1058
+ }
1059
+ }
1060
+ function getLocationHostname() {
1061
+ const loc = getWindowLocation();
1062
+ if (!loc) return null;
1063
+ try {
1064
+ return loc.hostname;
1065
+ } catch {
1066
+ return null;
1067
+ }
1068
+ }
1069
+ function getLocationPathname() {
1070
+ const loc = getWindowLocation();
1071
+ if (!loc) return null;
1072
+ try {
1073
+ return loc.pathname;
1074
+ } catch {
1075
+ return null;
1076
+ }
1077
+ }
1078
+ function getLocationSearch() {
1079
+ const loc = getWindowLocation();
1080
+ if (!loc) return null;
1081
+ try {
1082
+ return loc.search;
1083
+ } catch {
1084
+ return null;
1085
+ }
1086
+ }
1087
+ function getLocationHash() {
1088
+ const loc = getWindowLocation();
1089
+ if (!loc) return null;
1090
+ try {
1091
+ return loc.hash;
1092
+ } catch {
1093
+ return null;
1094
+ }
1095
+ }
1096
+ function getLocationProtocol() {
1097
+ const loc = getWindowLocation();
1098
+ if (!loc) return null;
1099
+ try {
1100
+ return loc.protocol;
1101
+ } catch {
1102
+ return null;
1103
+ }
1104
+ }
1105
+ function getLocationHost() {
1106
+ const loc = getWindowLocation();
1107
+ if (!loc) return null;
1108
+ try {
1109
+ return loc.host;
1110
+ } catch {
1111
+ return null;
1112
+ }
1113
+ }
1114
+ function constructFullUrl() {
1115
+ if (!hasWindow()) return null;
1116
+ const protocol = getLocationProtocol();
1117
+ const host = getLocationHost();
1118
+ const pathname = getLocationPathname();
1119
+ const search = getLocationSearch();
1120
+ const hash = getLocationHash();
1121
+ if (!protocol || !host) return null;
1122
+ return `${protocol}//${host}${pathname || ""}${search || ""}${hash || ""}`;
1123
+ }
1124
+ function getDocumentReferrer() {
1125
+ if (!hasDocument()) return null;
1126
+ try {
1127
+ return document.referrer || null;
1128
+ } catch {
1129
+ return null;
1130
+ }
1131
+ }
1132
+ function getWindowInnerWidth() {
1133
+ if (!hasWindow()) return null;
1134
+ try {
1135
+ return window.innerWidth;
1136
+ } catch {
1137
+ return null;
1138
+ }
1139
+ }
1140
+ function isIframe() {
1141
+ if (!hasWindow()) return false;
1142
+ try {
1143
+ return window.self !== window.top;
1144
+ } catch {
1145
+ return true;
1146
+ }
1147
+ }
1148
+ function getSessionStorage() {
1149
+ if (!hasWindow()) return null;
1150
+ try {
1151
+ return window.sessionStorage;
1152
+ } catch {
1153
+ return null;
1154
+ }
1155
+ }
1156
+
1021
1157
  // src/auth.ts
1022
1158
  var BlinkAuth = class {
1023
1159
  config;
@@ -1039,13 +1175,16 @@ var BlinkAuth = class {
1039
1175
  this.authConfig = {
1040
1176
  mode: "managed",
1041
1177
  // Default mode
1042
- authUrl: "https://blink.new",
1178
+ authUrl: "http://localhost:3000",
1043
1179
  coreUrl: "https://core.blink.new",
1180
+ detectSessionInUrl: true,
1181
+ // Default to true for web compatibility
1044
1182
  ...config.auth
1045
1183
  };
1046
1184
  this.authUrl = this.authConfig.authUrl || "https://blink.new";
1047
1185
  this.coreUrl = this.authConfig.coreUrl || "https://core.blink.new";
1048
- if (typeof window !== "undefined" && this.authUrl === "https://blink.new" && (window.location.hostname === "localhost" || window.location.hostname === "127.0.0.1")) {
1186
+ const hostname = getLocationHostname();
1187
+ if (hostname && this.authUrl === "https://blink.new" && (hostname === "localhost" || hostname === "127.0.0.1")) {
1049
1188
  console.warn("\u26A0\uFE0F Using default authUrl in development. Set auth.authUrl to your app origin for headless auth endpoints to work.");
1050
1189
  }
1051
1190
  if (config.authRequired !== void 0 && !config.auth?.mode) {
@@ -1059,7 +1198,7 @@ var BlinkAuth = class {
1059
1198
  };
1060
1199
  this.storage = config.auth?.storage || config.storage || getDefaultStorageAdapter();
1061
1200
  if (isWeb) {
1062
- this.isIframe = window.self !== window.top;
1201
+ this.isIframe = isIframe();
1063
1202
  this.setupParentWindowListener();
1064
1203
  this.setupCrossTabSync();
1065
1204
  this.initializationPromise = this.initialize();
@@ -1095,7 +1234,7 @@ var BlinkAuth = class {
1095
1234
  * Setup listener for tokens from parent window
1096
1235
  */
1097
1236
  setupParentWindowListener() {
1098
- if (!isWeb || !this.isIframe) return;
1237
+ if (!isWeb || !this.isIframe || !hasWindow()) return;
1099
1238
  window.addEventListener("message", (event) => {
1100
1239
  if (event.origin !== "https://blink.new" && event.origin !== "http://localhost:3000" && event.origin !== "http://localhost:3001") {
1101
1240
  return;
@@ -1117,7 +1256,7 @@ var BlinkAuth = class {
1117
1256
  this.clearTokens();
1118
1257
  }
1119
1258
  });
1120
- if (window.parent !== window) {
1259
+ if (hasWindow() && window.parent !== window) {
1121
1260
  console.log("\u{1F504} Requesting auth tokens from parent window");
1122
1261
  window.parent.postMessage({
1123
1262
  type: "BLINK_REQUEST_AUTH_TOKENS",
@@ -1142,67 +1281,14 @@ var BlinkAuth = class {
1142
1281
  return;
1143
1282
  }
1144
1283
  }
1145
- try {
1146
- if (typeof window !== "undefined") {
1147
- console.log("\u{1F50D} Checking URL for errors/tokens:", {
1148
- href: window.location.href,
1149
- search: window.location.search,
1150
- hash: window.location.hash
1151
- });
1152
- const urlParams = this.extractUrlParams();
1153
- const errorParam = urlParams.get("error");
1154
- if (errorParam) {
1155
- const errorCode = this.mapErrorCodeFromResponse(errorParam);
1156
- const errorMessage = urlParams.get("error_description") || "Authentication failed";
1157
- const error = new BlinkAuthError(errorCode, errorMessage);
1158
- console.error("\u274C Auth error in URL:", {
1159
- error: errorParam,
1160
- errorMessage,
1161
- allParams: Object.fromEntries(urlParams.entries())
1162
- });
1163
- if (typeof window !== "undefined") {
1164
- window.dispatchEvent(new CustomEvent("blink:auth:error", {
1165
- detail: { error, errorMessage }
1166
- }));
1167
- }
1168
- this.clearUrlTokens();
1169
- return;
1170
- }
1171
- }
1172
- } catch (error) {
1173
- console.error("Error handling failed redirect:", error);
1174
- }
1175
- const tokensFromUrl = this.extractTokensFromUrl();
1176
- if (tokensFromUrl) {
1177
- console.log("\u{1F4E5} Found tokens in URL, setting them...");
1178
- await this.setTokens(tokensFromUrl, true);
1179
- this.clearUrlTokens();
1180
- console.log("\u2705 Auth initialization complete (from URL)");
1181
- return;
1182
- } else {
1183
- console.log("\u26A0\uFE0F No tokens found in URL after redirect - checking if this was a redirect callback");
1184
- if (typeof window !== "undefined") {
1185
- const urlParams = this.extractUrlParams();
1186
- const state = urlParams.get("state");
1187
- if (state) {
1188
- console.log("\u26A0\uFE0F State found in URL but no tokens - redirect may have failed silently");
1189
- try {
1190
- const expectedState = sessionStorage.getItem("blink_oauth_state");
1191
- if (expectedState === state) {
1192
- console.error("\u274C Redirect callback received but no tokens - authentication may have failed");
1193
- const errorMessage = "Authentication failed. Please try again.";
1194
- if (typeof window !== "undefined") {
1195
- window.dispatchEvent(new CustomEvent("blink:auth:error", {
1196
- detail: { error: new BlinkAuthError("NETWORK_ERROR" /* NETWORK_ERROR */, errorMessage), errorMessage }
1197
- }));
1198
- }
1199
- this.clearUrlTokens();
1200
- return;
1201
- }
1202
- } catch (e) {
1203
- console.error("Error checking sessionStorage:", e);
1204
- }
1205
- }
1284
+ if (this.authConfig.detectSessionInUrl !== false) {
1285
+ const tokensFromUrl = this.extractTokensFromUrl();
1286
+ if (tokensFromUrl) {
1287
+ console.log("\u{1F4E5} Found tokens in URL, setting them...");
1288
+ await this.setTokens(tokensFromUrl, true);
1289
+ this.clearUrlTokens();
1290
+ console.log("\u2705 Auth initialization complete (from URL)");
1291
+ return;
1206
1292
  }
1207
1293
  }
1208
1294
  const storedTokens = await this.getStoredTokens();
@@ -1227,11 +1313,11 @@ var BlinkAuth = class {
1227
1313
  }
1228
1314
  }
1229
1315
  console.log("\u274C No tokens found");
1230
- if (this.config.authRequired) {
1316
+ if (this.config.authRequired && hasWindowLocation()) {
1231
1317
  console.log("\u{1F504} Auth required, redirecting to auth page...");
1232
1318
  this.redirectToAuth();
1233
1319
  } else {
1234
- console.log("\u26A0\uFE0F Auth not required, continuing without authentication");
1320
+ console.log("\u26A0\uFE0F Auth not required or no window.location, continuing without authentication");
1235
1321
  }
1236
1322
  } finally {
1237
1323
  this.setLoading(false);
@@ -1242,15 +1328,20 @@ var BlinkAuth = class {
1242
1328
  * Redirect to Blink auth page
1243
1329
  */
1244
1330
  login(nextUrl) {
1331
+ if (!hasWindowLocation()) {
1332
+ console.warn("login() called in non-browser environment (no window.location available)");
1333
+ return;
1334
+ }
1245
1335
  let redirectUrl = nextUrl || this.authConfig.redirectUrl;
1246
- if (!redirectUrl && typeof window !== "undefined") {
1247
- if (window.location.href.startsWith("http")) {
1248
- redirectUrl = window.location.href;
1336
+ if (!redirectUrl) {
1337
+ const href = getLocationHref();
1338
+ if (href && href.startsWith("http")) {
1339
+ redirectUrl = href;
1249
1340
  } else {
1250
- redirectUrl = `${window.location.protocol}//${window.location.host}${window.location.pathname}${window.location.search}${window.location.hash}`;
1341
+ redirectUrl = constructFullUrl() || void 0;
1251
1342
  }
1252
1343
  }
1253
- if (redirectUrl && typeof window !== "undefined") {
1344
+ if (redirectUrl) {
1254
1345
  try {
1255
1346
  const url = new URL(redirectUrl);
1256
1347
  url.searchParams.delete("redirect_url");
@@ -1265,16 +1356,14 @@ var BlinkAuth = class {
1265
1356
  if (this.config.projectId) {
1266
1357
  authUrl.searchParams.set("project_id", this.config.projectId);
1267
1358
  }
1268
- if (typeof window !== "undefined") {
1269
- window.location.href = authUrl.toString();
1270
- }
1359
+ window.location.href = authUrl.toString();
1271
1360
  }
1272
1361
  /**
1273
1362
  * Logout and clear stored tokens
1274
1363
  */
1275
1364
  logout(redirectUrl) {
1276
1365
  this.clearTokens();
1277
- if (redirectUrl && typeof window !== "undefined") {
1366
+ if (redirectUrl && hasWindowLocation()) {
1278
1367
  window.location.href = redirectUrl;
1279
1368
  }
1280
1369
  }
@@ -1523,6 +1612,16 @@ var BlinkAuth = class {
1523
1612
  }
1524
1613
  /**
1525
1614
  * Sign in with Google (headless mode)
1615
+ *
1616
+ * **Universal OAuth** - Works on both Web and React Native!
1617
+ *
1618
+ * On React Native, requires `webBrowser` to be configured in client:
1619
+ * ```typescript
1620
+ * const blink = createClient({
1621
+ * auth: { mode: 'headless', webBrowser: WebBrowser }
1622
+ * })
1623
+ * await blink.auth.signInWithGoogle() // Works on both platforms!
1624
+ * ```
1526
1625
  */
1527
1626
  async signInWithGoogle(options) {
1528
1627
  if (this.authConfig.mode !== "headless") {
@@ -1532,6 +1631,9 @@ var BlinkAuth = class {
1532
1631
  }
1533
1632
  /**
1534
1633
  * Sign in with GitHub (headless mode)
1634
+ *
1635
+ * **Universal OAuth** - Works on both Web and React Native!
1636
+ * See signInWithGoogle() for setup instructions.
1535
1637
  */
1536
1638
  async signInWithGitHub(options) {
1537
1639
  if (this.authConfig.mode !== "headless") {
@@ -1541,6 +1643,9 @@ var BlinkAuth = class {
1541
1643
  }
1542
1644
  /**
1543
1645
  * Sign in with Apple (headless mode)
1646
+ *
1647
+ * **Universal OAuth** - Works on both Web and React Native!
1648
+ * See signInWithGoogle() for setup instructions.
1544
1649
  */
1545
1650
  async signInWithApple(options) {
1546
1651
  if (this.authConfig.mode !== "headless") {
@@ -1550,6 +1655,9 @@ var BlinkAuth = class {
1550
1655
  }
1551
1656
  /**
1552
1657
  * Sign in with Microsoft (headless mode)
1658
+ *
1659
+ * **Universal OAuth** - Works on both Web and React Native!
1660
+ * See signInWithGoogle() for setup instructions.
1553
1661
  */
1554
1662
  async signInWithMicrosoft(options) {
1555
1663
  if (this.authConfig.mode !== "headless") {
@@ -1558,144 +1666,264 @@ var BlinkAuth = class {
1558
1666
  return this.signInWithProvider("microsoft", options);
1559
1667
  }
1560
1668
  /**
1561
- * Check if current browser is Safari (desktop, iPhone, iPad)
1562
- * Safari has strict popup blocking, so we must use redirect flow
1669
+ * Initiate OAuth for mobile without deep linking (expo-web-browser pattern)
1670
+ *
1671
+ * This method:
1672
+ * 1. Generates a unique session ID
1673
+ * 2. Returns OAuth URL with session parameter
1674
+ * 3. App opens URL in expo-web-browser
1675
+ * 4. App polls checkMobileOAuthSession() until complete
1676
+ *
1677
+ * @param provider - OAuth provider (google, github, apple, etc.)
1678
+ * @param options - Optional metadata
1679
+ * @returns Session ID and OAuth URL
1680
+ *
1681
+ * @example
1682
+ * // React Native with expo-web-browser
1683
+ * import * as WebBrowser from 'expo-web-browser';
1684
+ *
1685
+ * const { sessionId, authUrl } = await blink.auth.initiateMobileOAuth('google');
1686
+ *
1687
+ * // Open browser
1688
+ * await WebBrowser.openAuthSessionAsync(authUrl);
1689
+ *
1690
+ * // Poll for completion
1691
+ * const user = await blink.auth.pollMobileOAuthSession(sessionId);
1692
+ * console.log('Authenticated:', user.email);
1693
+ */
1694
+ async initiateMobileOAuth(provider, options) {
1695
+ if (this.authConfig.mode !== "headless") {
1696
+ throw new BlinkAuthError(
1697
+ "INVALID_CREDENTIALS" /* INVALID_CREDENTIALS */,
1698
+ "initiateMobileOAuth is only available in headless mode"
1699
+ );
1700
+ }
1701
+ const sessionId = this.generateSessionId();
1702
+ const authUrl = new URL("/auth", this.authUrl);
1703
+ authUrl.searchParams.set("provider", provider);
1704
+ authUrl.searchParams.set("project_id", this.config.projectId);
1705
+ authUrl.searchParams.set("mode", "mobile-session");
1706
+ authUrl.searchParams.set("session_id", sessionId);
1707
+ if (options?.metadata) {
1708
+ authUrl.searchParams.set("metadata", JSON.stringify(options.metadata));
1709
+ }
1710
+ return {
1711
+ sessionId,
1712
+ authUrl: authUrl.toString()
1713
+ };
1714
+ }
1715
+ /**
1716
+ * Check mobile OAuth session status (single check)
1717
+ *
1718
+ * @param sessionId - Session ID from initiateMobileOAuth
1719
+ * @returns Tokens if session is complete, null if still pending
1563
1720
  */
1564
- isSafari() {
1565
- if (typeof window === "undefined") return false;
1566
- const ua = navigator.userAgent.toLowerCase();
1567
- const hasSafariUA = /safari/i.test(ua) && !/chrome|chromium|android|edg|firefox|opera/i.test(ua);
1568
- const isSafariVendor = typeof navigator !== "undefined" && !!navigator.vendor && /apple/i.test(navigator.vendor);
1569
- const isIOSSafari = /safari/i.test(ua) && /iphone|ipad|ipod/i.test(ua);
1570
- const hasSafariProperties = typeof window !== "undefined" && ("safari" in window || window.safari !== void 0) && !("chrome" in window);
1571
- const isMacSafari = /macintosh/i.test(ua) && isSafariVendor && !/chrome|chromium|firefox|edg/i.test(ua);
1572
- const isSafari = !!(hasSafariUA || isSafariVendor || isIOSSafari || hasSafariProperties || isMacSafari);
1573
- if (isSafari) {
1574
- console.log("\u{1F34E} Safari browser detected - will use redirect flow", {
1575
- hasSafariUA,
1576
- isSafariVendor,
1577
- isIOSSafari,
1578
- hasSafariProperties,
1579
- isMacSafari,
1580
- userAgent: ua.substring(0, 100),
1581
- vendor: navigator.vendor
1721
+ async checkMobileOAuthSession(sessionId) {
1722
+ try {
1723
+ const response = await fetch(`${this.authUrl}/api/auth/mobile-session/${sessionId}`, {
1724
+ method: "GET",
1725
+ headers: {
1726
+ "Content-Type": "application/json"
1727
+ }
1582
1728
  });
1583
- } else {
1584
- console.log("\u{1F50D} Not detected as Safari:", {
1585
- hasSafariUA,
1586
- isSafariVendor,
1587
- isIOSSafari,
1588
- hasSafariProperties,
1589
- isMacSafari,
1590
- userAgent: ua.substring(0, 100),
1591
- vendor: navigator.vendor
1729
+ if (response.status === 404 || response.status === 202) {
1730
+ return null;
1731
+ }
1732
+ if (!response.ok) {
1733
+ const errorData = await response.json();
1734
+ const errorCode = this.mapErrorCodeFromResponse(errorData.code);
1735
+ throw new BlinkAuthError(
1736
+ errorCode,
1737
+ errorData.error || "Failed to check OAuth session"
1738
+ );
1739
+ }
1740
+ const data = await response.json();
1741
+ return {
1742
+ access_token: data.access_token,
1743
+ refresh_token: data.refresh_token,
1744
+ token_type: data.token_type || "Bearer",
1745
+ expires_in: data.expires_in || 3600,
1746
+ refresh_expires_in: data.refresh_expires_in
1747
+ };
1748
+ } catch (error) {
1749
+ if (error instanceof BlinkAuthError) {
1750
+ throw error;
1751
+ }
1752
+ throw new BlinkAuthError(
1753
+ "NETWORK_ERROR" /* NETWORK_ERROR */,
1754
+ `Network error: ${error instanceof Error ? error.message : "Unknown error"}`
1755
+ );
1756
+ }
1757
+ }
1758
+ /**
1759
+ * Poll mobile OAuth session until complete (convenience method)
1760
+ *
1761
+ * @param sessionId - Session ID from initiateMobileOAuth
1762
+ * @param options - Polling options
1763
+ * @returns Authenticated user
1764
+ *
1765
+ * @example
1766
+ * const { sessionId, authUrl } = await blink.auth.initiateMobileOAuth('google');
1767
+ * await WebBrowser.openAuthSessionAsync(authUrl);
1768
+ * const user = await blink.auth.pollMobileOAuthSession(sessionId, {
1769
+ * maxAttempts: 60,
1770
+ * intervalMs: 1000
1771
+ * });
1772
+ */
1773
+ async pollMobileOAuthSession(sessionId, options) {
1774
+ const maxAttempts = options?.maxAttempts || 60;
1775
+ const intervalMs = options?.intervalMs || 1e3;
1776
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
1777
+ const tokens = await this.checkMobileOAuthSession(sessionId);
1778
+ if (tokens) {
1779
+ await this.setTokens(tokens, true);
1780
+ return this.authState.user;
1781
+ }
1782
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
1783
+ }
1784
+ throw new BlinkAuthError(
1785
+ "AUTH_TIMEOUT" /* AUTH_TIMEOUT */,
1786
+ "Mobile OAuth session timed out"
1787
+ );
1788
+ }
1789
+ /**
1790
+ * Sign in with OAuth provider using expo-web-browser (React Native)
1791
+ *
1792
+ * This is a convenience method that handles the entire flow:
1793
+ * 1. Initiates mobile OAuth session
1794
+ * 2. Returns auth URL to open in WebBrowser
1795
+ * 3. Provides polling function to call after browser opens
1796
+ *
1797
+ * @param provider - OAuth provider
1798
+ * @returns Object with authUrl and authenticate function
1799
+ *
1800
+ * @example
1801
+ * import * as WebBrowser from 'expo-web-browser';
1802
+ *
1803
+ * const { authUrl, authenticate } = await blink.auth.signInWithProviderMobile('google');
1804
+ *
1805
+ * // Open browser
1806
+ * await WebBrowser.openAuthSessionAsync(authUrl);
1807
+ *
1808
+ * // Wait for authentication
1809
+ * const user = await authenticate();
1810
+ */
1811
+ async signInWithProviderMobile(provider, options) {
1812
+ const { sessionId, authUrl } = await this.initiateMobileOAuth(provider, options);
1813
+ return {
1814
+ authUrl,
1815
+ authenticate: () => this.pollMobileOAuthSession(sessionId, {
1816
+ maxAttempts: 60,
1817
+ intervalMs: 1e3
1818
+ })
1819
+ };
1820
+ }
1821
+ /**
1822
+ * Universal OAuth flow using session-based authentication (internal)
1823
+ * Works on ALL platforms: Web, iOS, Android
1824
+ * Uses expo-web-browser to open auth URL and polls for completion
1825
+ */
1826
+ async signInWithProviderUniversal(provider, options) {
1827
+ const webBrowser = this.authConfig.webBrowser;
1828
+ if (!webBrowser) {
1829
+ throw new BlinkAuthError(
1830
+ "NETWORK_ERROR" /* NETWORK_ERROR */,
1831
+ "webBrowser module is required for universal OAuth flow"
1832
+ );
1833
+ }
1834
+ const { sessionId, authUrl } = await this.initiateMobileOAuth(provider, options);
1835
+ console.log("\u{1F510} Opening OAuth browser for", provider);
1836
+ const result = await webBrowser.openAuthSessionAsync(authUrl);
1837
+ console.log("\u{1F510} Browser closed with result:", result.type);
1838
+ try {
1839
+ const user = await this.pollMobileOAuthSession(sessionId, {
1840
+ maxAttempts: 60,
1841
+ // 30 seconds (500ms intervals)
1842
+ intervalMs: 500
1592
1843
  });
1844
+ console.log("\u2705 OAuth completed successfully");
1845
+ return user;
1846
+ } catch (pollError) {
1847
+ if (result.type === "cancel" || result.type === "dismiss") {
1848
+ throw new BlinkAuthError(
1849
+ "POPUP_CANCELED" /* POPUP_CANCELED */,
1850
+ "Authentication was canceled"
1851
+ );
1852
+ }
1853
+ throw pollError;
1593
1854
  }
1594
- return isSafari;
1595
1855
  }
1596
1856
  /**
1597
1857
  * Generic provider sign-in method (headless mode)
1598
- * Uses redirect flow for Safari, popup flow for other browsers
1858
+ *
1859
+ * **Universal OAuth** - Works seamlessly on both Web and React Native!
1860
+ *
1861
+ * When `webBrowser` is configured in the client, this method automatically
1862
+ * uses the session-based OAuth flow that works on ALL platforms.
1863
+ *
1864
+ * **Universal Setup (configure once, works everywhere):**
1865
+ * ```typescript
1866
+ * import * as WebBrowser from 'expo-web-browser'
1867
+ * import AsyncStorage from '@react-native-async-storage/async-storage'
1868
+ *
1869
+ * const blink = createClient({
1870
+ * projectId: 'your-project',
1871
+ * auth: {
1872
+ * mode: 'headless',
1873
+ * webBrowser: WebBrowser // Pass the module here
1874
+ * },
1875
+ * storage: new AsyncStorageAdapter(AsyncStorage)
1876
+ * })
1877
+ *
1878
+ * // Now this works on ALL platforms - no platform checks needed!
1879
+ * const user = await blink.auth.signInWithGoogle()
1880
+ * ```
1881
+ *
1882
+ * @param provider - OAuth provider (google, github, apple, etc.)
1883
+ * @param options - Optional redirect URL and metadata
1884
+ * @returns Promise that resolves with authenticated user
1599
1885
  */
1600
1886
  async signInWithProvider(provider, options) {
1601
1887
  if (this.authConfig.mode !== "headless") {
1602
1888
  throw new BlinkAuthError("INVALID_CREDENTIALS" /* INVALID_CREDENTIALS */, "signInWithProvider is only available in headless mode");
1603
1889
  }
1604
- const state = this.generateState();
1605
- let redirectUrl = options?.redirectUrl || "";
1606
- if (!redirectUrl && typeof window !== "undefined") {
1607
- if (window.location.href.startsWith("http")) {
1608
- redirectUrl = window.location.href;
1609
- } else {
1610
- redirectUrl = `${window.location.protocol}//${window.location.host}${window.location.pathname}${window.location.search}${window.location.hash}`;
1611
- }
1890
+ if (this.authConfig.webBrowser) {
1891
+ return this.signInWithProviderUniversal(provider, options);
1612
1892
  }
1613
- try {
1614
- const redirectUrlObj = new URL(redirectUrl);
1615
- redirectUrlObj.searchParams.delete("access_token");
1616
- redirectUrlObj.searchParams.delete("refresh_token");
1617
- redirectUrlObj.searchParams.delete("state");
1618
- redirectUrlObj.searchParams.delete("error");
1619
- redirectUrlObj.searchParams.delete("error_description");
1620
- redirectUrlObj.hash = "";
1621
- redirectUrl = redirectUrlObj.toString();
1622
- } catch (e) {
1623
- console.warn("Failed to clean redirect URL:", e);
1893
+ if (isReactNative2()) {
1894
+ throw new BlinkAuthError(
1895
+ "NETWORK_ERROR" /* NETWORK_ERROR */,
1896
+ 'React Native OAuth requires webBrowser in config!\n\nimport * as WebBrowser from "expo-web-browser";\n\nconst blink = createClient({\n projectId: "your-project",\n auth: {\n mode: "headless",\n webBrowser: WebBrowser\n }\n})\n\nawait blink.auth.signInWithGoogle() // Works on all platforms!'
1897
+ );
1624
1898
  }
1625
- try {
1626
- if (typeof window !== "undefined") {
1627
- sessionStorage.setItem("blink_oauth_state", state);
1628
- sessionStorage.setItem("blink_oauth_redirect_url", redirectUrl);
1629
- console.log("\u{1F4BE} Stored OAuth state:", { state, redirectUrl });
1630
- }
1631
- } catch (e) {
1632
- console.warn("Failed to store OAuth state in sessionStorage:", e);
1633
- }
1634
- const isSafari = this.isSafari();
1635
- const forceRedirect = options?.forceRedirect === true;
1636
- if ((isSafari || forceRedirect) && typeof window !== "undefined") {
1637
- console.log("\u{1F310} Using redirect flow (no popup)", {
1638
- provider,
1639
- projectId: this.config.projectId,
1640
- state,
1641
- redirectUrl,
1642
- authUrl: this.authUrl,
1643
- reason: isSafari ? "Safari detected" : "forceRedirect option set"
1644
- });
1645
- const authRedirectUrl = new URL("/auth", this.authUrl);
1646
- authRedirectUrl.searchParams.set("provider", provider);
1647
- authRedirectUrl.searchParams.set("project_id", this.config.projectId);
1648
- authRedirectUrl.searchParams.set("state", state);
1649
- authRedirectUrl.searchParams.set("mode", "redirect");
1650
- authRedirectUrl.searchParams.set("redirect_url", redirectUrl);
1651
- console.log("\u{1F504} Redirecting to:", authRedirectUrl.toString());
1652
- window.location.href = authRedirectUrl.toString();
1653
- return new Promise(() => {
1654
- });
1899
+ if (!hasWindow()) {
1900
+ throw new BlinkAuthError("NETWORK_ERROR" /* NETWORK_ERROR */, "signInWithProvider requires a browser environment");
1655
1901
  }
1656
1902
  return new Promise((resolve, reject) => {
1903
+ const state = this.generateState();
1904
+ try {
1905
+ const sessionStorage = getSessionStorage();
1906
+ if (sessionStorage) {
1907
+ sessionStorage.setItem("blink_oauth_state", state);
1908
+ }
1909
+ } catch {
1910
+ }
1911
+ const redirectUrl = options?.redirectUrl || getLocationOrigin() || "";
1657
1912
  const popupUrl = new URL("/auth", this.authUrl);
1658
1913
  popupUrl.searchParams.set("provider", provider);
1659
1914
  popupUrl.searchParams.set("project_id", this.config.projectId);
1660
1915
  popupUrl.searchParams.set("state", state);
1661
1916
  popupUrl.searchParams.set("mode", "popup");
1662
1917
  popupUrl.searchParams.set("redirect_url", redirectUrl);
1663
- if (typeof window !== "undefined") {
1664
- popupUrl.searchParams.set("opener_origin", window.location.origin);
1665
- }
1666
1918
  const popup = window.open(
1667
1919
  popupUrl.toString(),
1668
1920
  "blink-auth",
1669
1921
  "width=500,height=600,scrollbars=yes,resizable=yes"
1670
1922
  );
1671
1923
  if (!popup) {
1672
- console.warn("\u26A0\uFE0F Popup was blocked, falling back to redirect flow");
1673
- const authRedirectUrl = new URL("/auth", this.authUrl);
1674
- authRedirectUrl.searchParams.set("provider", provider);
1675
- authRedirectUrl.searchParams.set("project_id", this.config.projectId);
1676
- authRedirectUrl.searchParams.set("state", state);
1677
- authRedirectUrl.searchParams.set("mode", "redirect");
1678
- authRedirectUrl.searchParams.set("redirect_url", redirectUrl);
1679
- console.log("\u{1F504} Falling back to redirect:", authRedirectUrl.toString());
1680
- window.location.href = authRedirectUrl.toString();
1924
+ reject(new BlinkAuthError("POPUP_CANCELED" /* POPUP_CANCELED */, "Popup was blocked"));
1681
1925
  return;
1682
1926
  }
1683
- try {
1684
- if (popup.closed || !popup.window) {
1685
- console.warn("\u26A0\uFE0F Popup appears to be blocked (closed immediately), falling back to redirect flow");
1686
- const authRedirectUrl = new URL("/auth", this.authUrl);
1687
- authRedirectUrl.searchParams.set("provider", provider);
1688
- authRedirectUrl.searchParams.set("project_id", this.config.projectId);
1689
- authRedirectUrl.searchParams.set("state", state);
1690
- authRedirectUrl.searchParams.set("mode", "redirect");
1691
- authRedirectUrl.searchParams.set("redirect_url", redirectUrl);
1692
- console.log("\u{1F504} Falling back to redirect:", authRedirectUrl.toString());
1693
- window.location.href = authRedirectUrl.toString();
1694
- return;
1695
- }
1696
- } catch (e) {
1697
- console.log("\u26A0\uFE0F Could not verify popup state, assuming it opened successfully");
1698
- }
1699
1927
  let timeoutId;
1700
1928
  const messageListener = (event) => {
1701
1929
  let allowed = false;
@@ -1705,16 +1933,12 @@ var BlinkAuth = class {
1705
1933
  } catch {
1706
1934
  }
1707
1935
  if (event.origin === "http://localhost:3000" || event.origin === "http://localhost:3001") allowed = true;
1708
- if (typeof window !== "undefined" && event.origin === window.location.origin) allowed = true;
1709
- if (!allowed) {
1710
- console.log("\u{1F6AB} Blocked postMessage from untrusted origin:", event.origin);
1711
- return;
1712
- }
1713
- console.log("\u2705 Accepted postMessage from origin:", event.origin);
1936
+ if (!allowed) return;
1714
1937
  if (event.data?.type === "BLINK_AUTH_TOKENS") {
1715
1938
  const { access_token, refresh_token, token_type, expires_in, refresh_expires_in, projectId, state: returnedState } = event.data;
1716
1939
  try {
1717
- const expected = sessionStorage.getItem("blink_oauth_state");
1940
+ const sessionStorage = getSessionStorage();
1941
+ const expected = sessionStorage?.getItem("blink_oauth_state");
1718
1942
  if (returnedState && expected && returnedState !== expected) {
1719
1943
  reject(new BlinkAuthError("VERIFICATION_FAILED" /* VERIFICATION_FAILED */, "State mismatch"));
1720
1944
  clearTimeout(timeoutId);
@@ -2238,6 +2462,48 @@ var BlinkAuth = class {
2238
2462
  };
2239
2463
  await this.setTokens(tokens, persist);
2240
2464
  }
2465
+ /**
2466
+ * Manually set auth session from tokens (React Native deep link OAuth)
2467
+ *
2468
+ * Use this method to set the user session after receiving tokens from a deep link callback.
2469
+ * This is the React Native equivalent of automatic URL token detection on web.
2470
+ *
2471
+ * @param tokens - Auth tokens received from deep link or OAuth callback
2472
+ * @param persist - Whether to persist tokens to storage (default: true)
2473
+ *
2474
+ * @example
2475
+ * // React Native: Handle deep link OAuth callback
2476
+ * import * as Linking from 'expo-linking'
2477
+ *
2478
+ * Linking.addEventListener('url', async ({ url }) => {
2479
+ * const { queryParams } = Linking.parse(url)
2480
+ *
2481
+ * if (queryParams.access_token) {
2482
+ * await blink.auth.setSession({
2483
+ * access_token: queryParams.access_token,
2484
+ * refresh_token: queryParams.refresh_token,
2485
+ * expires_in: parseInt(queryParams.expires_in) || 3600,
2486
+ * refresh_expires_in: parseInt(queryParams.refresh_expires_in)
2487
+ * })
2488
+ *
2489
+ * console.log('User authenticated:', blink.auth.currentUser())
2490
+ * }
2491
+ * })
2492
+ */
2493
+ async setSession(tokens, persist = true) {
2494
+ const authTokens = {
2495
+ access_token: tokens.access_token,
2496
+ refresh_token: tokens.refresh_token,
2497
+ token_type: "Bearer",
2498
+ expires_in: tokens.expires_in || 3600,
2499
+ // Default 1 hour
2500
+ refresh_expires_in: tokens.refresh_expires_in,
2501
+ issued_at: Math.floor(Date.now() / 1e3)
2502
+ };
2503
+ await this.setTokens(authTokens, persist);
2504
+ const user = await this.me();
2505
+ return user;
2506
+ }
2241
2507
  /**
2242
2508
  * Refresh access token using refresh token
2243
2509
  */
@@ -2470,50 +2736,18 @@ var BlinkAuth = class {
2470
2736
  return null;
2471
2737
  }
2472
2738
  }
2473
- /**
2474
- * Extract URL parameters from both search params and hash fragments
2475
- * Safari OAuth redirects often use hash fragments instead of query params
2476
- */
2477
- extractUrlParams() {
2478
- const params = new URLSearchParams();
2479
- if (typeof window !== "undefined" && window.location.search) {
2480
- const searchParams = new URLSearchParams(window.location.search);
2481
- for (const [key, value] of searchParams.entries()) {
2482
- params.set(key, value);
2483
- }
2484
- }
2485
- if (typeof window !== "undefined" && window.location.hash) {
2486
- const hash = window.location.hash.substring(1);
2487
- const hashParams = new URLSearchParams(hash);
2488
- for (const [key, value] of hashParams.entries()) {
2489
- if (!params.has(key)) {
2490
- params.set(key, value);
2491
- }
2492
- }
2493
- }
2494
- return params;
2495
- }
2496
2739
  extractTokensFromUrl() {
2497
- if (typeof window === "undefined") return null;
2498
- const params = this.extractUrlParams();
2740
+ const search = getLocationSearch();
2741
+ if (!search) return null;
2742
+ const params = new URLSearchParams(search);
2499
2743
  const accessToken = params.get("access_token");
2500
2744
  const refreshToken = params.get("refresh_token");
2501
- const state = params.get("state");
2502
- const error = params.get("error");
2503
2745
  console.log("\u{1F50D} Extracting tokens from URL:", {
2504
- url: window.location.href,
2505
- search: window.location.search,
2506
- hash: window.location.hash,
2746
+ url: getLocationHref(),
2507
2747
  accessToken: accessToken ? `${accessToken.substring(0, 20)}...` : null,
2508
2748
  refreshToken: refreshToken ? `${refreshToken.substring(0, 20)}...` : null,
2509
- state: state ? `${state.substring(0, 10)}...` : null,
2510
- error: error || null,
2511
2749
  allParams: Object.fromEntries(params.entries())
2512
2750
  });
2513
- if (error) {
2514
- console.log("\u274C Error parameter found in URL, not extracting tokens");
2515
- return null;
2516
- }
2517
2751
  if (accessToken) {
2518
2752
  const tokens = {
2519
2753
  access_token: accessToken,
@@ -2528,8 +2762,7 @@ var BlinkAuth = class {
2528
2762
  };
2529
2763
  console.log("\u2705 Tokens extracted successfully:", {
2530
2764
  hasAccessToken: !!tokens.access_token,
2531
- hasRefreshToken: !!tokens.refresh_token,
2532
- state: state || "no state"
2765
+ hasRefreshToken: !!tokens.refresh_token
2533
2766
  });
2534
2767
  return tokens;
2535
2768
  }
@@ -2537,8 +2770,9 @@ var BlinkAuth = class {
2537
2770
  return null;
2538
2771
  }
2539
2772
  clearUrlTokens() {
2540
- if (typeof window === "undefined") return;
2541
- const url = new URL(window.location.href);
2773
+ const href = getLocationHref();
2774
+ if (!href || !hasWindowLocation()) return;
2775
+ const url = new URL(href);
2542
2776
  url.searchParams.delete("access_token");
2543
2777
  url.searchParams.delete("refresh_token");
2544
2778
  url.searchParams.delete("token_type");
@@ -2549,12 +2783,11 @@ var BlinkAuth = class {
2549
2783
  url.searchParams.delete("code");
2550
2784
  url.searchParams.delete("error");
2551
2785
  url.searchParams.delete("error_description");
2552
- url.hash = "";
2553
2786
  window.history.replaceState({}, "", url.toString());
2554
- console.log("\u{1F9F9} URL cleaned up, removed auth parameters from both search and hash");
2787
+ console.log("\u{1F9F9} URL cleaned up, removed auth parameters");
2555
2788
  }
2556
2789
  redirectToAuth() {
2557
- if (typeof window !== "undefined") {
2790
+ if (hasWindowLocation()) {
2558
2791
  this.login();
2559
2792
  }
2560
2793
  }
@@ -2586,12 +2819,25 @@ var BlinkAuth = class {
2586
2819
  return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
2587
2820
  }
2588
2821
  }
2822
+ /**
2823
+ * Generate unique session ID for mobile OAuth
2824
+ */
2825
+ generateSessionId() {
2826
+ if (typeof crypto !== "undefined" && crypto.getRandomValues) {
2827
+ const array = new Uint8Array(32);
2828
+ crypto.getRandomValues(array);
2829
+ return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join("");
2830
+ } else {
2831
+ return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
2832
+ }
2833
+ }
2589
2834
  /**
2590
2835
  * Extract magic link token from URL
2591
2836
  */
2592
2837
  extractMagicTokenFromUrl() {
2593
- if (typeof window === "undefined") return null;
2594
- const params = new URLSearchParams(window.location.search);
2838
+ const search = getLocationSearch();
2839
+ if (!search) return null;
2840
+ const params = new URLSearchParams(search);
2595
2841
  return params.get("magic_token") || params.get("token");
2596
2842
  }
2597
2843
  /**
@@ -2647,7 +2893,7 @@ var BlinkAuth = class {
2647
2893
  * Setup cross-tab authentication synchronization
2648
2894
  */
2649
2895
  setupCrossTabSync() {
2650
- if (!isWeb) return;
2896
+ if (!isWeb || !hasWindow()) return;
2651
2897
  window.addEventListener("storage", (e) => {
2652
2898
  if (e.key === this.getStorageKey("tokens")) {
2653
2899
  const newTokens = e.newValue ? JSON.parse(e.newValue) : null;
@@ -4951,9 +5197,9 @@ var BlinkAnalyticsImpl = class {
4951
5197
  user_id: this.userId,
4952
5198
  user_email: this.userEmail,
4953
5199
  session_id: sessionId,
4954
- pathname: window.location.pathname,
4955
- referrer: document.referrer || null,
4956
- screen_width: window.innerWidth,
5200
+ pathname: getLocationPathname(),
5201
+ referrer: getDocumentReferrer(),
5202
+ screen_width: getWindowInnerWidth(),
4957
5203
  channel,
4958
5204
  utm_source: this.utmParams.utm_source || this.persistedAttribution.utm_source || null,
4959
5205
  utm_medium: this.utmParams.utm_medium || this.persistedAttribution.utm_medium || null,
@@ -5106,7 +5352,7 @@ var BlinkAnalyticsImpl = class {
5106
5352
  window.__blinkAnalyticsInstances?.add(this);
5107
5353
  }
5108
5354
  setupUnloadListener() {
5109
- if (!isWeb) return;
5355
+ if (!isWeb || !hasWindow()) return;
5110
5356
  window.addEventListener("pagehide", () => {
5111
5357
  this.flush();
5112
5358
  });
@@ -5116,7 +5362,12 @@ var BlinkAnalyticsImpl = class {
5116
5362
  }
5117
5363
  captureUTMParams() {
5118
5364
  if (!isWeb) return;
5119
- const urlParams = new URLSearchParams(window.location.search);
5365
+ const search = getLocationSearch();
5366
+ if (!search) {
5367
+ this.utmParams = {};
5368
+ return;
5369
+ }
5370
+ const urlParams = new URLSearchParams(search);
5120
5371
  this.utmParams = {
5121
5372
  utm_source: urlParams.get("utm_source"),
5122
5373
  utm_medium: urlParams.get("utm_medium"),
@@ -5153,7 +5404,7 @@ var BlinkAnalyticsImpl = class {
5153
5404
  }
5154
5405
  }
5155
5406
  detectChannel() {
5156
- const referrer = document.referrer;
5407
+ const referrer = getDocumentReferrer();
5157
5408
  const utmMedium = this.utmParams.utm_medium;
5158
5409
  this.utmParams.utm_source;
5159
5410
  if (utmMedium) {