@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.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: "
|
|
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
|
-
|
|
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 =
|
|
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,13 +1281,15 @@ var BlinkAuth = class {
|
|
|
1142
1281
|
return;
|
|
1143
1282
|
}
|
|
1144
1283
|
}
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
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;
|
|
1292
|
+
}
|
|
1152
1293
|
}
|
|
1153
1294
|
const storedTokens = await this.getStoredTokens();
|
|
1154
1295
|
if (storedTokens) {
|
|
@@ -1172,11 +1313,11 @@ var BlinkAuth = class {
|
|
|
1172
1313
|
}
|
|
1173
1314
|
}
|
|
1174
1315
|
console.log("\u274C No tokens found");
|
|
1175
|
-
if (this.config.authRequired) {
|
|
1316
|
+
if (this.config.authRequired && hasWindowLocation()) {
|
|
1176
1317
|
console.log("\u{1F504} Auth required, redirecting to auth page...");
|
|
1177
1318
|
this.redirectToAuth();
|
|
1178
1319
|
} else {
|
|
1179
|
-
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");
|
|
1180
1321
|
}
|
|
1181
1322
|
} finally {
|
|
1182
1323
|
this.setLoading(false);
|
|
@@ -1187,15 +1328,20 @@ var BlinkAuth = class {
|
|
|
1187
1328
|
* Redirect to Blink auth page
|
|
1188
1329
|
*/
|
|
1189
1330
|
login(nextUrl) {
|
|
1331
|
+
if (!hasWindowLocation()) {
|
|
1332
|
+
console.warn("login() called in non-browser environment (no window.location available)");
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1190
1335
|
let redirectUrl = nextUrl || this.authConfig.redirectUrl;
|
|
1191
|
-
if (!redirectUrl
|
|
1192
|
-
|
|
1193
|
-
|
|
1336
|
+
if (!redirectUrl) {
|
|
1337
|
+
const href = getLocationHref();
|
|
1338
|
+
if (href && href.startsWith("http")) {
|
|
1339
|
+
redirectUrl = href;
|
|
1194
1340
|
} else {
|
|
1195
|
-
redirectUrl =
|
|
1341
|
+
redirectUrl = constructFullUrl() || void 0;
|
|
1196
1342
|
}
|
|
1197
1343
|
}
|
|
1198
|
-
if (redirectUrl
|
|
1344
|
+
if (redirectUrl) {
|
|
1199
1345
|
try {
|
|
1200
1346
|
const url = new URL(redirectUrl);
|
|
1201
1347
|
url.searchParams.delete("redirect_url");
|
|
@@ -1210,16 +1356,14 @@ var BlinkAuth = class {
|
|
|
1210
1356
|
if (this.config.projectId) {
|
|
1211
1357
|
authUrl.searchParams.set("project_id", this.config.projectId);
|
|
1212
1358
|
}
|
|
1213
|
-
|
|
1214
|
-
window.location.href = authUrl.toString();
|
|
1215
|
-
}
|
|
1359
|
+
window.location.href = authUrl.toString();
|
|
1216
1360
|
}
|
|
1217
1361
|
/**
|
|
1218
1362
|
* Logout and clear stored tokens
|
|
1219
1363
|
*/
|
|
1220
1364
|
logout(redirectUrl) {
|
|
1221
1365
|
this.clearTokens();
|
|
1222
|
-
if (redirectUrl &&
|
|
1366
|
+
if (redirectUrl && hasWindowLocation()) {
|
|
1223
1367
|
window.location.href = redirectUrl;
|
|
1224
1368
|
}
|
|
1225
1369
|
}
|
|
@@ -1468,6 +1612,16 @@ var BlinkAuth = class {
|
|
|
1468
1612
|
}
|
|
1469
1613
|
/**
|
|
1470
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
|
+
* ```
|
|
1471
1625
|
*/
|
|
1472
1626
|
async signInWithGoogle(options) {
|
|
1473
1627
|
if (this.authConfig.mode !== "headless") {
|
|
@@ -1477,6 +1631,9 @@ var BlinkAuth = class {
|
|
|
1477
1631
|
}
|
|
1478
1632
|
/**
|
|
1479
1633
|
* Sign in with GitHub (headless mode)
|
|
1634
|
+
*
|
|
1635
|
+
* **Universal OAuth** - Works on both Web and React Native!
|
|
1636
|
+
* See signInWithGoogle() for setup instructions.
|
|
1480
1637
|
*/
|
|
1481
1638
|
async signInWithGitHub(options) {
|
|
1482
1639
|
if (this.authConfig.mode !== "headless") {
|
|
@@ -1486,6 +1643,9 @@ var BlinkAuth = class {
|
|
|
1486
1643
|
}
|
|
1487
1644
|
/**
|
|
1488
1645
|
* Sign in with Apple (headless mode)
|
|
1646
|
+
*
|
|
1647
|
+
* **Universal OAuth** - Works on both Web and React Native!
|
|
1648
|
+
* See signInWithGoogle() for setup instructions.
|
|
1489
1649
|
*/
|
|
1490
1650
|
async signInWithApple(options) {
|
|
1491
1651
|
if (this.authConfig.mode !== "headless") {
|
|
@@ -1495,6 +1655,9 @@ var BlinkAuth = class {
|
|
|
1495
1655
|
}
|
|
1496
1656
|
/**
|
|
1497
1657
|
* Sign in with Microsoft (headless mode)
|
|
1658
|
+
*
|
|
1659
|
+
* **Universal OAuth** - Works on both Web and React Native!
|
|
1660
|
+
* See signInWithGoogle() for setup instructions.
|
|
1498
1661
|
*/
|
|
1499
1662
|
async signInWithMicrosoft(options) {
|
|
1500
1663
|
if (this.authConfig.mode !== "headless") {
|
|
@@ -1502,22 +1665,250 @@ var BlinkAuth = class {
|
|
|
1502
1665
|
}
|
|
1503
1666
|
return this.signInWithProvider("microsoft", options);
|
|
1504
1667
|
}
|
|
1668
|
+
/**
|
|
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
|
|
1720
|
+
*/
|
|
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
|
+
}
|
|
1728
|
+
});
|
|
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
|
|
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;
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1505
1856
|
/**
|
|
1506
1857
|
* Generic provider sign-in method (headless mode)
|
|
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
|
|
1507
1885
|
*/
|
|
1508
1886
|
async signInWithProvider(provider, options) {
|
|
1509
1887
|
if (this.authConfig.mode !== "headless") {
|
|
1510
1888
|
throw new BlinkAuthError("INVALID_CREDENTIALS" /* INVALID_CREDENTIALS */, "signInWithProvider is only available in headless mode");
|
|
1511
1889
|
}
|
|
1890
|
+
if (this.authConfig.webBrowser) {
|
|
1891
|
+
return this.signInWithProviderUniversal(provider, options);
|
|
1892
|
+
}
|
|
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
|
+
);
|
|
1898
|
+
}
|
|
1899
|
+
if (!hasWindow()) {
|
|
1900
|
+
throw new BlinkAuthError("NETWORK_ERROR" /* NETWORK_ERROR */, "signInWithProvider requires a browser environment");
|
|
1901
|
+
}
|
|
1512
1902
|
return new Promise((resolve, reject) => {
|
|
1513
1903
|
const state = this.generateState();
|
|
1514
1904
|
try {
|
|
1515
|
-
|
|
1905
|
+
const sessionStorage = getSessionStorage();
|
|
1906
|
+
if (sessionStorage) {
|
|
1516
1907
|
sessionStorage.setItem("blink_oauth_state", state);
|
|
1517
1908
|
}
|
|
1518
1909
|
} catch {
|
|
1519
1910
|
}
|
|
1520
|
-
const redirectUrl = options?.redirectUrl ||
|
|
1911
|
+
const redirectUrl = options?.redirectUrl || getLocationOrigin() || "";
|
|
1521
1912
|
const popupUrl = new URL("/auth", this.authUrl);
|
|
1522
1913
|
popupUrl.searchParams.set("provider", provider);
|
|
1523
1914
|
popupUrl.searchParams.set("project_id", this.config.projectId);
|
|
@@ -1546,7 +1937,8 @@ var BlinkAuth = class {
|
|
|
1546
1937
|
if (event.data?.type === "BLINK_AUTH_TOKENS") {
|
|
1547
1938
|
const { access_token, refresh_token, token_type, expires_in, refresh_expires_in, projectId, state: returnedState } = event.data;
|
|
1548
1939
|
try {
|
|
1549
|
-
const
|
|
1940
|
+
const sessionStorage = getSessionStorage();
|
|
1941
|
+
const expected = sessionStorage?.getItem("blink_oauth_state");
|
|
1550
1942
|
if (returnedState && expected && returnedState !== expected) {
|
|
1551
1943
|
reject(new BlinkAuthError("VERIFICATION_FAILED" /* VERIFICATION_FAILED */, "State mismatch"));
|
|
1552
1944
|
clearTimeout(timeoutId);
|
|
@@ -2070,6 +2462,48 @@ var BlinkAuth = class {
|
|
|
2070
2462
|
};
|
|
2071
2463
|
await this.setTokens(tokens, persist);
|
|
2072
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
|
+
}
|
|
2073
2507
|
/**
|
|
2074
2508
|
* Refresh access token using refresh token
|
|
2075
2509
|
*/
|
|
@@ -2303,12 +2737,13 @@ var BlinkAuth = class {
|
|
|
2303
2737
|
}
|
|
2304
2738
|
}
|
|
2305
2739
|
extractTokensFromUrl() {
|
|
2306
|
-
|
|
2307
|
-
|
|
2740
|
+
const search = getLocationSearch();
|
|
2741
|
+
if (!search) return null;
|
|
2742
|
+
const params = new URLSearchParams(search);
|
|
2308
2743
|
const accessToken = params.get("access_token");
|
|
2309
2744
|
const refreshToken = params.get("refresh_token");
|
|
2310
2745
|
console.log("\u{1F50D} Extracting tokens from URL:", {
|
|
2311
|
-
url:
|
|
2746
|
+
url: getLocationHref(),
|
|
2312
2747
|
accessToken: accessToken ? `${accessToken.substring(0, 20)}...` : null,
|
|
2313
2748
|
refreshToken: refreshToken ? `${refreshToken.substring(0, 20)}...` : null,
|
|
2314
2749
|
allParams: Object.fromEntries(params.entries())
|
|
@@ -2335,8 +2770,9 @@ var BlinkAuth = class {
|
|
|
2335
2770
|
return null;
|
|
2336
2771
|
}
|
|
2337
2772
|
clearUrlTokens() {
|
|
2338
|
-
|
|
2339
|
-
|
|
2773
|
+
const href = getLocationHref();
|
|
2774
|
+
if (!href || !hasWindowLocation()) return;
|
|
2775
|
+
const url = new URL(href);
|
|
2340
2776
|
url.searchParams.delete("access_token");
|
|
2341
2777
|
url.searchParams.delete("refresh_token");
|
|
2342
2778
|
url.searchParams.delete("token_type");
|
|
@@ -2351,7 +2787,7 @@ var BlinkAuth = class {
|
|
|
2351
2787
|
console.log("\u{1F9F9} URL cleaned up, removed auth parameters");
|
|
2352
2788
|
}
|
|
2353
2789
|
redirectToAuth() {
|
|
2354
|
-
if (
|
|
2790
|
+
if (hasWindowLocation()) {
|
|
2355
2791
|
this.login();
|
|
2356
2792
|
}
|
|
2357
2793
|
}
|
|
@@ -2383,12 +2819,25 @@ var BlinkAuth = class {
|
|
|
2383
2819
|
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
|
|
2384
2820
|
}
|
|
2385
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
|
+
}
|
|
2386
2834
|
/**
|
|
2387
2835
|
* Extract magic link token from URL
|
|
2388
2836
|
*/
|
|
2389
2837
|
extractMagicTokenFromUrl() {
|
|
2390
|
-
|
|
2391
|
-
|
|
2838
|
+
const search = getLocationSearch();
|
|
2839
|
+
if (!search) return null;
|
|
2840
|
+
const params = new URLSearchParams(search);
|
|
2392
2841
|
return params.get("magic_token") || params.get("token");
|
|
2393
2842
|
}
|
|
2394
2843
|
/**
|
|
@@ -2444,7 +2893,7 @@ var BlinkAuth = class {
|
|
|
2444
2893
|
* Setup cross-tab authentication synchronization
|
|
2445
2894
|
*/
|
|
2446
2895
|
setupCrossTabSync() {
|
|
2447
|
-
if (!isWeb) return;
|
|
2896
|
+
if (!isWeb || !hasWindow()) return;
|
|
2448
2897
|
window.addEventListener("storage", (e) => {
|
|
2449
2898
|
if (e.key === this.getStorageKey("tokens")) {
|
|
2450
2899
|
const newTokens = e.newValue ? JSON.parse(e.newValue) : null;
|
|
@@ -4748,9 +5197,9 @@ var BlinkAnalyticsImpl = class {
|
|
|
4748
5197
|
user_id: this.userId,
|
|
4749
5198
|
user_email: this.userEmail,
|
|
4750
5199
|
session_id: sessionId,
|
|
4751
|
-
pathname:
|
|
4752
|
-
referrer:
|
|
4753
|
-
screen_width:
|
|
5200
|
+
pathname: getLocationPathname(),
|
|
5201
|
+
referrer: getDocumentReferrer(),
|
|
5202
|
+
screen_width: getWindowInnerWidth(),
|
|
4754
5203
|
channel,
|
|
4755
5204
|
utm_source: this.utmParams.utm_source || this.persistedAttribution.utm_source || null,
|
|
4756
5205
|
utm_medium: this.utmParams.utm_medium || this.persistedAttribution.utm_medium || null,
|
|
@@ -4903,7 +5352,7 @@ var BlinkAnalyticsImpl = class {
|
|
|
4903
5352
|
window.__blinkAnalyticsInstances?.add(this);
|
|
4904
5353
|
}
|
|
4905
5354
|
setupUnloadListener() {
|
|
4906
|
-
if (!isWeb) return;
|
|
5355
|
+
if (!isWeb || !hasWindow()) return;
|
|
4907
5356
|
window.addEventListener("pagehide", () => {
|
|
4908
5357
|
this.flush();
|
|
4909
5358
|
});
|
|
@@ -4913,7 +5362,12 @@ var BlinkAnalyticsImpl = class {
|
|
|
4913
5362
|
}
|
|
4914
5363
|
captureUTMParams() {
|
|
4915
5364
|
if (!isWeb) return;
|
|
4916
|
-
const
|
|
5365
|
+
const search = getLocationSearch();
|
|
5366
|
+
if (!search) {
|
|
5367
|
+
this.utmParams = {};
|
|
5368
|
+
return;
|
|
5369
|
+
}
|
|
5370
|
+
const urlParams = new URLSearchParams(search);
|
|
4917
5371
|
this.utmParams = {
|
|
4918
5372
|
utm_source: urlParams.get("utm_source"),
|
|
4919
5373
|
utm_medium: urlParams.get("utm_medium"),
|
|
@@ -4950,7 +5404,7 @@ var BlinkAnalyticsImpl = class {
|
|
|
4950
5404
|
}
|
|
4951
5405
|
}
|
|
4952
5406
|
detectChannel() {
|
|
4953
|
-
const referrer =
|
|
5407
|
+
const referrer = getDocumentReferrer();
|
|
4954
5408
|
const utmMedium = this.utmParams.utm_medium;
|
|
4955
5409
|
this.utmParams.utm_source;
|
|
4956
5410
|
if (utmMedium) {
|