@blinkdotnew/sdk 0.19.1 → 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/README.md +135 -16
- package/dist/index.d.mts +233 -1
- package/dist/index.d.ts +233 -1
- package/dist/index.js +495 -41
- package/dist/index.mjs +495 -41
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1020,6 +1020,142 @@ var HttpClient = class {
|
|
|
1020
1020
|
}
|
|
1021
1021
|
};
|
|
1022
1022
|
|
|
1023
|
+
// src/utils/browser-env.ts
|
|
1024
|
+
function hasWindow() {
|
|
1025
|
+
return typeof window !== "undefined";
|
|
1026
|
+
}
|
|
1027
|
+
function hasWindowLocation() {
|
|
1028
|
+
return typeof window !== "undefined" && typeof window.location !== "undefined";
|
|
1029
|
+
}
|
|
1030
|
+
function hasDocument() {
|
|
1031
|
+
return typeof document !== "undefined";
|
|
1032
|
+
}
|
|
1033
|
+
function isReactNative2() {
|
|
1034
|
+
return typeof navigator !== "undefined" && navigator.product === "ReactNative";
|
|
1035
|
+
}
|
|
1036
|
+
function getWindowLocation() {
|
|
1037
|
+
if (!hasWindow()) return null;
|
|
1038
|
+
try {
|
|
1039
|
+
return window.location;
|
|
1040
|
+
} catch {
|
|
1041
|
+
return null;
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
function getLocationHref() {
|
|
1045
|
+
const loc = getWindowLocation();
|
|
1046
|
+
if (!loc) return null;
|
|
1047
|
+
try {
|
|
1048
|
+
return loc.href;
|
|
1049
|
+
} catch {
|
|
1050
|
+
return null;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
function getLocationOrigin() {
|
|
1054
|
+
const loc = getWindowLocation();
|
|
1055
|
+
if (!loc) return null;
|
|
1056
|
+
try {
|
|
1057
|
+
return loc.origin;
|
|
1058
|
+
} catch {
|
|
1059
|
+
return null;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
function getLocationHostname() {
|
|
1063
|
+
const loc = getWindowLocation();
|
|
1064
|
+
if (!loc) return null;
|
|
1065
|
+
try {
|
|
1066
|
+
return loc.hostname;
|
|
1067
|
+
} catch {
|
|
1068
|
+
return null;
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
function getLocationPathname() {
|
|
1072
|
+
const loc = getWindowLocation();
|
|
1073
|
+
if (!loc) return null;
|
|
1074
|
+
try {
|
|
1075
|
+
return loc.pathname;
|
|
1076
|
+
} catch {
|
|
1077
|
+
return null;
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
function getLocationSearch() {
|
|
1081
|
+
const loc = getWindowLocation();
|
|
1082
|
+
if (!loc) return null;
|
|
1083
|
+
try {
|
|
1084
|
+
return loc.search;
|
|
1085
|
+
} catch {
|
|
1086
|
+
return null;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
function getLocationHash() {
|
|
1090
|
+
const loc = getWindowLocation();
|
|
1091
|
+
if (!loc) return null;
|
|
1092
|
+
try {
|
|
1093
|
+
return loc.hash;
|
|
1094
|
+
} catch {
|
|
1095
|
+
return null;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
function getLocationProtocol() {
|
|
1099
|
+
const loc = getWindowLocation();
|
|
1100
|
+
if (!loc) return null;
|
|
1101
|
+
try {
|
|
1102
|
+
return loc.protocol;
|
|
1103
|
+
} catch {
|
|
1104
|
+
return null;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
function getLocationHost() {
|
|
1108
|
+
const loc = getWindowLocation();
|
|
1109
|
+
if (!loc) return null;
|
|
1110
|
+
try {
|
|
1111
|
+
return loc.host;
|
|
1112
|
+
} catch {
|
|
1113
|
+
return null;
|
|
1114
|
+
}
|
|
1115
|
+
}
|
|
1116
|
+
function constructFullUrl() {
|
|
1117
|
+
if (!hasWindow()) return null;
|
|
1118
|
+
const protocol = getLocationProtocol();
|
|
1119
|
+
const host = getLocationHost();
|
|
1120
|
+
const pathname = getLocationPathname();
|
|
1121
|
+
const search = getLocationSearch();
|
|
1122
|
+
const hash = getLocationHash();
|
|
1123
|
+
if (!protocol || !host) return null;
|
|
1124
|
+
return `${protocol}//${host}${pathname || ""}${search || ""}${hash || ""}`;
|
|
1125
|
+
}
|
|
1126
|
+
function getDocumentReferrer() {
|
|
1127
|
+
if (!hasDocument()) return null;
|
|
1128
|
+
try {
|
|
1129
|
+
return document.referrer || null;
|
|
1130
|
+
} catch {
|
|
1131
|
+
return null;
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
function getWindowInnerWidth() {
|
|
1135
|
+
if (!hasWindow()) return null;
|
|
1136
|
+
try {
|
|
1137
|
+
return window.innerWidth;
|
|
1138
|
+
} catch {
|
|
1139
|
+
return null;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
function isIframe() {
|
|
1143
|
+
if (!hasWindow()) return false;
|
|
1144
|
+
try {
|
|
1145
|
+
return window.self !== window.top;
|
|
1146
|
+
} catch {
|
|
1147
|
+
return true;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
function getSessionStorage() {
|
|
1151
|
+
if (!hasWindow()) return null;
|
|
1152
|
+
try {
|
|
1153
|
+
return window.sessionStorage;
|
|
1154
|
+
} catch {
|
|
1155
|
+
return null;
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1023
1159
|
// src/auth.ts
|
|
1024
1160
|
var BlinkAuth = class {
|
|
1025
1161
|
config;
|
|
@@ -1041,13 +1177,16 @@ var BlinkAuth = class {
|
|
|
1041
1177
|
this.authConfig = {
|
|
1042
1178
|
mode: "managed",
|
|
1043
1179
|
// Default mode
|
|
1044
|
-
authUrl: "
|
|
1180
|
+
authUrl: "http://localhost:3000",
|
|
1045
1181
|
coreUrl: "https://core.blink.new",
|
|
1182
|
+
detectSessionInUrl: true,
|
|
1183
|
+
// Default to true for web compatibility
|
|
1046
1184
|
...config.auth
|
|
1047
1185
|
};
|
|
1048
1186
|
this.authUrl = this.authConfig.authUrl || "https://blink.new";
|
|
1049
1187
|
this.coreUrl = this.authConfig.coreUrl || "https://core.blink.new";
|
|
1050
|
-
|
|
1188
|
+
const hostname = getLocationHostname();
|
|
1189
|
+
if (hostname && this.authUrl === "https://blink.new" && (hostname === "localhost" || hostname === "127.0.0.1")) {
|
|
1051
1190
|
console.warn("\u26A0\uFE0F Using default authUrl in development. Set auth.authUrl to your app origin for headless auth endpoints to work.");
|
|
1052
1191
|
}
|
|
1053
1192
|
if (config.authRequired !== void 0 && !config.auth?.mode) {
|
|
@@ -1061,7 +1200,7 @@ var BlinkAuth = class {
|
|
|
1061
1200
|
};
|
|
1062
1201
|
this.storage = config.auth?.storage || config.storage || getDefaultStorageAdapter();
|
|
1063
1202
|
if (isWeb) {
|
|
1064
|
-
this.isIframe =
|
|
1203
|
+
this.isIframe = isIframe();
|
|
1065
1204
|
this.setupParentWindowListener();
|
|
1066
1205
|
this.setupCrossTabSync();
|
|
1067
1206
|
this.initializationPromise = this.initialize();
|
|
@@ -1097,7 +1236,7 @@ var BlinkAuth = class {
|
|
|
1097
1236
|
* Setup listener for tokens from parent window
|
|
1098
1237
|
*/
|
|
1099
1238
|
setupParentWindowListener() {
|
|
1100
|
-
if (!isWeb || !this.isIframe) return;
|
|
1239
|
+
if (!isWeb || !this.isIframe || !hasWindow()) return;
|
|
1101
1240
|
window.addEventListener("message", (event) => {
|
|
1102
1241
|
if (event.origin !== "https://blink.new" && event.origin !== "http://localhost:3000" && event.origin !== "http://localhost:3001") {
|
|
1103
1242
|
return;
|
|
@@ -1119,7 +1258,7 @@ var BlinkAuth = class {
|
|
|
1119
1258
|
this.clearTokens();
|
|
1120
1259
|
}
|
|
1121
1260
|
});
|
|
1122
|
-
if (window.parent !== window) {
|
|
1261
|
+
if (hasWindow() && window.parent !== window) {
|
|
1123
1262
|
console.log("\u{1F504} Requesting auth tokens from parent window");
|
|
1124
1263
|
window.parent.postMessage({
|
|
1125
1264
|
type: "BLINK_REQUEST_AUTH_TOKENS",
|
|
@@ -1144,13 +1283,15 @@ var BlinkAuth = class {
|
|
|
1144
1283
|
return;
|
|
1145
1284
|
}
|
|
1146
1285
|
}
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1286
|
+
if (this.authConfig.detectSessionInUrl !== false) {
|
|
1287
|
+
const tokensFromUrl = this.extractTokensFromUrl();
|
|
1288
|
+
if (tokensFromUrl) {
|
|
1289
|
+
console.log("\u{1F4E5} Found tokens in URL, setting them...");
|
|
1290
|
+
await this.setTokens(tokensFromUrl, true);
|
|
1291
|
+
this.clearUrlTokens();
|
|
1292
|
+
console.log("\u2705 Auth initialization complete (from URL)");
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1154
1295
|
}
|
|
1155
1296
|
const storedTokens = await this.getStoredTokens();
|
|
1156
1297
|
if (storedTokens) {
|
|
@@ -1174,11 +1315,11 @@ var BlinkAuth = class {
|
|
|
1174
1315
|
}
|
|
1175
1316
|
}
|
|
1176
1317
|
console.log("\u274C No tokens found");
|
|
1177
|
-
if (this.config.authRequired) {
|
|
1318
|
+
if (this.config.authRequired && hasWindowLocation()) {
|
|
1178
1319
|
console.log("\u{1F504} Auth required, redirecting to auth page...");
|
|
1179
1320
|
this.redirectToAuth();
|
|
1180
1321
|
} else {
|
|
1181
|
-
console.log("\u26A0\uFE0F Auth not required, continuing without authentication");
|
|
1322
|
+
console.log("\u26A0\uFE0F Auth not required or no window.location, continuing without authentication");
|
|
1182
1323
|
}
|
|
1183
1324
|
} finally {
|
|
1184
1325
|
this.setLoading(false);
|
|
@@ -1189,15 +1330,20 @@ var BlinkAuth = class {
|
|
|
1189
1330
|
* Redirect to Blink auth page
|
|
1190
1331
|
*/
|
|
1191
1332
|
login(nextUrl) {
|
|
1333
|
+
if (!hasWindowLocation()) {
|
|
1334
|
+
console.warn("login() called in non-browser environment (no window.location available)");
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1192
1337
|
let redirectUrl = nextUrl || this.authConfig.redirectUrl;
|
|
1193
|
-
if (!redirectUrl
|
|
1194
|
-
|
|
1195
|
-
|
|
1338
|
+
if (!redirectUrl) {
|
|
1339
|
+
const href = getLocationHref();
|
|
1340
|
+
if (href && href.startsWith("http")) {
|
|
1341
|
+
redirectUrl = href;
|
|
1196
1342
|
} else {
|
|
1197
|
-
redirectUrl =
|
|
1343
|
+
redirectUrl = constructFullUrl() || void 0;
|
|
1198
1344
|
}
|
|
1199
1345
|
}
|
|
1200
|
-
if (redirectUrl
|
|
1346
|
+
if (redirectUrl) {
|
|
1201
1347
|
try {
|
|
1202
1348
|
const url = new URL(redirectUrl);
|
|
1203
1349
|
url.searchParams.delete("redirect_url");
|
|
@@ -1212,16 +1358,14 @@ var BlinkAuth = class {
|
|
|
1212
1358
|
if (this.config.projectId) {
|
|
1213
1359
|
authUrl.searchParams.set("project_id", this.config.projectId);
|
|
1214
1360
|
}
|
|
1215
|
-
|
|
1216
|
-
window.location.href = authUrl.toString();
|
|
1217
|
-
}
|
|
1361
|
+
window.location.href = authUrl.toString();
|
|
1218
1362
|
}
|
|
1219
1363
|
/**
|
|
1220
1364
|
* Logout and clear stored tokens
|
|
1221
1365
|
*/
|
|
1222
1366
|
logout(redirectUrl) {
|
|
1223
1367
|
this.clearTokens();
|
|
1224
|
-
if (redirectUrl &&
|
|
1368
|
+
if (redirectUrl && hasWindowLocation()) {
|
|
1225
1369
|
window.location.href = redirectUrl;
|
|
1226
1370
|
}
|
|
1227
1371
|
}
|
|
@@ -1470,6 +1614,16 @@ var BlinkAuth = class {
|
|
|
1470
1614
|
}
|
|
1471
1615
|
/**
|
|
1472
1616
|
* Sign in with Google (headless mode)
|
|
1617
|
+
*
|
|
1618
|
+
* **Universal OAuth** - Works on both Web and React Native!
|
|
1619
|
+
*
|
|
1620
|
+
* On React Native, requires `webBrowser` to be configured in client:
|
|
1621
|
+
* ```typescript
|
|
1622
|
+
* const blink = createClient({
|
|
1623
|
+
* auth: { mode: 'headless', webBrowser: WebBrowser }
|
|
1624
|
+
* })
|
|
1625
|
+
* await blink.auth.signInWithGoogle() // Works on both platforms!
|
|
1626
|
+
* ```
|
|
1473
1627
|
*/
|
|
1474
1628
|
async signInWithGoogle(options) {
|
|
1475
1629
|
if (this.authConfig.mode !== "headless") {
|
|
@@ -1479,6 +1633,9 @@ var BlinkAuth = class {
|
|
|
1479
1633
|
}
|
|
1480
1634
|
/**
|
|
1481
1635
|
* Sign in with GitHub (headless mode)
|
|
1636
|
+
*
|
|
1637
|
+
* **Universal OAuth** - Works on both Web and React Native!
|
|
1638
|
+
* See signInWithGoogle() for setup instructions.
|
|
1482
1639
|
*/
|
|
1483
1640
|
async signInWithGitHub(options) {
|
|
1484
1641
|
if (this.authConfig.mode !== "headless") {
|
|
@@ -1488,6 +1645,9 @@ var BlinkAuth = class {
|
|
|
1488
1645
|
}
|
|
1489
1646
|
/**
|
|
1490
1647
|
* Sign in with Apple (headless mode)
|
|
1648
|
+
*
|
|
1649
|
+
* **Universal OAuth** - Works on both Web and React Native!
|
|
1650
|
+
* See signInWithGoogle() for setup instructions.
|
|
1491
1651
|
*/
|
|
1492
1652
|
async signInWithApple(options) {
|
|
1493
1653
|
if (this.authConfig.mode !== "headless") {
|
|
@@ -1497,6 +1657,9 @@ var BlinkAuth = class {
|
|
|
1497
1657
|
}
|
|
1498
1658
|
/**
|
|
1499
1659
|
* Sign in with Microsoft (headless mode)
|
|
1660
|
+
*
|
|
1661
|
+
* **Universal OAuth** - Works on both Web and React Native!
|
|
1662
|
+
* See signInWithGoogle() for setup instructions.
|
|
1500
1663
|
*/
|
|
1501
1664
|
async signInWithMicrosoft(options) {
|
|
1502
1665
|
if (this.authConfig.mode !== "headless") {
|
|
@@ -1504,22 +1667,250 @@ var BlinkAuth = class {
|
|
|
1504
1667
|
}
|
|
1505
1668
|
return this.signInWithProvider("microsoft", options);
|
|
1506
1669
|
}
|
|
1670
|
+
/**
|
|
1671
|
+
* Initiate OAuth for mobile without deep linking (expo-web-browser pattern)
|
|
1672
|
+
*
|
|
1673
|
+
* This method:
|
|
1674
|
+
* 1. Generates a unique session ID
|
|
1675
|
+
* 2. Returns OAuth URL with session parameter
|
|
1676
|
+
* 3. App opens URL in expo-web-browser
|
|
1677
|
+
* 4. App polls checkMobileOAuthSession() until complete
|
|
1678
|
+
*
|
|
1679
|
+
* @param provider - OAuth provider (google, github, apple, etc.)
|
|
1680
|
+
* @param options - Optional metadata
|
|
1681
|
+
* @returns Session ID and OAuth URL
|
|
1682
|
+
*
|
|
1683
|
+
* @example
|
|
1684
|
+
* // React Native with expo-web-browser
|
|
1685
|
+
* import * as WebBrowser from 'expo-web-browser';
|
|
1686
|
+
*
|
|
1687
|
+
* const { sessionId, authUrl } = await blink.auth.initiateMobileOAuth('google');
|
|
1688
|
+
*
|
|
1689
|
+
* // Open browser
|
|
1690
|
+
* await WebBrowser.openAuthSessionAsync(authUrl);
|
|
1691
|
+
*
|
|
1692
|
+
* // Poll for completion
|
|
1693
|
+
* const user = await blink.auth.pollMobileOAuthSession(sessionId);
|
|
1694
|
+
* console.log('Authenticated:', user.email);
|
|
1695
|
+
*/
|
|
1696
|
+
async initiateMobileOAuth(provider, options) {
|
|
1697
|
+
if (this.authConfig.mode !== "headless") {
|
|
1698
|
+
throw new BlinkAuthError(
|
|
1699
|
+
"INVALID_CREDENTIALS" /* INVALID_CREDENTIALS */,
|
|
1700
|
+
"initiateMobileOAuth is only available in headless mode"
|
|
1701
|
+
);
|
|
1702
|
+
}
|
|
1703
|
+
const sessionId = this.generateSessionId();
|
|
1704
|
+
const authUrl = new URL("/auth", this.authUrl);
|
|
1705
|
+
authUrl.searchParams.set("provider", provider);
|
|
1706
|
+
authUrl.searchParams.set("project_id", this.config.projectId);
|
|
1707
|
+
authUrl.searchParams.set("mode", "mobile-session");
|
|
1708
|
+
authUrl.searchParams.set("session_id", sessionId);
|
|
1709
|
+
if (options?.metadata) {
|
|
1710
|
+
authUrl.searchParams.set("metadata", JSON.stringify(options.metadata));
|
|
1711
|
+
}
|
|
1712
|
+
return {
|
|
1713
|
+
sessionId,
|
|
1714
|
+
authUrl: authUrl.toString()
|
|
1715
|
+
};
|
|
1716
|
+
}
|
|
1717
|
+
/**
|
|
1718
|
+
* Check mobile OAuth session status (single check)
|
|
1719
|
+
*
|
|
1720
|
+
* @param sessionId - Session ID from initiateMobileOAuth
|
|
1721
|
+
* @returns Tokens if session is complete, null if still pending
|
|
1722
|
+
*/
|
|
1723
|
+
async checkMobileOAuthSession(sessionId) {
|
|
1724
|
+
try {
|
|
1725
|
+
const response = await fetch(`${this.authUrl}/api/auth/mobile-session/${sessionId}`, {
|
|
1726
|
+
method: "GET",
|
|
1727
|
+
headers: {
|
|
1728
|
+
"Content-Type": "application/json"
|
|
1729
|
+
}
|
|
1730
|
+
});
|
|
1731
|
+
if (response.status === 404 || response.status === 202) {
|
|
1732
|
+
return null;
|
|
1733
|
+
}
|
|
1734
|
+
if (!response.ok) {
|
|
1735
|
+
const errorData = await response.json();
|
|
1736
|
+
const errorCode = this.mapErrorCodeFromResponse(errorData.code);
|
|
1737
|
+
throw new BlinkAuthError(
|
|
1738
|
+
errorCode,
|
|
1739
|
+
errorData.error || "Failed to check OAuth session"
|
|
1740
|
+
);
|
|
1741
|
+
}
|
|
1742
|
+
const data = await response.json();
|
|
1743
|
+
return {
|
|
1744
|
+
access_token: data.access_token,
|
|
1745
|
+
refresh_token: data.refresh_token,
|
|
1746
|
+
token_type: data.token_type || "Bearer",
|
|
1747
|
+
expires_in: data.expires_in || 3600,
|
|
1748
|
+
refresh_expires_in: data.refresh_expires_in
|
|
1749
|
+
};
|
|
1750
|
+
} catch (error) {
|
|
1751
|
+
if (error instanceof BlinkAuthError) {
|
|
1752
|
+
throw error;
|
|
1753
|
+
}
|
|
1754
|
+
throw new BlinkAuthError(
|
|
1755
|
+
"NETWORK_ERROR" /* NETWORK_ERROR */,
|
|
1756
|
+
`Network error: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1757
|
+
);
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
/**
|
|
1761
|
+
* Poll mobile OAuth session until complete (convenience method)
|
|
1762
|
+
*
|
|
1763
|
+
* @param sessionId - Session ID from initiateMobileOAuth
|
|
1764
|
+
* @param options - Polling options
|
|
1765
|
+
* @returns Authenticated user
|
|
1766
|
+
*
|
|
1767
|
+
* @example
|
|
1768
|
+
* const { sessionId, authUrl } = await blink.auth.initiateMobileOAuth('google');
|
|
1769
|
+
* await WebBrowser.openAuthSessionAsync(authUrl);
|
|
1770
|
+
* const user = await blink.auth.pollMobileOAuthSession(sessionId, {
|
|
1771
|
+
* maxAttempts: 60,
|
|
1772
|
+
* intervalMs: 1000
|
|
1773
|
+
* });
|
|
1774
|
+
*/
|
|
1775
|
+
async pollMobileOAuthSession(sessionId, options) {
|
|
1776
|
+
const maxAttempts = options?.maxAttempts || 60;
|
|
1777
|
+
const intervalMs = options?.intervalMs || 1e3;
|
|
1778
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1779
|
+
const tokens = await this.checkMobileOAuthSession(sessionId);
|
|
1780
|
+
if (tokens) {
|
|
1781
|
+
await this.setTokens(tokens, true);
|
|
1782
|
+
return this.authState.user;
|
|
1783
|
+
}
|
|
1784
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
1785
|
+
}
|
|
1786
|
+
throw new BlinkAuthError(
|
|
1787
|
+
"AUTH_TIMEOUT" /* AUTH_TIMEOUT */,
|
|
1788
|
+
"Mobile OAuth session timed out"
|
|
1789
|
+
);
|
|
1790
|
+
}
|
|
1791
|
+
/**
|
|
1792
|
+
* Sign in with OAuth provider using expo-web-browser (React Native)
|
|
1793
|
+
*
|
|
1794
|
+
* This is a convenience method that handles the entire flow:
|
|
1795
|
+
* 1. Initiates mobile OAuth session
|
|
1796
|
+
* 2. Returns auth URL to open in WebBrowser
|
|
1797
|
+
* 3. Provides polling function to call after browser opens
|
|
1798
|
+
*
|
|
1799
|
+
* @param provider - OAuth provider
|
|
1800
|
+
* @returns Object with authUrl and authenticate function
|
|
1801
|
+
*
|
|
1802
|
+
* @example
|
|
1803
|
+
* import * as WebBrowser from 'expo-web-browser';
|
|
1804
|
+
*
|
|
1805
|
+
* const { authUrl, authenticate } = await blink.auth.signInWithProviderMobile('google');
|
|
1806
|
+
*
|
|
1807
|
+
* // Open browser
|
|
1808
|
+
* await WebBrowser.openAuthSessionAsync(authUrl);
|
|
1809
|
+
*
|
|
1810
|
+
* // Wait for authentication
|
|
1811
|
+
* const user = await authenticate();
|
|
1812
|
+
*/
|
|
1813
|
+
async signInWithProviderMobile(provider, options) {
|
|
1814
|
+
const { sessionId, authUrl } = await this.initiateMobileOAuth(provider, options);
|
|
1815
|
+
return {
|
|
1816
|
+
authUrl,
|
|
1817
|
+
authenticate: () => this.pollMobileOAuthSession(sessionId, {
|
|
1818
|
+
maxAttempts: 60,
|
|
1819
|
+
intervalMs: 1e3
|
|
1820
|
+
})
|
|
1821
|
+
};
|
|
1822
|
+
}
|
|
1823
|
+
/**
|
|
1824
|
+
* Universal OAuth flow using session-based authentication (internal)
|
|
1825
|
+
* Works on ALL platforms: Web, iOS, Android
|
|
1826
|
+
* Uses expo-web-browser to open auth URL and polls for completion
|
|
1827
|
+
*/
|
|
1828
|
+
async signInWithProviderUniversal(provider, options) {
|
|
1829
|
+
const webBrowser = this.authConfig.webBrowser;
|
|
1830
|
+
if (!webBrowser) {
|
|
1831
|
+
throw new BlinkAuthError(
|
|
1832
|
+
"NETWORK_ERROR" /* NETWORK_ERROR */,
|
|
1833
|
+
"webBrowser module is required for universal OAuth flow"
|
|
1834
|
+
);
|
|
1835
|
+
}
|
|
1836
|
+
const { sessionId, authUrl } = await this.initiateMobileOAuth(provider, options);
|
|
1837
|
+
console.log("\u{1F510} Opening OAuth browser for", provider);
|
|
1838
|
+
const result = await webBrowser.openAuthSessionAsync(authUrl);
|
|
1839
|
+
console.log("\u{1F510} Browser closed with result:", result.type);
|
|
1840
|
+
try {
|
|
1841
|
+
const user = await this.pollMobileOAuthSession(sessionId, {
|
|
1842
|
+
maxAttempts: 60,
|
|
1843
|
+
// 30 seconds (500ms intervals)
|
|
1844
|
+
intervalMs: 500
|
|
1845
|
+
});
|
|
1846
|
+
console.log("\u2705 OAuth completed successfully");
|
|
1847
|
+
return user;
|
|
1848
|
+
} catch (pollError) {
|
|
1849
|
+
if (result.type === "cancel" || result.type === "dismiss") {
|
|
1850
|
+
throw new BlinkAuthError(
|
|
1851
|
+
"POPUP_CANCELED" /* POPUP_CANCELED */,
|
|
1852
|
+
"Authentication was canceled"
|
|
1853
|
+
);
|
|
1854
|
+
}
|
|
1855
|
+
throw pollError;
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1507
1858
|
/**
|
|
1508
1859
|
* Generic provider sign-in method (headless mode)
|
|
1860
|
+
*
|
|
1861
|
+
* **Universal OAuth** - Works seamlessly on both Web and React Native!
|
|
1862
|
+
*
|
|
1863
|
+
* When `webBrowser` is configured in the client, this method automatically
|
|
1864
|
+
* uses the session-based OAuth flow that works on ALL platforms.
|
|
1865
|
+
*
|
|
1866
|
+
* **Universal Setup (configure once, works everywhere):**
|
|
1867
|
+
* ```typescript
|
|
1868
|
+
* import * as WebBrowser from 'expo-web-browser'
|
|
1869
|
+
* import AsyncStorage from '@react-native-async-storage/async-storage'
|
|
1870
|
+
*
|
|
1871
|
+
* const blink = createClient({
|
|
1872
|
+
* projectId: 'your-project',
|
|
1873
|
+
* auth: {
|
|
1874
|
+
* mode: 'headless',
|
|
1875
|
+
* webBrowser: WebBrowser // Pass the module here
|
|
1876
|
+
* },
|
|
1877
|
+
* storage: new AsyncStorageAdapter(AsyncStorage)
|
|
1878
|
+
* })
|
|
1879
|
+
*
|
|
1880
|
+
* // Now this works on ALL platforms - no platform checks needed!
|
|
1881
|
+
* const user = await blink.auth.signInWithGoogle()
|
|
1882
|
+
* ```
|
|
1883
|
+
*
|
|
1884
|
+
* @param provider - OAuth provider (google, github, apple, etc.)
|
|
1885
|
+
* @param options - Optional redirect URL and metadata
|
|
1886
|
+
* @returns Promise that resolves with authenticated user
|
|
1509
1887
|
*/
|
|
1510
1888
|
async signInWithProvider(provider, options) {
|
|
1511
1889
|
if (this.authConfig.mode !== "headless") {
|
|
1512
1890
|
throw new BlinkAuthError("INVALID_CREDENTIALS" /* INVALID_CREDENTIALS */, "signInWithProvider is only available in headless mode");
|
|
1513
1891
|
}
|
|
1892
|
+
if (this.authConfig.webBrowser) {
|
|
1893
|
+
return this.signInWithProviderUniversal(provider, options);
|
|
1894
|
+
}
|
|
1895
|
+
if (isReactNative2()) {
|
|
1896
|
+
throw new BlinkAuthError(
|
|
1897
|
+
"NETWORK_ERROR" /* NETWORK_ERROR */,
|
|
1898
|
+
'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!'
|
|
1899
|
+
);
|
|
1900
|
+
}
|
|
1901
|
+
if (!hasWindow()) {
|
|
1902
|
+
throw new BlinkAuthError("NETWORK_ERROR" /* NETWORK_ERROR */, "signInWithProvider requires a browser environment");
|
|
1903
|
+
}
|
|
1514
1904
|
return new Promise((resolve, reject) => {
|
|
1515
1905
|
const state = this.generateState();
|
|
1516
1906
|
try {
|
|
1517
|
-
|
|
1907
|
+
const sessionStorage = getSessionStorage();
|
|
1908
|
+
if (sessionStorage) {
|
|
1518
1909
|
sessionStorage.setItem("blink_oauth_state", state);
|
|
1519
1910
|
}
|
|
1520
1911
|
} catch {
|
|
1521
1912
|
}
|
|
1522
|
-
const redirectUrl = options?.redirectUrl ||
|
|
1913
|
+
const redirectUrl = options?.redirectUrl || getLocationOrigin() || "";
|
|
1523
1914
|
const popupUrl = new URL("/auth", this.authUrl);
|
|
1524
1915
|
popupUrl.searchParams.set("provider", provider);
|
|
1525
1916
|
popupUrl.searchParams.set("project_id", this.config.projectId);
|
|
@@ -1548,7 +1939,8 @@ var BlinkAuth = class {
|
|
|
1548
1939
|
if (event.data?.type === "BLINK_AUTH_TOKENS") {
|
|
1549
1940
|
const { access_token, refresh_token, token_type, expires_in, refresh_expires_in, projectId, state: returnedState } = event.data;
|
|
1550
1941
|
try {
|
|
1551
|
-
const
|
|
1942
|
+
const sessionStorage = getSessionStorage();
|
|
1943
|
+
const expected = sessionStorage?.getItem("blink_oauth_state");
|
|
1552
1944
|
if (returnedState && expected && returnedState !== expected) {
|
|
1553
1945
|
reject(new BlinkAuthError("VERIFICATION_FAILED" /* VERIFICATION_FAILED */, "State mismatch"));
|
|
1554
1946
|
clearTimeout(timeoutId);
|
|
@@ -2072,6 +2464,48 @@ var BlinkAuth = class {
|
|
|
2072
2464
|
};
|
|
2073
2465
|
await this.setTokens(tokens, persist);
|
|
2074
2466
|
}
|
|
2467
|
+
/**
|
|
2468
|
+
* Manually set auth session from tokens (React Native deep link OAuth)
|
|
2469
|
+
*
|
|
2470
|
+
* Use this method to set the user session after receiving tokens from a deep link callback.
|
|
2471
|
+
* This is the React Native equivalent of automatic URL token detection on web.
|
|
2472
|
+
*
|
|
2473
|
+
* @param tokens - Auth tokens received from deep link or OAuth callback
|
|
2474
|
+
* @param persist - Whether to persist tokens to storage (default: true)
|
|
2475
|
+
*
|
|
2476
|
+
* @example
|
|
2477
|
+
* // React Native: Handle deep link OAuth callback
|
|
2478
|
+
* import * as Linking from 'expo-linking'
|
|
2479
|
+
*
|
|
2480
|
+
* Linking.addEventListener('url', async ({ url }) => {
|
|
2481
|
+
* const { queryParams } = Linking.parse(url)
|
|
2482
|
+
*
|
|
2483
|
+
* if (queryParams.access_token) {
|
|
2484
|
+
* await blink.auth.setSession({
|
|
2485
|
+
* access_token: queryParams.access_token,
|
|
2486
|
+
* refresh_token: queryParams.refresh_token,
|
|
2487
|
+
* expires_in: parseInt(queryParams.expires_in) || 3600,
|
|
2488
|
+
* refresh_expires_in: parseInt(queryParams.refresh_expires_in)
|
|
2489
|
+
* })
|
|
2490
|
+
*
|
|
2491
|
+
* console.log('User authenticated:', blink.auth.currentUser())
|
|
2492
|
+
* }
|
|
2493
|
+
* })
|
|
2494
|
+
*/
|
|
2495
|
+
async setSession(tokens, persist = true) {
|
|
2496
|
+
const authTokens = {
|
|
2497
|
+
access_token: tokens.access_token,
|
|
2498
|
+
refresh_token: tokens.refresh_token,
|
|
2499
|
+
token_type: "Bearer",
|
|
2500
|
+
expires_in: tokens.expires_in || 3600,
|
|
2501
|
+
// Default 1 hour
|
|
2502
|
+
refresh_expires_in: tokens.refresh_expires_in,
|
|
2503
|
+
issued_at: Math.floor(Date.now() / 1e3)
|
|
2504
|
+
};
|
|
2505
|
+
await this.setTokens(authTokens, persist);
|
|
2506
|
+
const user = await this.me();
|
|
2507
|
+
return user;
|
|
2508
|
+
}
|
|
2075
2509
|
/**
|
|
2076
2510
|
* Refresh access token using refresh token
|
|
2077
2511
|
*/
|
|
@@ -2305,12 +2739,13 @@ var BlinkAuth = class {
|
|
|
2305
2739
|
}
|
|
2306
2740
|
}
|
|
2307
2741
|
extractTokensFromUrl() {
|
|
2308
|
-
|
|
2309
|
-
|
|
2742
|
+
const search = getLocationSearch();
|
|
2743
|
+
if (!search) return null;
|
|
2744
|
+
const params = new URLSearchParams(search);
|
|
2310
2745
|
const accessToken = params.get("access_token");
|
|
2311
2746
|
const refreshToken = params.get("refresh_token");
|
|
2312
2747
|
console.log("\u{1F50D} Extracting tokens from URL:", {
|
|
2313
|
-
url:
|
|
2748
|
+
url: getLocationHref(),
|
|
2314
2749
|
accessToken: accessToken ? `${accessToken.substring(0, 20)}...` : null,
|
|
2315
2750
|
refreshToken: refreshToken ? `${refreshToken.substring(0, 20)}...` : null,
|
|
2316
2751
|
allParams: Object.fromEntries(params.entries())
|
|
@@ -2337,8 +2772,9 @@ var BlinkAuth = class {
|
|
|
2337
2772
|
return null;
|
|
2338
2773
|
}
|
|
2339
2774
|
clearUrlTokens() {
|
|
2340
|
-
|
|
2341
|
-
|
|
2775
|
+
const href = getLocationHref();
|
|
2776
|
+
if (!href || !hasWindowLocation()) return;
|
|
2777
|
+
const url = new URL(href);
|
|
2342
2778
|
url.searchParams.delete("access_token");
|
|
2343
2779
|
url.searchParams.delete("refresh_token");
|
|
2344
2780
|
url.searchParams.delete("token_type");
|
|
@@ -2353,7 +2789,7 @@ var BlinkAuth = class {
|
|
|
2353
2789
|
console.log("\u{1F9F9} URL cleaned up, removed auth parameters");
|
|
2354
2790
|
}
|
|
2355
2791
|
redirectToAuth() {
|
|
2356
|
-
if (
|
|
2792
|
+
if (hasWindowLocation()) {
|
|
2357
2793
|
this.login();
|
|
2358
2794
|
}
|
|
2359
2795
|
}
|
|
@@ -2385,12 +2821,25 @@ var BlinkAuth = class {
|
|
|
2385
2821
|
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
|
|
2386
2822
|
}
|
|
2387
2823
|
}
|
|
2824
|
+
/**
|
|
2825
|
+
* Generate unique session ID for mobile OAuth
|
|
2826
|
+
*/
|
|
2827
|
+
generateSessionId() {
|
|
2828
|
+
if (typeof crypto !== "undefined" && crypto.getRandomValues) {
|
|
2829
|
+
const array = new Uint8Array(32);
|
|
2830
|
+
crypto.getRandomValues(array);
|
|
2831
|
+
return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
2832
|
+
} else {
|
|
2833
|
+
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2388
2836
|
/**
|
|
2389
2837
|
* Extract magic link token from URL
|
|
2390
2838
|
*/
|
|
2391
2839
|
extractMagicTokenFromUrl() {
|
|
2392
|
-
|
|
2393
|
-
|
|
2840
|
+
const search = getLocationSearch();
|
|
2841
|
+
if (!search) return null;
|
|
2842
|
+
const params = new URLSearchParams(search);
|
|
2394
2843
|
return params.get("magic_token") || params.get("token");
|
|
2395
2844
|
}
|
|
2396
2845
|
/**
|
|
@@ -2446,7 +2895,7 @@ var BlinkAuth = class {
|
|
|
2446
2895
|
* Setup cross-tab authentication synchronization
|
|
2447
2896
|
*/
|
|
2448
2897
|
setupCrossTabSync() {
|
|
2449
|
-
if (!isWeb) return;
|
|
2898
|
+
if (!isWeb || !hasWindow()) return;
|
|
2450
2899
|
window.addEventListener("storage", (e) => {
|
|
2451
2900
|
if (e.key === this.getStorageKey("tokens")) {
|
|
2452
2901
|
const newTokens = e.newValue ? JSON.parse(e.newValue) : null;
|
|
@@ -4750,9 +5199,9 @@ var BlinkAnalyticsImpl = class {
|
|
|
4750
5199
|
user_id: this.userId,
|
|
4751
5200
|
user_email: this.userEmail,
|
|
4752
5201
|
session_id: sessionId,
|
|
4753
|
-
pathname:
|
|
4754
|
-
referrer:
|
|
4755
|
-
screen_width:
|
|
5202
|
+
pathname: getLocationPathname(),
|
|
5203
|
+
referrer: getDocumentReferrer(),
|
|
5204
|
+
screen_width: getWindowInnerWidth(),
|
|
4756
5205
|
channel,
|
|
4757
5206
|
utm_source: this.utmParams.utm_source || this.persistedAttribution.utm_source || null,
|
|
4758
5207
|
utm_medium: this.utmParams.utm_medium || this.persistedAttribution.utm_medium || null,
|
|
@@ -4905,7 +5354,7 @@ var BlinkAnalyticsImpl = class {
|
|
|
4905
5354
|
window.__blinkAnalyticsInstances?.add(this);
|
|
4906
5355
|
}
|
|
4907
5356
|
setupUnloadListener() {
|
|
4908
|
-
if (!isWeb) return;
|
|
5357
|
+
if (!isWeb || !hasWindow()) return;
|
|
4909
5358
|
window.addEventListener("pagehide", () => {
|
|
4910
5359
|
this.flush();
|
|
4911
5360
|
});
|
|
@@ -4915,7 +5364,12 @@ var BlinkAnalyticsImpl = class {
|
|
|
4915
5364
|
}
|
|
4916
5365
|
captureUTMParams() {
|
|
4917
5366
|
if (!isWeb) return;
|
|
4918
|
-
const
|
|
5367
|
+
const search = getLocationSearch();
|
|
5368
|
+
if (!search) {
|
|
5369
|
+
this.utmParams = {};
|
|
5370
|
+
return;
|
|
5371
|
+
}
|
|
5372
|
+
const urlParams = new URLSearchParams(search);
|
|
4919
5373
|
this.utmParams = {
|
|
4920
5374
|
utm_source: urlParams.get("utm_source"),
|
|
4921
5375
|
utm_medium: urlParams.get("utm_medium"),
|
|
@@ -4952,7 +5406,7 @@ var BlinkAnalyticsImpl = class {
|
|
|
4952
5406
|
}
|
|
4953
5407
|
}
|
|
4954
5408
|
detectChannel() {
|
|
4955
|
-
const referrer =
|
|
5409
|
+
const referrer = getDocumentReferrer();
|
|
4956
5410
|
const utmMedium = this.utmParams.utm_medium;
|
|
4957
5411
|
this.utmParams.utm_source;
|
|
4958
5412
|
if (utmMedium) {
|