├── .github ├── ISSUE_TEMPLATES │ ├── bug_report.md │ ├── ci.md │ ├── documentation.md │ └── feature-request.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── run-test-build-deploy.yaml ├── .gitignore ├── README.md ├── commitlint.config.cjs ├── example ├── .gitignore ├── README.md ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.js ├── public │ ├── next.svg │ └── vercel.svg ├── src │ └── app │ │ ├── debug │ │ ├── README.md │ │ └── page.tsx │ │ ├── favicon.ico │ │ ├── globals.css │ │ ├── layout.tsx │ │ └── page.tsx ├── tailwind.config.ts └── tsconfig.json ├── jest.config.js ├── package-lock.json ├── package.json ├── readme ├── depemdency-diagram.svg ├── usage-flow-2.svg ├── usage-flow-3.svg └── usage-flow.svg ├── scripts └── build.sh ├── src ├── Reclaim.ts ├── contract-types │ ├── config.json │ ├── contracts │ │ ├── factories │ │ │ ├── Reclaim__factory.ts │ │ │ └── index.ts │ │ └── index.ts │ └── index.ts ├── index.ts ├── smart-contract.ts ├── utils │ ├── __tests__ │ │ ├── device.test.ts │ │ └── helper.test.ts │ ├── constants.ts │ ├── device.ts │ ├── errors.ts │ ├── helper.ts │ ├── interfaces.ts │ ├── logger.ts │ ├── modalUtils.ts │ ├── proofUtils.ts │ ├── sessionUtils.ts │ ├── types.ts │ └── validationUtils.ts └── witness.ts └── tsconfig.json /.github/ISSUE_TEMPLATES/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐛 General Bug Report 3 | about: Report an issue to help improve the project. 4 | title: '' 5 | labels: 'kind/bug' 6 | assignees: '' 7 | --- 8 | 9 | ### Current Behavior 10 | 11 | 12 | ### Expected Behavior 13 | 14 | 15 | ### Screenshots/Logs 16 | 17 | 18 | ### To Reproduce 19 | 1. Go to '...' 20 | 2. Click on '....' 21 | 3. Scroll down to '....' 22 | 4. See error 23 | 24 | --- 25 | ## Before You Begin 26 | 27 | Before submitting an issue, please ensure you have completed the following steps: 28 | 29 | - [ ] I have read and agree to follow the [Code of Conduct](https://github.com/reclaimprotocol/.github/blob/main/Code-of-Conduct.md). 30 | - [ ] I have checked that this is not a security-related issue. For security concerns, please refer to our [Security Policy](https://github.com/reclaimprotocol/.github/blob/main/SECURITY.md). 31 | - [ ] I have read and signed the [Contributor License Agreement (CLA)](https://github.com/reclaimprotocol/.github/blob/main/CLA.md). 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATES/ci.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🛠 Continuous Integration / DevOps 3 | about: Improve or update workflows or other automation 4 | title: '[CI] ' 5 | labels: 'area/ci' 6 | assignees: '' 7 | --- 8 | 9 | ## Issue Description 10 | 11 | ### Current Situation 12 | 13 | 14 | ### Desired Outcome 15 | 16 | 17 | ### Proposed Implementation 18 | 19 | 20 | ### Additional Context 21 | 22 | 23 | --- 24 | ## Before You Begin 25 | 26 | Before submitting an issue, please ensure you have completed the following steps: 27 | 28 | - [ ] I have read and agree to follow the [Code of Conduct](https://github.com/reclaimprotocol/.github/blob/main/Code-of-Conduct.md). 29 | - [ ] I have checked that this is not a security-related issue. For security concerns, please refer to our [Security Policy](https://github.com/reclaimprotocol/.github/blob/main/SECURITY.md). 30 | - [ ] I have read and signed the [Contributor License Agreement (CLA)](https://github.com/reclaimprotocol/.github/blob/main/CLA.md). 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATES/documentation.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 📄 Documentation issue 3 | about: Issues related to documentation. 4 | title: '[Docs]' 5 | labels: 'area/docs, language/markdown' 6 | assignees: '' 7 | --- 8 | 9 | ### Current State 10 | 11 | 12 | ### Desired State 13 | 14 | 15 | --- 16 | ## Before You Begin 17 | 18 | Before submitting an issue, please ensure you have completed the following steps: 19 | 20 | - [ ] I have read and agree to follow the [Code of Conduct](https://github.com/reclaimprotocol/.github/blob/main/Code-of-Conduct.md). 21 | - [ ] I have checked that this is not a security-related issue. For security concerns, please refer to our [Security Policy](https://github.com/reclaimprotocol/.github/blob/main/SECURITY.md). 22 | - [ ] I have read and signed the [Contributor License Agreement (CLA)](https://github.com/reclaimprotocol/.github/blob/main/CLA.md). 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATES/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 💡 Feature Request 3 | about: Suggest an enhancement for this project 4 | title: '[FEATURE] ' 5 | labels: 'enhancement' 6 | assignees: '' 7 | --- 8 | 9 | ## Feature Description 10 | 11 | ### Current Situation 12 | 13 | 14 | ### Proposed Enhancement 15 | 16 | 17 | ### Benefits (Optional) 18 | 19 | 20 | ### Possible Implementation (Optional) 21 | 22 | 23 | 24 | ### Additional Context 25 | 26 | 27 | --- 28 | ## Before You Begin 29 | 30 | Before submitting an issue, please ensure you have completed the following steps: 31 | 32 | - [ ] I have read and agree to follow the [Code of Conduct](https://github.com/reclaimprotocol/.github/blob/main/Code-of-Conduct.md). 33 | - [ ] I have checked that this is not a security-related issue. For security concerns, please refer to our [Security Policy](https://github.com/reclaimprotocol/.github/blob/main/SECURITY.md). 34 | - [ ] I have read and signed the [Contributor License Agreement (CLA)](https://github.com/reclaimprotocol/.github/blob/main/CLA.md). 35 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | 4 | ### Testing (ignore for documentation update) 5 | 6 | 7 | ### Type of change 8 | - [ ] Bug fix 9 | - [ ] New feature 10 | - [ ] Breaking change 11 | - [ ] Documentation update 12 | 13 | ### Checklist: 14 | - [ ] I have read and agree to the [Code of Conduct](https://github.com/reclaimprotocol/.github/blob/main/Code-of-Conduct.md). 15 | - [ ] I have signed the [Contributor License Agreement (CLA)](https://github.com/reclaimprotocol/.github/blob/main/CLA.md). 16 | - [ ] I have considered the [Security Policy](https://github.com/reclaimprotocol/.github/blob/main/SECURITY.md). 17 | - [ ] I have self-reviewed and tested my code. 18 | - [ ] I have updated documentation as needed. 19 | - [ ] I agree to the [project's custom license](https://github.com/reclaimprotocol/.github/blob/main/LICENSE). 20 | 21 | ### Additional Notes: 22 | 23 | -------------------------------------------------------------------------------- /.github/workflows/run-test-build-deploy.yaml: -------------------------------------------------------------------------------- 1 | name: build-deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | workflow_dispatch: 8 | inputs: 9 | release_type: 10 | description: "Type of release" 11 | required: true 12 | default: "patch" 13 | type: choice 14 | options: 15 | - patch 16 | - minor 17 | - major 18 | - bugfix 19 | - hotfix 20 | 21 | permissions: 22 | contents: write 23 | packages: write 24 | 25 | jobs: 26 | test_and_build: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v3 30 | 31 | - name: Set up Node.js 32 | uses: actions/setup-node@v3 33 | with: 34 | node-version: "16" 35 | 36 | - name: Clean install dependencies 37 | run: | 38 | rm -rf node_modules 39 | rm -rf package-lock.json 40 | npm i 41 | 42 | - name: Build package 43 | run: npm run build 44 | 45 | - name: Upload build artifacts 46 | uses: actions/upload-artifact@v4 47 | with: 48 | name: dist 49 | path: dist/ 50 | 51 | publish: 52 | needs: test_and_build 53 | runs-on: ubuntu-latest 54 | if: github.event_name == 'workflow_dispatch' || github.event_name == 'push' 55 | steps: 56 | - uses: actions/checkout@v3 57 | with: 58 | persist-credentials: false # Disable automatic token authentication 59 | 60 | - name: Set up Node.js 61 | uses: actions/setup-node@v3 62 | with: 63 | node-version: "16" 64 | registry-url: "https://registry.npmjs.org" 65 | 66 | - name: Download build artifacts 67 | uses: actions/download-artifact@v4 68 | with: 69 | name: dist 70 | path: dist/ 71 | 72 | - name: Version and publish 73 | env: 74 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 75 | run: | 76 | npm publish 77 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .DS_Store 3 | dist 4 | coverage 5 | 6 | .env* 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | 7 | # Reclaim Protocol JavaScript SDK Integration Guide 8 | 9 | This guide will walk you through integrating the Reclaim Protocol JavaScript SDK into your application. We'll create a simple React application that demonstrates how to use the SDK to generate proofs and verify claims. 10 | 11 | ## Prerequisites 12 | 13 | Before we begin, make sure you have: 14 | 15 | 1. An application ID from Reclaim Protocol. 16 | 2. An application secret from Reclaim Protocol. 17 | 3. A provider ID for the specific service you want to verify. 18 | 19 | You can obtain these details from the [Reclaim Developer Portal](https://dev.reclaimprotocol.org/). 20 | 21 | ## Step 1: Create a new React application 22 | 23 | Let's start by creating a new React application: 24 | 25 | ```bash 26 | npx create-react-app reclaim-app 27 | cd reclaim-app 28 | ``` 29 | 30 | ## Step 2: Install necessary dependencies 31 | 32 | Install the Reclaim Protocol SDK and a QR code generator: 33 | 34 | ```bash 35 | npm install @reclaimprotocol/js-sdk react-qr-code 36 | ``` 37 | 38 | **Current SDK Version**: 4.4.0 39 | 40 | ## Step 3: Set up your React component 41 | 42 | Replace the contents of `src/App.js` with the following code: 43 | 44 | ```javascript 45 | import React, { useState, useEffect } from 'react' 46 | import { ReclaimProofRequest, verifyProof, ClaimCreationType } from '@reclaimprotocol/js-sdk' 47 | import QRCode from 'react-qr-code' 48 | 49 | function App() { 50 | const [reclaimProofRequest, setReclaimProofRequest] = useState(null) 51 | const [requestUrl, setRequestUrl] = useState('') 52 | const [statusUrl, setStatusUrl] = useState('') 53 | const [proofs, setProofs] = useState(null) 54 | 55 | useEffect(() => { 56 | async function initializeReclaim() { 57 | const APP_ID = 'YOUR_APPLICATION_ID_HERE' 58 | const APP_SECRET = 'YOUR_APPLICATION_SECRET_HERE' 59 | const PROVIDER_ID = 'YOUR_PROVIDER_ID_HERE' 60 | 61 | const proofRequest = await ReclaimProofRequest.init( 62 | APP_ID, 63 | APP_SECRET, 64 | PROVIDER_ID 65 | ) 66 | setReclaimProofRequest(proofRequest) 67 | } 68 | 69 | initializeReclaim() 70 | }, []) 71 | 72 | async function handleCreateClaim() { 73 | if (!reclaimProofRequest) { 74 | console.error('Reclaim Proof Request not initialized') 75 | return 76 | } 77 | 78 | const url = await reclaimProofRequest.getRequestUrl() 79 | setRequestUrl(url) 80 | 81 | const status = reclaimProofRequest.getStatusUrl() 82 | setStatusUrl(status) 83 | console.log('Status URL:', status) 84 | 85 | await reclaimProofRequest.startSession({ 86 | onSuccess: (proofs) => { 87 | if (proofs && typeof proofs === 'string') { 88 | // When using a custom callback url, the proof is returned to the callback url and we get a message instead of a proof 89 | console.log('SDK Message:', proofs) 90 | setProofs(proofs) 91 | } else if (proofs && typeof proofs !== 'string') { 92 | // When using the default callback url, we get a proof 93 | if (Array.isArray(proofs)) { 94 | // when using the cascading providers, providers having more than one proof will return an array of proofs 95 | console.log(JSON.stringify(proofs.map(p => p.claimData.context))) 96 | } else { 97 | console.log('Proof received:', proofs?.claimData.context) 98 | } 99 | setProofs(proofs) 100 | } 101 | }, 102 | onFailure: (error) => { 103 | console.error('Verification failed', error) 104 | } 105 | }) 106 | } 107 | 108 | return ( 109 |
110 |

Reclaim Protocol Demo

111 | 112 | {requestUrl && ( 113 |
114 |

Scan this QR code to start the verification process:

115 | 116 |
117 | )} 118 | {proofs && ( 119 |
120 |

Verification Successful!

121 |
{JSON.stringify(proofs, null, 2)}
122 |
123 | )} 124 |
125 | ) 126 | } 127 | 128 | export default App 129 | ``` 130 | 131 | ## Step 4: Understanding the code 132 | 133 | Let's break down what's happening in this code: 134 | 135 | 1. We initialize the Reclaim SDK with your application ID, secret, and provider ID. This happens once when the component mounts. 136 | 137 | 2. When the user clicks the "Create Claim" button, we: 138 | - Generate a request URL using `getRequestUrl()`. This URL is used to create the QR code. 139 | - Get the status URL using `getStatusUrl()`. This URL can be used to check the status of the claim process. 140 | - Start a session with `startSession()`, which sets up callbacks for successful and failed verifications. 141 | 142 | 3. We display a QR code using the request URL. When a user scans this code, it starts the verification process. 143 | 144 | 4. The status URL is logged to the console. You can use this URL to check the status of the claim process programmatically. 145 | 146 | 5. When the verification is successful, we display the proof data on the page. 147 | 148 | ## Step 5: New Streamlined Flow with Browser Extension Support 149 | 150 | The Reclaim SDK now provides a simplified `triggerReclaimFlow()` method that automatically handles the verification process across different platforms and devices. This method intelligently chooses the best verification method based on the user's environment. 151 | 152 | ### Using triggerReclaimFlow() 153 | 154 | Replace the `handleCreateClaim` function in your React component with this simpler approach: 155 | 156 | ```javascript 157 | async function handleCreateClaim() { 158 | if (!reclaimProofRequest) { 159 | console.error('Reclaim Proof Request not initialized') 160 | return 161 | } 162 | 163 | try { 164 | // Start the verification process automatically 165 | await reclaimProofRequest.triggerReclaimFlow() 166 | 167 | // Listen for the verification results 168 | await reclaimProofRequest.startSession({ 169 | onSuccess: (proofs) => { 170 | if (proofs && typeof proofs === 'string') { 171 | console.log('SDK Message:', proofs) 172 | setProofs(proofs) 173 | } else if (proofs && typeof proofs !== 'string') { 174 | if (Array.isArray(proofs)) { 175 | console.log(JSON.stringify(proofs.map(p => p.claimData.context))) 176 | } else { 177 | console.log('Proof received:', proofs?.claimData.context) 178 | } 179 | setProofs(proofs) 180 | } 181 | }, 182 | onFailure: (error) => { 183 | console.error('Verification failed', error) 184 | } 185 | }) 186 | } catch (error) { 187 | console.error('Error triggering Reclaim flow:', error) 188 | } 189 | } 190 | ``` 191 | 192 | ### How triggerReclaimFlow() Works 193 | 194 | The `triggerReclaimFlow()` method automatically detects the user's environment and chooses the optimal verification method: 195 | 196 | #### On Desktop Browsers: 197 | 1. **Browser Extension First**: If the Reclaim browser extension is installed, it will use the extension for a seamless in-browser verification experience. 198 | 2. **QR Code Fallback**: If the extension is not available, it automatically displays a QR code modal for mobile scanning. 199 | 200 | #### On Mobile Devices: 201 | 1. **iOS Devices**: Automatically redirects to the Reclaim App Clip for native iOS verification. 202 | 2. **Android Devices**: Automatically redirects to the Reclaim Instant App for native Android verification. 203 | 204 | ### Browser Extension Support 205 | 206 | The SDK now includes built-in support for the Reclaim browser extension, providing users with a seamless verification experience without leaving their current browser tab. 207 | 208 | #### Features: 209 | - **Automatic Detection**: The SDK automatically detects if the Reclaim browser extension is installed 210 | - **Seamless Integration**: No additional setup required - the extension integration works out of the box 211 | - **Fallback Support**: If the extension is not available, the SDK gracefully falls back to QR code or mobile app flows 212 | 213 | #### Manual Extension Detection: 214 | 215 | You can also manually check if the browser extension is available: 216 | 217 | ```javascript 218 | const isExtensionAvailable = await reclaimProofRequest.isBrowserExtensionAvailable() 219 | if (isExtensionAvailable) { 220 | console.log('Reclaim browser extension is installed') 221 | } else { 222 | console.log('Browser extension not available, will use alternative flow') 223 | } 224 | ``` 225 | 226 | #### Configuring Browser Extension Options: 227 | 228 | You can customize the browser extension behavior during SDK initialization: 229 | 230 | ```javascript 231 | const proofRequest = await ReclaimProofRequest.init( 232 | APP_ID, 233 | APP_SECRET, 234 | PROVIDER_ID, 235 | { 236 | useBrowserExtension: true, // Enable/disable browser extension (default: true) 237 | extensionID: 'custom-extension-id', // Use custom extension ID if needed 238 | // ... other options 239 | } 240 | ) 241 | ``` 242 | 243 | ### Modal Customization 244 | 245 | When the QR code modal is displayed (fallback on desktop), you can customize its appearance and behavior: 246 | 247 | ```javascript 248 | // Set modal options before triggering the flow 249 | reclaimProofRequest.setModalOptions({ 250 | title: 'Custom Verification Title', 251 | description: 'Scan this QR code with your mobile device to verify your account', 252 | darkTheme: true, // Enable dark theme (default: false) 253 | modalPopupTimer: 5, // Auto-close modal after 5 minutes (default: 1 minute) 254 | showExtensionInstallButton: true, // Show extension install button (default: false) 255 | extensionUrl: 'https://custom-extension-url.com', // Custom extension download URL 256 | onClose: () => { 257 | console.log('Modal was closed'); 258 | } // Callback when modal is closed 259 | }) 260 | 261 | await reclaimProofRequest.triggerReclaimFlow() 262 | ``` 263 | 264 | ### Benefits of the New Flow: 265 | 266 | 1. **Platform Adaptive**: Automatically chooses the best verification method for each platform 267 | 2. **User-Friendly**: Provides the most seamless experience possible for each user 268 | 3. **Simplified Integration**: Single method call handles all verification scenarios 269 | 4. **Extension Support**: Leverages browser extension for desktop users when available 270 | 5. **Mobile Optimized**: Native app experiences on mobile devices 271 | 272 | ## Step 6: Run your application 273 | 274 | Start your development server: 275 | 276 | ```bash 277 | npm start 278 | ``` 279 | 280 | Your Reclaim SDK demo should now be running. Click the "Create Claim" button to generate a QR code. Scan this code to start the verification process. 281 | 282 | ## Understanding the Claim Process 283 | 284 | 1. **Creating a Claim**: When you click "Create Claim", the SDK generates a unique request for verification. 285 | 286 | 2. **QR Code**: The QR code contains the request URL. When scanned, it initiates the verification process. 287 | 288 | 3. **Status URL**: This URL (logged to the console) can be used to check the status of the claim process. It's useful for tracking the progress of verification. 289 | 290 | 4. **Verification**: The `onSuccess` is called when verification is successful, providing the proof data. When using a custom callback url, the proof is returned to the callback url and we get a message instead of a proof. 291 | 292 | 5. **Handling Failures**: The `onFailure` is called if verification fails, allowing you to handle errors gracefully. 293 | 294 | ## Advanced Configuration 295 | 296 | The Reclaim SDK offers several advanced options to customize your integration: 297 | 298 | 1. **Adding Context**: 299 | You can add context to your proof request, which can be useful for providing additional information: 300 | ```javascript 301 | reclaimProofRequest.addContext('0x00000000000', 'Example context message') 302 | ``` 303 | 304 | 2. **Setting Parameters**: 305 | If your provider requires specific parameters, you can set them like this: 306 | ```javascript 307 | reclaimProofRequest.setParams({ email: "test@example.com", userName: "testUser" }) 308 | ``` 309 | 310 | 3. **Custom Redirect URL**: 311 | Set a custom URL to redirect users after the verification process: 312 | ```javascript 313 | reclaimProofRequest.setRedirectUrl('https://example.com/redirect') 314 | ``` 315 | 316 | 4. **Custom Callback URL**: 317 | Set a custom callback URL for your app which allows you to receive proofs and status updates on your callback URL: 318 | Pass in `jsonProofResponse: true` to receive the proof in JSON format: By default, the proof is returned as a url encoded string. 319 | ```javascript 320 | reclaimProofRequest.setAppCallbackUrl('https://example.com/callback', true) 321 | ``` 322 | 323 | 5. **Modal Customization for Desktop Users**: 324 | Customize the appearance and behavior of the QR code modal shown to desktop users: 325 | ```javascript 326 | reclaimProofRequest.setModalOptions({ 327 | title: 'Verify Your Account', 328 | description: 'Scan the QR code with your mobile device or install our browser extension', 329 | darkTheme: false, // Enable dark theme (default: false) 330 | extensionUrl: 'https://chrome.google.com/webstore/detail/reclaim' // Custom extension URL 331 | }) 332 | ``` 333 | 334 | 6. **Browser Extension Configuration**: 335 | Configure browser extension behavior and detection: 336 | ```javascript 337 | // Check if browser extension is available 338 | const isExtensionAvailable = await reclaimProofRequest.isBrowserExtensionAvailable() 339 | 340 | // Trigger the verification flow with automatic platform detection 341 | await reclaimProofRequest.triggerReclaimFlow() 342 | 343 | // Initialize with browser extension options 344 | const proofRequest = await ReclaimProofRequest.init( 345 | APP_ID, 346 | APP_SECRET, 347 | PROVIDER_ID, 348 | { 349 | useBrowserExtension: true, // Enable browser extension support (default: true) 350 | extensionID: 'custom-extension-id', // Custom extension identifier 351 | useAppClip: true, // Enable mobile app clips (default: true) 352 | log: true // Enable logging for debugging 353 | } 354 | ) 355 | ``` 356 | 357 | 7. **Custom Share Page and App Clip URLs**: 358 | You can customize the share page and app clip URLs for your app: 359 | 360 | ```javascript 361 | const proofRequest = await ReclaimProofRequest.init( 362 | APP_ID, 363 | APP_SECRET, 364 | PROVIDER_ID, 365 | { 366 | customSharePageUrl: 'https://your-custom-domain.com/verify', // Custom share page URL 367 | customAppClipUrl: 'https://appclip.apple.com/id?p=your.custom.app.clip', // Custom iOS App Clip URL 368 | // ... other options 369 | } 370 | ) 371 | ``` 372 | 373 | 374 | 8. **Platform-Specific Flow Control**: 375 | The `triggerReclaimFlow()` method provides intelligent platform detection, but you can still use traditional methods for custom flows: 376 | ```javascript 377 | // Traditional approach with manual QR code handling 378 | const requestUrl = await reclaimProofRequest.getRequestUrl() 379 | // Display your own QR code implementation 380 | 381 | // Or use the new streamlined approach 382 | await reclaimProofRequest.triggerReclaimFlow() 383 | // Automatically handles platform detection and optimal user experience 384 | ``` 385 | 386 | 9. **Exporting and Importing SDK Configuration**: 387 | You can export the entire Reclaim SDK configuration as a JSON string and use it to initialize the SDK with the same configuration on a different service or backend: 388 | ```javascript 389 | // On the client-side or initial service 390 | const configJson = reclaimProofRequest.toJsonString() 391 | console.log('Exportable config:', configJson) 392 | 393 | // Send this configJson to your backend or another service 394 | 395 | // On the backend or different service 396 | const importedRequest = ReclaimProofRequest.fromJsonString(configJson) 397 | const requestUrl = await importedRequest.getRequestUrl() 398 | ``` 399 | This allows you to generate request URLs and other details from your backend or a different service while maintaining the same configuration. 400 | 401 | 10. **Utility Methods**: 402 | Additional utility methods for managing your proof requests: 403 | ```javascript 404 | // Get the current session ID 405 | const sessionId = reclaimProofRequest.getSessionId() 406 | console.log('Current session ID:', sessionId) 407 | ``` 408 | 409 | ## Handling Proofs on Your Backend 410 | 411 | For production applications, it's recommended to handle proofs on your backend: 412 | 413 | 1. Set a callback URL: 414 | ```javascript 415 | reclaimProofRequest.setCallbackUrl('https://your-backend.com/receive-proofs') 416 | ``` 417 | 418 | These options allow you to securely process proofs and status updates on your server. 419 | 420 | ## Proof Verification 421 | 422 | The SDK provides a `verifyProof` function to manually verify proofs. This is useful when you need to validate proofs outside of the normal flow: 423 | 424 | ```javascript 425 | import { verifyProof } from '@reclaimprotocol/js-sdk' 426 | 427 | // Verify a single proof 428 | const isValid = await verifyProof(proof) 429 | if (isValid) { 430 | console.log('Proof is valid') 431 | } else { 432 | console.log('Proof is invalid') 433 | } 434 | 435 | // Verify multiple proofs 436 | const areValid = await verifyProof([proof1, proof2, proof3]) 437 | if (areValid) { 438 | console.log('All proofs are valid') 439 | } else { 440 | console.log('One or more proofs are invalid') 441 | } 442 | ``` 443 | 444 | The `verifyProof` function: 445 | - Accepts either a single proof or an array of proofs 446 | - Returns a boolean indicating if the proof(s) are valid 447 | - Verifies signatures, witness integrity, and claim data 448 | - Handles both standalone and blockchain-based proofs 449 | 450 | ## Error Handling 451 | 452 | The SDK provides specific error types for different failure scenarios. Here's how to handle them: 453 | 454 | ```javascript 455 | import { ReclaimProofRequest } from '@reclaimprotocol/js-sdk' 456 | 457 | try { 458 | const proofRequest = await ReclaimProofRequest.init( 459 | APP_ID, 460 | APP_SECRET, 461 | PROVIDER_ID 462 | ) 463 | 464 | await proofRequest.startSession({ 465 | onSuccess: (proof) => { 466 | console.log('Proof received:', proof) 467 | }, 468 | onError: (error) => { 469 | // Handle different error types 470 | if (error.name === 'ProofNotVerifiedError') { 471 | console.error('Proof verification failed') 472 | } else if (error.name === 'ProviderFailedError') { 473 | console.error('Provider failed to generate proof') 474 | } else if (error.name === 'SessionNotStartedError') { 475 | console.error('Session could not be started') 476 | } else { 477 | console.error('Unknown error:', error.message) 478 | } 479 | } 480 | }) 481 | } catch (error) { 482 | // Handle initialization errors 483 | if (error.name === 'InitError') { 484 | console.error('Failed to initialize SDK:', error.message) 485 | } else if (error.name === 'InvalidParamError') { 486 | console.error('Invalid parameters provided:', error.message) 487 | } 488 | } 489 | ``` 490 | 491 | **Common Error Types:** 492 | - `InitError`: SDK initialization failed 493 | - `InvalidParamError`: Invalid parameters provided 494 | - `SignatureNotFoundError`: Missing or invalid signature 495 | - `ProofNotVerifiedError`: Proof verification failed 496 | - `ProviderFailedError`: Provider failed to generate proof 497 | - `SessionNotStartedError`: Session could not be started 498 | - `ProofSubmissionFailedError`: Proof submission to callback failed 499 | 500 | ## Next Steps 501 | 502 | Explore the [Reclaim Protocol documentation](https://docs.reclaimprotocol.org/) for more advanced features and best practices for integrating the SDK into your production applications. 503 | 504 | Happy coding with Reclaim Protocol! 505 | 506 | ## Contributing to Our Project 507 | 508 | We welcome contributions to our project! If you find any issues or have suggestions for improvements, please open an issue or submit a pull request. 509 | 510 | ## Security Note 511 | 512 | Always keep your Application Secret secure. Never expose it in client-side code or public repositories. 513 | 514 | ## Code of Conduct 515 | 516 | Please read and follow our [Code of Conduct](https://github.com/reclaimprotocol/.github/blob/main/Code-of-Conduct.md) to ensure a positive and inclusive environment for all contributors. 517 | 518 | ## Security 519 | 520 | If you discover any security-related issues, please refer to our [Security Policy](https://github.com/reclaimprotocol/.github/blob/main/SECURITY.md) for information on how to responsibly disclose vulnerabilities. 521 | 522 | ## Contributor License Agreement 523 | 524 | Before contributing to this project, please read and sign our [Contributor License Agreement (CLA)](https://github.com/reclaimprotocol/.github/blob/main/CLA.md). 525 | 526 | ## Indie Hackers 527 | 528 | For Indie Hackers: [Check out our guidelines and potential grant opportunities](https://github.com/reclaimprotocol/.github/blob/main/Indie-Hackers.md) 529 | 530 | ## License 531 | 532 | This project is licensed under a [custom license](https://github.com/reclaimprotocol/.github/blob/main/LICENSE). By contributing to this project, you agree that your contributions will be licensed under its terms. 533 | 534 | Thank you for your contributions! -------------------------------------------------------------------------------- /commitlint.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | }; -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /example/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@reclaimprotocol/js-sdk": "file:..", 13 | "next": "^15.5.2", 14 | "react": "^18", 15 | "react-dom": "^18" 16 | }, 17 | "devDependencies": { 18 | "@types/node": "^20", 19 | "@types/react": "^18", 20 | "@types/react-dom": "^18", 21 | "autoprefixer": "^10.0.1", 22 | "postcss": "^8", 23 | "tailwindcss": "^3.3.0", 24 | "typescript": "^5" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /example/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /example/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/src/app/debug/README.md: -------------------------------------------------------------------------------- 1 | # Device Detection Debug Page 2 | 3 | This debug page helps developers troubleshoot device detection issues in the Reclaim SDK. 4 | 5 | ## Usage 6 | 7 | ### Access the Debug Page 8 | Navigate to `/debug` in your example app: 9 | ``` 10 | http://localhost:3000/debug 11 | ``` 12 | 13 | ### Features 14 | 15 | 1. **Real-time Detection Results** 16 | - Shows current device type (mobile/desktop) 17 | - Mobile OS detection (iOS/Android) 18 | - Live updates on window resize 19 | 20 | 2. **Detection Factors** 21 | - All scoring factors used in detection 22 | - Visual indicators for each factor 23 | - Screen dimensions and capabilities 24 | 25 | 3. **Environment Information** 26 | - User agent string 27 | - Platform and vendor details 28 | - Screen resolution and window size 29 | - Touch points and orientation 30 | 31 | 4. **Browser Capabilities** 32 | - Touch support 33 | - Geolocation, notifications, service workers 34 | - WebGL, WebRTC support 35 | - Device motion/orientation APIs 36 | 37 | 5. **Export Options** 38 | - Copy debug info as JSON 39 | - Download complete debug report 40 | - Share with issue reports 41 | 42 | ## Adding to Your App 43 | 44 | To add this debug page to your own app: 45 | 46 | 1. Copy the debug page component to your app 47 | 2. Import the device detection functions from the SDK: 48 | ```typescript 49 | import { 50 | getDeviceType, 51 | getMobileDeviceType, 52 | isMobileDevice, 53 | isDesktopDevice 54 | } from '@reclaimprotocol/js-sdk' 55 | ``` 56 | 57 | 3. Add a route to access the debug page (keep it hidden in production) 58 | 59 | ## Reporting Issues 60 | 61 | If you find a device that's incorrectly detected: 62 | 63 | 1. Visit the debug page on that device 64 | 2. Click "Download Report" 65 | 3. Include the JSON file in your GitHub issue 66 | 4. Describe the expected vs actual behavior 67 | 68 | ## Common Detection Patterns 69 | 70 | ### Desktop with Touchscreen 71 | - Touch Device: Yes 72 | - Has Mouse: Yes 73 | - Large Screen: Yes 74 | - Can Hover: Yes 75 | - **Result**: Desktop ✓ 76 | 77 | ### iPad Pro in Desktop Mode 78 | - User Agent: Contains "Macintosh" 79 | - Touch Device: Yes 80 | - Mobile APIs: Yes 81 | - **Result**: Mobile (iOS) ✓ 82 | 83 | ### Android Tablet 84 | - User Agent: Contains "Android" 85 | - Touch Device: Yes 86 | - Screen Size: Variable 87 | - **Result**: Mobile (Android) ✓ 88 | 89 | ### iPhone/Android Phone 90 | - Small Screen: Yes 91 | - Touch Device: Yes 92 | - Mobile User Agent: Yes 93 | - **Result**: Mobile ✓ 94 | 95 | ## Privacy Note 96 | 97 | The debug page only collects browser information locally. No data is sent to external servers unless you explicitly export and share it. -------------------------------------------------------------------------------- /example/src/app/debug/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import React, { useEffect, useState } from 'react' 3 | import { getDeviceType, getMobileDeviceType, isMobileDevice, isDesktopDevice } from '@reclaimprotocol/js-sdk' 4 | 5 | interface DebugInfo { 6 | detection: { 7 | deviceType: string 8 | mobileType?: string 9 | isMobile: boolean 10 | isDesktop: boolean 11 | } 12 | scoring: { 13 | touchDevice: boolean 14 | hasMouse: boolean 15 | screenSize: string 16 | isSmallScreen: boolean 17 | isLargeScreen: boolean 18 | userAgent: string 19 | hasMobileUA: boolean 20 | hasMobileAPIs: boolean 21 | devicePixelRatio: number 22 | hasViewportMeta: boolean 23 | canHover: boolean 24 | pointerType: string 25 | } 26 | environment: { 27 | userAgent: string 28 | platform: string 29 | vendor: string 30 | language: string 31 | screenResolution: string 32 | windowSize: string 33 | orientation?: string 34 | touchPoints: number 35 | } 36 | capabilities: { 37 | touch: boolean 38 | geolocation: boolean 39 | notifications: boolean 40 | serviceWorker: boolean 41 | webGL: boolean 42 | webRTC: boolean 43 | deviceMotion: boolean 44 | deviceOrientation: boolean 45 | } 46 | recommendations: string[] 47 | } 48 | 49 | export default function DebugPage() { 50 | const [debugInfo, setDebugInfo] = useState(null) 51 | const [copied, setCopied] = useState(false) 52 | 53 | useEffect(() => { 54 | if (typeof window === 'undefined') return 55 | 56 | const collectDebugInfo = (): DebugInfo => { 57 | // Detection results 58 | const deviceType = getDeviceType() 59 | const isMobile = isMobileDevice() 60 | const isDesktop = isDesktopDevice() 61 | const mobileType = isMobile ? getMobileDeviceType() : undefined 62 | 63 | // Screen info 64 | const screenWidth = window.innerWidth || window.screen?.width || 0 65 | const screenHeight = window.innerHeight || window.screen?.height || 0 66 | const isSmallScreen = screenWidth <= 768 || screenHeight <= 768 67 | const isLargeScreen = screenWidth > 1024 && screenHeight > 768 68 | 69 | // Touch detection 70 | const hasTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 0 71 | 72 | // Pointer detection 73 | const getPointerType = () => { 74 | if (!window.matchMedia) return 'unknown' 75 | if (window.matchMedia('(pointer: fine)').matches) return 'fine (mouse/trackpad)' 76 | if (window.matchMedia('(pointer: coarse)').matches) return 'coarse (touch)' 77 | return 'none' 78 | } 79 | 80 | // User agent check 81 | const userAgent = navigator.userAgent 82 | const hasMobileUA = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini|mobile/i.test(userAgent.toLowerCase()) 83 | 84 | // Mobile APIs 85 | const hasMobileAPIs = 'orientation' in window || 86 | 'DeviceMotionEvent' in window || 87 | 'DeviceOrientationEvent' in window 88 | 89 | // Viewport meta 90 | const hasViewportMeta = document.querySelector('meta[name="viewport"]') !== null 91 | 92 | // Hover capability 93 | const canHover = window.matchMedia?.('(hover: hover)')?.matches || false 94 | const hasMouse = window.matchMedia?.('(pointer: fine)')?.matches || false 95 | 96 | // Capabilities check 97 | const checkWebGL = (): boolean => { 98 | try { 99 | const canvas = document.createElement('canvas') 100 | return !!(window.WebGLRenderingContext && 101 | (canvas.getContext('webgl') || canvas.getContext('experimental-webgl'))) 102 | } catch { 103 | return false 104 | } 105 | } 106 | 107 | // Generate recommendations 108 | const recommendations: string[] = [] 109 | 110 | if (deviceType === 'desktop' && hasTouch && hasMouse) { 111 | recommendations.push('Desktop with touchscreen detected - device correctly identified') 112 | } 113 | 114 | if (deviceType === 'mobile' && isLargeScreen) { 115 | recommendations.push('Large screen mobile device (tablet) detected') 116 | } 117 | 118 | if (deviceType === 'desktop' && isSmallScreen) { 119 | recommendations.push('Desktop with small window - resize detection working correctly') 120 | } 121 | 122 | if (userAgent.includes('iPad') && deviceType === 'mobile') { 123 | recommendations.push('iPad correctly detected as mobile device') 124 | } 125 | 126 | if (userAgent.includes('Macintosh') && hasTouch) { 127 | recommendations.push('iPad Pro in desktop mode detected via touch + Mac UA') 128 | } 129 | 130 | if (!window.matchMedia) { 131 | recommendations.push('⚠️ matchMedia not supported - using fallback detection') 132 | } 133 | 134 | if (hasMobileAPIs && deviceType === 'desktop') { 135 | recommendations.push('Desktop browser with mobile APIs - correctly identified as desktop') 136 | } 137 | 138 | return { 139 | detection: { 140 | deviceType, 141 | mobileType, 142 | isMobile, 143 | isDesktop 144 | }, 145 | scoring: { 146 | touchDevice: hasTouch, 147 | hasMouse, 148 | screenSize: `${screenWidth} × ${screenHeight}`, 149 | isSmallScreen, 150 | isLargeScreen, 151 | userAgent: navigator.userAgent, 152 | hasMobileUA, 153 | hasMobileAPIs, 154 | devicePixelRatio: window.devicePixelRatio || 1, 155 | hasViewportMeta, 156 | canHover, 157 | pointerType: getPointerType() 158 | }, 159 | environment: { 160 | userAgent: navigator.userAgent, 161 | platform: navigator.platform, 162 | vendor: navigator.vendor, 163 | language: navigator.language, 164 | screenResolution: `${window.screen?.width || 0} × ${window.screen?.height || 0}`, 165 | windowSize: `${window.innerWidth} × ${window.innerHeight}`, 166 | orientation: (window.screen as any)?.orientation?.type, 167 | touchPoints: navigator.maxTouchPoints || 0 168 | }, 169 | capabilities: { 170 | touch: hasTouch, 171 | geolocation: 'geolocation' in navigator, 172 | notifications: 'Notification' in window, 173 | serviceWorker: 'serviceWorker' in navigator, 174 | webGL: checkWebGL(), 175 | webRTC: 'RTCPeerConnection' in window, 176 | deviceMotion: 'DeviceMotionEvent' in window, 177 | deviceOrientation: 'DeviceOrientationEvent' in window 178 | }, 179 | recommendations 180 | } 181 | } 182 | 183 | setDebugInfo(collectDebugInfo()) 184 | 185 | // Update on resize 186 | const handleResize = () => { 187 | setDebugInfo(collectDebugInfo()) 188 | } 189 | window.addEventListener('resize', handleResize) 190 | 191 | return () => { 192 | window.removeEventListener('resize', handleResize) 193 | } 194 | }, []) 195 | 196 | const copyDebugInfo = () => { 197 | if (!debugInfo) return 198 | 199 | const debugText = JSON.stringify(debugInfo, null, 2) 200 | navigator.clipboard.writeText(debugText) 201 | setCopied(true) 202 | setTimeout(() => setCopied(false), 2000) 203 | } 204 | 205 | const downloadDebugReport = () => { 206 | if (!debugInfo) return 207 | 208 | const report = { 209 | timestamp: new Date().toISOString(), 210 | url: window.location.href, 211 | ...debugInfo 212 | } 213 | 214 | const blob = new Blob([JSON.stringify(report, null, 2)], { type: 'application/json' }) 215 | const url = URL.createObjectURL(blob) 216 | const a = document.createElement('a') 217 | a.href = url 218 | a.download = `reclaim-device-debug-${Date.now()}.json` 219 | document.body.appendChild(a) 220 | a.click() 221 | document.body.removeChild(a) 222 | URL.revokeObjectURL(url) 223 | } 224 | 225 | if (!debugInfo) { 226 | return ( 227 |
228 |
229 |
230 | ) 231 | } 232 | 233 | const getStatusColor = (value: boolean | string) => { 234 | if (typeof value === 'boolean') { 235 | return value ? 'text-green-600' : 'text-gray-500' 236 | } 237 | return 'text-gray-900' 238 | } 239 | 240 | const getDeviceIcon = () => { 241 | if (debugInfo.detection.deviceType === 'mobile') { 242 | return ( 243 | 244 | 246 | 247 | ) 248 | } 249 | return ( 250 | 251 | 253 | 254 | ) 255 | } 256 | 257 | return ( 258 |
259 |
260 | {/* Header */} 261 |
262 |
263 |
264 | {getDeviceIcon()} 265 |
266 |

Device Detection Debug

267 |

Reclaim SDK Device Detection Analysis

268 |
269 |
270 |
271 | 277 | 283 |
284 |
285 | 286 | {/* Detection Result */} 287 |
288 |
289 |

Device Type

290 |

{debugInfo.detection.deviceType}

291 |
292 | {debugInfo.detection.mobileType && ( 293 |
294 |

Mobile Type

295 |

{debugInfo.detection.mobileType}

296 |
297 | )} 298 |
299 |

Is Mobile

300 |

{debugInfo.detection.isMobile ? 'Yes' : 'No'}

301 |
302 |
303 |

Is Desktop

304 |

{debugInfo.detection.isDesktop ? 'Yes' : 'No'}

305 |
306 |
307 |
308 | 309 | {/* Recommendations */} 310 | {debugInfo.recommendations.length > 0 && ( 311 |
312 |

Detection Insights

313 |
    314 | {debugInfo.recommendations.map((rec, idx) => ( 315 |
  • 316 | 317 | {rec} 318 |
  • 319 | ))} 320 |
321 |
322 | )} 323 | 324 |
325 | {/* Scoring Factors */} 326 |
327 |

Detection Factors

328 |
329 |
330 | Touch Device 331 | 332 | {debugInfo.scoring.touchDevice ? 'Yes' : 'No'} 333 | 334 |
335 |
336 | Has Mouse 337 | 338 | {debugInfo.scoring.hasMouse ? 'Yes' : 'No'} 339 | 340 |
341 |
342 | Screen Size 343 | {debugInfo.scoring.screenSize} 344 |
345 |
346 | Small Screen (≤768px) 347 | 348 | {debugInfo.scoring.isSmallScreen ? 'Yes' : 'No'} 349 | 350 |
351 |
352 | Large Screen (>1024px) 353 | 354 | {debugInfo.scoring.isLargeScreen ? 'Yes' : 'No'} 355 | 356 |
357 |
358 | Mobile User Agent 359 | 360 | {debugInfo.scoring.hasMobileUA ? 'Yes' : 'No'} 361 | 362 |
363 |
364 | Mobile APIs 365 | 366 | {debugInfo.scoring.hasMobileAPIs ? 'Yes' : 'No'} 367 | 368 |
369 |
370 | Device Pixel Ratio 371 | {debugInfo.scoring.devicePixelRatio}x 372 |
373 |
374 | Viewport Meta Tag 375 | 376 | {debugInfo.scoring.hasViewportMeta ? 'Yes' : 'No'} 377 | 378 |
379 |
380 | Can Hover 381 | 382 | {debugInfo.scoring.canHover ? 'Yes' : 'No'} 383 | 384 |
385 |
386 | Pointer Type 387 | {debugInfo.scoring.pointerType} 388 |
389 |
390 |
391 | 392 | {/* Environment Info */} 393 |
394 |

Environment

395 |
396 |
397 |

User Agent

398 |

{debugInfo.environment.userAgent}

399 |
400 |
401 | Platform 402 | {debugInfo.environment.platform} 403 |
404 |
405 | Vendor 406 | {debugInfo.environment.vendor} 407 |
408 |
409 | Language 410 | {debugInfo.environment.language} 411 |
412 |
413 | Screen Resolution 414 | {debugInfo.environment.screenResolution} 415 |
416 |
417 | Window Size 418 | {debugInfo.environment.windowSize} 419 |
420 |
421 | Touch Points 422 | {debugInfo.environment.touchPoints} 423 |
424 | {debugInfo.environment.orientation && ( 425 |
426 | Orientation 427 | {debugInfo.environment.orientation} 428 |
429 | )} 430 |
431 |
432 |
433 | 434 | {/* Capabilities */} 435 |
436 |

Browser Capabilities

437 |
438 | {Object.entries(debugInfo.capabilities).map(([key, value]) => ( 439 |
440 |
441 | 442 | {key.replace(/([A-Z])/g, ' $1').trim()} 443 | 444 |
445 | ))} 446 |
447 |
448 | 449 | {/* Raw JSON Preview */} 450 |
451 | 452 | View Raw JSON Data 453 | 454 |
455 |             {JSON.stringify(debugInfo, null, 2)}
456 |           
457 |
458 | 459 | {/* Footer */} 460 |
461 |

Reclaim SDK Device Detection Debug Tool

462 |

Report any detection issues at: GitHub Issues

463 |
464 |
465 |
466 | ) 467 | } -------------------------------------------------------------------------------- /example/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reclaimprotocol/reclaim-js-sdk/2d0c22cacf35c17d3d38724c6ba930fd26a9935d/example/src/app/favicon.ico -------------------------------------------------------------------------------- /example/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --foreground-rgb: 0, 0, 0; 7 | --background-start-rgb: 214, 219, 220; 8 | --background-end-rgb: 255, 255, 255; 9 | } 10 | 11 | /* @media (prefers-color-scheme: dark) { 12 | :root { 13 | --foreground-rgb: 255, 255, 255; 14 | --background-start-rgb: 0, 0, 0; 15 | --background-end-rgb: 0, 0, 0; 16 | } 17 | } */ 18 | 19 | body { 20 | color: rgb(var(--foreground-rgb)); 21 | /* background: linear-gradient( 22 | to bottom, 23 | transparent, 24 | rgb(var(--background-end-rgb)) 25 | ) 26 | rgb(var(--background-start-rgb)); */ 27 | } 28 | 29 | @layer utilities { 30 | .text-balance { 31 | text-wrap: balance; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /example/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | 5 | const inter = Inter({ subsets: ["latin"] }); 6 | 7 | export const metadata: Metadata = { 8 | title: "Create Next App", 9 | description: "Generated by create next app", 10 | }; 11 | 12 | export default function RootLayout({ 13 | children, 14 | }: Readonly<{ 15 | children: React.ReactNode; 16 | }>) { 17 | return ( 18 | 19 | {children} 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /example/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import React, { useEffect, useState } from 'react' 3 | import { ReclaimProofRequest } from '@reclaimprotocol/js-sdk' 4 | import { Proof } from '@reclaimprotocol/js-sdk' 5 | 6 | export default function Home() { 7 | const [isLoading, setIsLoading] = useState(false) 8 | const [proofData, setProofData] = useState(null) 9 | const [error, setError] = useState(null) 10 | const [reclaimProofRequest, setReclaimProofRequest] = useState(null) 11 | 12 | useEffect(() => { 13 | // Initialize the ReclaimProofRequest when the component mounts 14 | initializeReclaimProofRequest() 15 | // verifyProofData() 16 | }, []) 17 | 18 | async function initializeReclaimProofRequest() { 19 | try { 20 | // ReclaimProofRequest Fields: 21 | // - applicationId: Unique identifier for your application 22 | // - providerId: Identifier for the specific provider you're using 23 | // - sessionId: Unique identifier for the current proof request session 24 | // - context: Additional context information for the proof request 25 | // - requestedProof: Details of the proof being requested 26 | // - signature: Cryptographic signature for request authentication 27 | // - redirectUrl: URL to redirect after proof generation (optional) 28 | // - appCallbackUrl: URL for receiving proof generation updates (optional) 29 | // - timeStamp: Timestamp of the proof request 30 | // - options: Additional configuration options 31 | 32 | 33 | const proofRequest = await ReclaimProofRequest.init( 34 | process.env.NEXT_PUBLIC_RECLAIM_APP_ID!, 35 | process.env.NEXT_PUBLIC_RECLAIM_APP_SECRET!, 36 | process.env.NEXT_PUBLIC_RECLAIM_PROVIDER_ID!, 37 | // Uncomment the following line to enable logging and AI providers 38 | { 39 | log: true, 40 | } 41 | ) 42 | setReclaimProofRequest(proofRequest) 43 | 44 | // // // Add context to the proof request (optional) 45 | // proofRequest.addContext("0x48796C654F7574707574", "test") 46 | 47 | // Set parameters for the proof request (if needed) 48 | // proofRequest.setParams({ email: "test@example.com", userName: "testUser" }) 49 | 50 | // Set a redirect URL (if needed) 51 | // proofRequest.setRedirectUrl('https://example.com/redirect') 52 | 53 | // Set a custom app callback URL (if needed) 54 | // proofRequest.setAppCallbackUrl('https://webhook.site/fd6cf442-0ea7-4427-8cb8-cb4dbe8884d2') 55 | 56 | // Uncomment the following line to log the proof request and to get the Json String 57 | // console.log('Proof request initialized:', proofRequest.toJsonString()) 58 | } catch (error) { 59 | console.error('Error initializing ReclaimProofRequest:', error) 60 | setError('Failed to initialize Reclaim. Please try again.') 61 | } 62 | } 63 | 64 | async function startClaimProcess() { 65 | if (!reclaimProofRequest) { 66 | setError('Reclaim not initialized. Please refresh the page.') 67 | return 68 | } 69 | 70 | setIsLoading(true) 71 | setError(null) 72 | 73 | try { 74 | // Start the verification session 75 | await reclaimProofRequest.triggerReclaimFlow() 76 | 77 | await reclaimProofRequest.startSession({ 78 | onSuccess: async (proof: Proof | Proof[] | string | undefined) => { 79 | setIsLoading(false) 80 | 81 | if (proof && typeof proof === 'string') { 82 | console.log('SDK Message:', proof) 83 | setError('Received string response instead of proof object.') 84 | } else if (proof && typeof proof !== 'string') { 85 | console.log('Proof received:', proof) 86 | if (Array.isArray(proof)) { 87 | setProofData(proof) 88 | } else { 89 | setProofData([proof]) 90 | } 91 | } 92 | }, 93 | onError: (error: Error) => { 94 | console.error('Error in proof generation:', error) 95 | setIsLoading(false) 96 | setError(`Error: ${error.message}`) 97 | } 98 | }) 99 | } catch (error) { 100 | console.error('Error starting verification session:', error) 101 | setIsLoading(false) 102 | setError('Failed to start verification. Please try again.') 103 | } 104 | } 105 | 106 | // Function to extract provider URL from parameters 107 | const getProviderUrl = (proof: Proof) => { 108 | try { 109 | const parameters = JSON.parse(proof.claimData.parameters); 110 | return parameters.url || "Unknown Provider"; 111 | } catch (e) { 112 | return proof.claimData.provider || "Unknown Provider"; 113 | } 114 | } 115 | 116 | // Function to beautify and display extracted parameters 117 | const renderExtractedParameters = (proof: Proof) => { 118 | try { 119 | const context = JSON.parse(proof.claimData.context) 120 | const extractedParams = context.extractedParameters || {} 121 | 122 | return ( 123 | <> 124 |

Extracted Parameters

125 | {Object.entries(extractedParams).length > 0 ? ( 126 |
127 | {Object.entries(extractedParams).map(([key, value]) => ( 128 |
129 |
130 | {key}: 131 | {String(value)} 132 |
133 |
134 | ))} 135 |
136 | ) : ( 137 |

No parameters extracted

138 | )} 139 | 140 | ) 141 | } catch (e) { 142 | return

Failed to parse parameters

143 | } 144 | } 145 | 146 | return ( 147 |
148 |
149 |

Reclaim SDK

150 | 151 | {!proofData && !isLoading && ( 152 |
153 |

154 | Click the button below to start the claim process. 155 |

156 | 163 |
164 | )} 165 | 166 | {isLoading && ( 167 |
168 |
169 |

Processing your claim...

170 |
171 | )} 172 | 173 | {error && ( 174 |
175 | {error} 176 |
177 | )} 178 | 179 | {proofData && proofData.length > 0 && ( 180 |
181 |

Verification Successful

182 | 183 | {proofData.map((proof, index) => ( 184 |
185 |
186 |

Proof #{index + 1}

187 | Verified 188 |
189 | 190 |
191 |
192 |

Provider

193 |

{getProviderUrl(proof)}

194 |
195 |
196 |

Timestamp

197 |

{new Date(proof.claimData.timestampS * 1000).toLocaleString()}

198 |
199 |
200 | 201 | {/* Extracted parameters section */} 202 |
203 | {renderExtractedParameters(proof)} 204 |
205 | 206 | {/* Witnesses section */} 207 | {proof.witnesses && proof.witnesses.length > 0 && ( 208 |
209 |

Attested by

210 |
211 | {proof.witnesses.map((witness, widx) => ( 212 |
213 |

{witness.id}

214 |
215 | ))} 216 |
217 |
218 | )} 219 | 220 | {/* Signatures section */} 221 | {proof.signatures && proof.signatures.length > 0 && ( 222 |
223 |

Signatures

224 |
225 | {proof.signatures.map((signature, sidx) => ( 226 |
227 |

{signature}

228 |
229 | ))} 230 |
231 |
232 | )} 233 | 234 | {/* Identifier (full) */} 235 |
236 |

Proof Identifier

237 |

238 | {proof.identifier} 239 |

240 |
241 |
242 | ))} 243 | 244 | 250 |
251 | )} 252 | 253 | {/* Optional: Add debug link for device detection testing */} 254 | {/* */} 259 |
260 |
261 | ) 262 | } 263 | -------------------------------------------------------------------------------- /example/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | content: [ 5 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}", 8 | ], 9 | theme: { 10 | extend: { 11 | backgroundImage: { 12 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", 13 | "gradient-conic": 14 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", 15 | }, 16 | }, 17 | }, 18 | plugins: [], 19 | }; 20 | export default config; 21 | -------------------------------------------------------------------------------- /example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "dom", 5 | "dom.iterable", 6 | "esnext" 7 | ], 8 | "allowJs": true, 9 | "skipLibCheck": true, 10 | "strict": true, 11 | "noEmit": true, 12 | "esModuleInterop": true, 13 | "module": "esnext", 14 | "moduleResolution": "bundler", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "jsx": "preserve", 18 | "incremental": true, 19 | "plugins": [ 20 | { 21 | "name": "next" 22 | } 23 | ], 24 | "paths": { 25 | "@/*": [ 26 | "./src/*" 27 | ] 28 | }, 29 | "target": "ES2017" 30 | }, 31 | "include": [ 32 | "next-env.d.ts", 33 | "**/*.ts", 34 | "**/*.tsx", 35 | ".next/types/**/*.ts" 36 | ], 37 | "exclude": [ 38 | "node_modules" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'jsdom', 4 | roots: ['/src'], 5 | testMatch: ['**/__tests__/**/*.test.ts'], 6 | transform: { 7 | '^.+\\.ts$': ['ts-jest', { 8 | tsconfig: { 9 | esModuleInterop: true, 10 | allowSyntheticDefaultImports: true, 11 | }, 12 | }], 13 | }, 14 | moduleFileExtensions: ['ts', 'js', 'json'], 15 | collectCoverageFrom: [ 16 | 'src/**/*.ts', 17 | '!src/**/*.d.ts', 18 | '!src/**/__tests__/**', 19 | '!src/index.ts', 20 | ], 21 | coverageDirectory: 'coverage', 22 | coverageReporters: ['text', 'lcov', 'html'], 23 | testTimeout: 10000, 24 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@reclaimprotocol/js-sdk", 3 | "version": "4.5.1", 4 | "description": "Designed to request proofs from the Reclaim protocol and manage the flow of claims and witness interactions.", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "keywords": [ 8 | "reclaim", 9 | "protocol", 10 | "blockchain", 11 | "proof", 12 | "verification", 13 | "identity", 14 | "claims", 15 | "witness", 16 | "sdk", 17 | "javascript", 18 | "typescript", 19 | "decentralized", 20 | "web3" 21 | ], 22 | "files": [ 23 | "dist" 24 | ], 25 | "tsup": { 26 | "entry": [ 27 | "src/index.ts" 28 | ], 29 | "splitting": false, 30 | "sourcemap": true, 31 | "clean": true 32 | }, 33 | "scripts": { 34 | "build": "sh scripts/build.sh", 35 | "release": "release-it", 36 | "test": "jest", 37 | "test:watch": "jest --watch", 38 | "test:coverage": "jest --coverage", 39 | "commitlint": "commitlint --edit" 40 | }, 41 | "repository": { 42 | "type": "git", 43 | "url": "https://github.com/reclaimprotocol/reclaim-js-sdk" 44 | }, 45 | "author": "ali ", 46 | "license": "See License in ", 47 | "bugs": { 48 | "url": "https://github.com/reclaimprotocol/reclaim-js-sdk/issues" 49 | }, 50 | "homepage": "https://github.com/reclaimprotocol/reclaim-js-sdk/", 51 | "publishConfig": { 52 | "registry": "https://registry.npmjs.org/", 53 | "access": "public" 54 | }, 55 | "release-it": { 56 | "git": { 57 | "commitMessage": "chore: release ${version}", 58 | "tagName": "v${version}" 59 | }, 60 | "npm": { 61 | "publish": true, 62 | "tag": "latest" 63 | }, 64 | "github": { 65 | "release": true 66 | }, 67 | "plugins": { 68 | "@release-it/conventional-changelog": { 69 | "preset": "angular" 70 | } 71 | } 72 | }, 73 | "devDependencies": { 74 | "@commitlint/cli": "^17.7.1", 75 | "@commitlint/config-conventional": "^17.7.0", 76 | "@types/jest": "^30.0.0", 77 | "@types/qs": "^6.9.11", 78 | "@types/url-parse": "^1.4.11", 79 | "@types/uuid": "^9.0.7", 80 | "jest": "^30.1.3", 81 | "jest-environment-jsdom": "^30.1.2", 82 | "ts-jest": "^29.4.1", 83 | "tsup": "^8.0.1", 84 | "typescript": "^5.3.3" 85 | }, 86 | "dependencies": { 87 | "@release-it/conventional-changelog": "^10.0.1", 88 | "canonicalize": "^2.0.0", 89 | "ethers": "^6.9.1", 90 | "qs": "^6.11.2", 91 | "release-it": "^19.0.4", 92 | "url-parse": "^1.5.10", 93 | "uuid": "^9.0.1" 94 | }, 95 | "overrides": { 96 | "@conventional-changelog/git-client": "^2.0.0" 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /readme/depemdency-diagram.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
Reclaim SDK
Reclaim SDK
uuid
uuid
canonicalize
canonicalize
ethers
ethers
Text is not SVG - cannot display
-------------------------------------------------------------------------------- /readme/usage-flow-2.svg: -------------------------------------------------------------------------------- 1 | participant%20AppClip%2FInstanApp%0Aparticipant%20User%0Aparticipant%20SDK%20Sample%20Web%20App%0Aparticipant%20Reclaim%20SDK%0A%0AUser-%3ESDK%20Sample%20Web%20App%3A%20ask%20to%20provide%20proof%0A%0ASDK%20Sample%20Web%20App-%3EReclaim%20SDK%3A%20requestProof(request%2C%20AppCallbackUrl)%0Aactivate%20Reclaim%20SDK%0AReclaim%20SDK-%3EReclaim%20SDK%3A%20Validate%20request%0AReclaim%20SDK-%3EReclaim%20SDK%3A%20Generate%20Template%20Instance%0AReclaim%20SDK--%3ESDK%20Sample%20Web%20App%3A%20TemplateInstance%0Adeactivate%20Reclaim%20SDK%0ASDK%20Sample%20Web%20App-%3ESDK%20Sample%20Web%20App%3A%20Generate%20QrCode%20from%20template%0A%0AUser--%3ESDK%20Sample%20Web%20App%3A%20Scan%20QrCode%0A%0ASDK%20Sample%20Web%20App--%3EAppClip%2FInstanApp%3A%20Open%20AppClip%2FInstanApp%0A%0AAppClip%2FInstanApp-%3EAppClip%2FInstanApp%3A%20complete%20verification%20task%0A%0AAppClip%2FInstanApp-%3ESDK%20Sample%20Web%20App%3A%20Submit%20Proof%20using%20AppCallbackUrl%0A%0ASDK%20Sample%20Web%20App--%3EUser%3A%20notify%20user%20(proof%20received)%0AAppClip/InstanAppUserSDK Sample Web AppReclaim SDKask to provide proofrequestProof(request, AppCallbackUrl)Validate requestGenerate Template InstanceTemplateInstanceGenerate QrCode from templateScan QrCodeOpen AppClip/InstanAppcomplete verification taskSubmit Proof using AppCallbackUrlnotify user (proof received) -------------------------------------------------------------------------------- /readme/usage-flow-3.svg: -------------------------------------------------------------------------------- 1 | participant%20AppClip%2FInstanApp%0Aparticipant%20User%0Aparticipant%20SDK%20Sample%20Web%20App%0Aparticipant%20Reclaim%20SDK%0A%0AUser-%3ESDK%20Sample%20Web%20App%3A%20ask%20to%20provide%20proof%0A%0ASDK%20Sample%20Web%20App-%3EReclaim%20SDK%3A%20requestProof(request%2C%20AppCallbackUrl)%0Aactivate%20Reclaim%20SDK%0AReclaim%20SDK-%3EReclaim%20SDK%3A%20Generate%20Template%20Instance%0AReclaim%20SDK--%3ESDK%20Sample%20Web%20App%3A%20TemplateInstance%0Adeactivate%20Reclaim%20SDK%0A%0ASDK%20Sample%20Web%20App-%3ESDK%20Sample%20Web%20App%3A%20Generate%20QrCode%20from%20template%0A%0AUser--%3ESDK%20Sample%20Web%20App%3A%20Scan%20QrCode%0A%0ASDK%20Sample%20Web%20App--%3EAppClip%2FInstanApp%3A%20Open%20AppClip%2FInstanApp%0A%0AAppClip%2FInstanApp-%3EAppClip%2FInstanApp%3A%20complete%20verification%20task%0A%0AAppClip%2FInstanApp-%3ESDK%20Sample%20Web%20App%3A%20Submit%20Proof%20using%20AppCallbackUrl%0A%0ASDK%20Sample%20Web%20App--%3EUser%3A%20notify%20user%20(proof%20received)%0AAppClip/InstanAppUserSDK Sample Web AppReclaim SDKask to provide proofrequestProof(request, AppCallbackUrl)Generate Template InstanceTemplateInstanceGenerate QrCode from templateScan QrCodeOpen AppClip/InstanAppcomplete verification taskSubmit Proof using AppCallbackUrlnotify user (proof received) -------------------------------------------------------------------------------- /readme/usage-flow.svg: -------------------------------------------------------------------------------- 1 | %0A%0Aparticipant%20AppClip%2FInstanApp%0Aparticipant%20User%0Aparticipant%Reclaim%0Aparticipant%20Reclaim%20SDK%0A%0AUser-%3EApp%3A%20ask%20to%20provide%20proof%0A%0AApp-%3EReclaim%20SDK%3A%20requestProof(request%2C%20AppCallbackUrl)%0Aactivate%20Reclaim%20SDK%0AReclaim%20SDK-%3EReclaim%20SDK%3A%20Validate%20request%0AReclaim%20SDK-%3EReclaim%20SDK%3A%20Generate%20Template%20Instance%0AReclaim%20SDK--%3EApp%3A%20TemplateInstance%0Adeactivate%20Reclaim%20SDK%0AApp-%3EApp%3A%20Generate%20QrCode%20from%20template%0A%0A%0AUser--%3EApp%3A%20Scan%20QrCode%0A%0AApp--%3EAppClip%2FInstanApp%3A%20Open%20AppClip%2FInstanApp%0A%0AAppClip%2FInstanApp-%3EAppClip%2FInstanApp%3A%20complete%20verification%20task%0A%0A%0AAppClip%2FInstanApp-%3EApp%3A%20Submit%20Proof%20using%20AppCallbackUrl%0A%0A%0AApp--%3EUser%3A%20notify%20user%20(proof%20received)%0AAppClip/InstanAppUserAppReclaim SDKask to provide proofrequestProof(request, AppCallbackUrl)Validate requestGenerate Template InstanceTemplateInstanceGenerate QrCode from templateScan QrCodeOpen AppClip/InstanAppcomplete verification taskSubmit Proof using AppCallbackUrlnotify user (proof received) -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Delete existing dist folder 4 | echo "Deleting existing dist folder" 5 | rm -rf dist 6 | 7 | # Build 8 | echo "Building the project" 9 | tsup --dts 10 | -------------------------------------------------------------------------------- /src/contract-types/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "0x1a4": { 3 | "chainName": "opt-goerli", 4 | "address": "0xF93F605142Fb1Efad7Aa58253dDffF67775b4520", 5 | "rpcUrl": "https://opt-goerli.g.alchemy.com/v2/rksDkSUXd2dyk2ANy_zzODknx_AAokui" 6 | }, 7 | "0xaa37dc": { 8 | "chainName": "opt-sepolia", 9 | "address": "0x6D0f81BDA11995f25921aAd5B43359630E65Ca96", 10 | "rpcUrl": "https://opt-sepolia.g.alchemy.com/v2/aO1-SfG4oFRLyAiLREqzyAUu0HTCwHgs" 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/contract-types/contracts/factories/Reclaim__factory.ts: -------------------------------------------------------------------------------- 1 | /* Autogenerated file. Do not edit manually. */ 2 | /* tslint:disable */ 3 | 4 | import { Contract } from 'ethers'; 5 | 6 | const _abi = [ 7 | { 8 | anonymous: false, 9 | inputs: [ 10 | { 11 | indexed: false, 12 | internalType: 'address', 13 | name: 'previousAdmin', 14 | type: 'address', 15 | }, 16 | { 17 | indexed: false, 18 | internalType: 'address', 19 | name: 'newAdmin', 20 | type: 'address', 21 | }, 22 | ], 23 | name: 'AdminChanged', 24 | type: 'event', 25 | }, 26 | { 27 | anonymous: false, 28 | inputs: [ 29 | { 30 | indexed: true, 31 | internalType: 'address', 32 | name: 'beacon', 33 | type: 'address', 34 | }, 35 | ], 36 | name: 'BeaconUpgraded', 37 | type: 'event', 38 | }, 39 | { 40 | anonymous: false, 41 | inputs: [ 42 | { 43 | components: [ 44 | { 45 | internalType: 'uint32', 46 | name: 'id', 47 | type: 'uint32', 48 | }, 49 | { 50 | internalType: 'uint32', 51 | name: 'timestampStart', 52 | type: 'uint32', 53 | }, 54 | { 55 | internalType: 'uint32', 56 | name: 'timestampEnd', 57 | type: 'uint32', 58 | }, 59 | { 60 | components: [ 61 | { 62 | internalType: 'address', 63 | name: 'addr', 64 | type: 'address', 65 | }, 66 | { 67 | internalType: 'string', 68 | name: 'host', 69 | type: 'string', 70 | }, 71 | ], 72 | internalType: 'struct Reclaim.Witness[]', 73 | name: 'witnesses', 74 | type: 'tuple[]', 75 | }, 76 | { 77 | internalType: 'uint8', 78 | name: 'minimumWitnessesForClaimCreation', 79 | type: 'uint8', 80 | }, 81 | ], 82 | indexed: false, 83 | internalType: 'struct Reclaim.Epoch', 84 | name: 'epoch', 85 | type: 'tuple', 86 | }, 87 | ], 88 | name: 'EpochAdded', 89 | type: 'event', 90 | }, 91 | { 92 | anonymous: false, 93 | inputs: [ 94 | { 95 | indexed: false, 96 | internalType: 'uint8', 97 | name: 'version', 98 | type: 'uint8', 99 | }, 100 | ], 101 | name: 'Initialized', 102 | type: 'event', 103 | }, 104 | { 105 | anonymous: false, 106 | inputs: [ 107 | { 108 | indexed: true, 109 | internalType: 'address', 110 | name: 'previousOwner', 111 | type: 'address', 112 | }, 113 | { 114 | indexed: true, 115 | internalType: 'address', 116 | name: 'newOwner', 117 | type: 'address', 118 | }, 119 | ], 120 | name: 'OwnershipTransferred', 121 | type: 'event', 122 | }, 123 | { 124 | anonymous: false, 125 | inputs: [ 126 | { 127 | indexed: true, 128 | internalType: 'address', 129 | name: 'implementation', 130 | type: 'address', 131 | }, 132 | ], 133 | name: 'Upgraded', 134 | type: 'event', 135 | }, 136 | { 137 | inputs: [ 138 | { 139 | internalType: 'address', 140 | name: 'witnessAddress', 141 | type: 'address', 142 | }, 143 | { 144 | internalType: 'string', 145 | name: 'host', 146 | type: 'string', 147 | }, 148 | ], 149 | name: 'addAsWitness', 150 | outputs: [], 151 | stateMutability: 'nonpayable', 152 | type: 'function', 153 | }, 154 | { 155 | inputs: [], 156 | name: 'addNewEpoch', 157 | outputs: [], 158 | stateMutability: 'nonpayable', 159 | type: 'function', 160 | }, 161 | { 162 | inputs: [ 163 | { 164 | internalType: 'uint32', 165 | name: 'epochNum', 166 | type: 'uint32', 167 | }, 168 | { 169 | components: [ 170 | { 171 | internalType: 'string', 172 | name: 'provider', 173 | type: 'string', 174 | }, 175 | { 176 | internalType: 'string', 177 | name: 'parameters', 178 | type: 'string', 179 | }, 180 | { 181 | internalType: 'string', 182 | name: 'context', 183 | type: 'string', 184 | }, 185 | ], 186 | internalType: 'struct Claims.ClaimInfo', 187 | name: 'claimInfo', 188 | type: 'tuple', 189 | }, 190 | { 191 | components: [ 192 | { 193 | internalType: 'bytes32', 194 | name: 'identifier', 195 | type: 'bytes32', 196 | }, 197 | { 198 | internalType: 'address', 199 | name: 'owner', 200 | type: 'address', 201 | }, 202 | { 203 | internalType: 'uint32', 204 | name: 'timestampS', 205 | type: 'uint32', 206 | }, 207 | { 208 | internalType: 'uint256', 209 | name: 'epoch', 210 | type: 'uint256', 211 | }, 212 | ], 213 | internalType: 'struct Claims.CompleteClaimData', 214 | name: 'claimData', 215 | type: 'tuple', 216 | }, 217 | { 218 | internalType: 'bytes[]', 219 | name: 'signatures', 220 | type: 'bytes[]', 221 | }, 222 | ], 223 | name: 'assertValidEpochAndSignedClaim', 224 | outputs: [], 225 | stateMutability: 'view', 226 | type: 'function', 227 | }, 228 | { 229 | inputs: [], 230 | name: 'currentEpoch', 231 | outputs: [ 232 | { 233 | internalType: 'uint32', 234 | name: '', 235 | type: 'uint32', 236 | }, 237 | ], 238 | stateMutability: 'view', 239 | type: 'function', 240 | }, 241 | { 242 | inputs: [], 243 | name: 'epochDurationS', 244 | outputs: [ 245 | { 246 | internalType: 'uint32', 247 | name: '', 248 | type: 'uint32', 249 | }, 250 | ], 251 | stateMutability: 'view', 252 | type: 'function', 253 | }, 254 | { 255 | inputs: [ 256 | { 257 | internalType: 'uint256', 258 | name: '', 259 | type: 'uint256', 260 | }, 261 | ], 262 | name: 'epochs', 263 | outputs: [ 264 | { 265 | internalType: 'uint32', 266 | name: 'id', 267 | type: 'uint32', 268 | }, 269 | { 270 | internalType: 'uint32', 271 | name: 'timestampStart', 272 | type: 'uint32', 273 | }, 274 | { 275 | internalType: 'uint32', 276 | name: 'timestampEnd', 277 | type: 'uint32', 278 | }, 279 | { 280 | internalType: 'uint8', 281 | name: 'minimumWitnessesForClaimCreation', 282 | type: 'uint8', 283 | }, 284 | ], 285 | stateMutability: 'view', 286 | type: 'function', 287 | }, 288 | { 289 | inputs: [ 290 | { 291 | internalType: 'uint32', 292 | name: 'epoch', 293 | type: 'uint32', 294 | }, 295 | ], 296 | name: 'fetchEpoch', 297 | outputs: [ 298 | { 299 | components: [ 300 | { 301 | internalType: 'uint32', 302 | name: 'id', 303 | type: 'uint32', 304 | }, 305 | { 306 | internalType: 'uint32', 307 | name: 'timestampStart', 308 | type: 'uint32', 309 | }, 310 | { 311 | internalType: 'uint32', 312 | name: 'timestampEnd', 313 | type: 'uint32', 314 | }, 315 | { 316 | components: [ 317 | { 318 | internalType: 'address', 319 | name: 'addr', 320 | type: 'address', 321 | }, 322 | { 323 | internalType: 'string', 324 | name: 'host', 325 | type: 'string', 326 | }, 327 | ], 328 | internalType: 'struct Reclaim.Witness[]', 329 | name: 'witnesses', 330 | type: 'tuple[]', 331 | }, 332 | { 333 | internalType: 'uint8', 334 | name: 'minimumWitnessesForClaimCreation', 335 | type: 'uint8', 336 | }, 337 | ], 338 | internalType: 'struct Reclaim.Epoch', 339 | name: '', 340 | type: 'tuple', 341 | }, 342 | ], 343 | stateMutability: 'view', 344 | type: 'function', 345 | }, 346 | { 347 | inputs: [ 348 | { 349 | internalType: 'uint32', 350 | name: 'epoch', 351 | type: 'uint32', 352 | }, 353 | { 354 | internalType: 'bytes32', 355 | name: 'identifier', 356 | type: 'bytes32', 357 | }, 358 | { 359 | internalType: 'uint32', 360 | name: 'timestampS', 361 | type: 'uint32', 362 | }, 363 | ], 364 | name: 'fetchWitnessesForClaim', 365 | outputs: [ 366 | { 367 | components: [ 368 | { 369 | internalType: 'address', 370 | name: 'addr', 371 | type: 'address', 372 | }, 373 | { 374 | internalType: 'string', 375 | name: 'host', 376 | type: 'string', 377 | }, 378 | ], 379 | internalType: 'struct Reclaim.Witness[]', 380 | name: '', 381 | type: 'tuple[]', 382 | }, 383 | ], 384 | stateMutability: 'view', 385 | type: 'function', 386 | }, 387 | { 388 | inputs: [], 389 | name: 'initialize', 390 | outputs: [], 391 | stateMutability: 'nonpayable', 392 | type: 'function', 393 | }, 394 | { 395 | inputs: [], 396 | name: 'minimumWitnessesForClaimCreation', 397 | outputs: [ 398 | { 399 | internalType: 'uint8', 400 | name: '', 401 | type: 'uint8', 402 | }, 403 | ], 404 | stateMutability: 'view', 405 | type: 'function', 406 | }, 407 | { 408 | inputs: [], 409 | name: 'owner', 410 | outputs: [ 411 | { 412 | internalType: 'address', 413 | name: '', 414 | type: 'address', 415 | }, 416 | ], 417 | stateMutability: 'view', 418 | type: 'function', 419 | }, 420 | { 421 | inputs: [], 422 | name: 'proxiableUUID', 423 | outputs: [ 424 | { 425 | internalType: 'bytes32', 426 | name: '', 427 | type: 'bytes32', 428 | }, 429 | ], 430 | stateMutability: 'view', 431 | type: 'function', 432 | }, 433 | { 434 | inputs: [ 435 | { 436 | internalType: 'address', 437 | name: 'witnessAddress', 438 | type: 'address', 439 | }, 440 | ], 441 | name: 'removeAsWitness', 442 | outputs: [], 443 | stateMutability: 'nonpayable', 444 | type: 'function', 445 | }, 446 | { 447 | inputs: [], 448 | name: 'renounceOwnership', 449 | outputs: [], 450 | stateMutability: 'nonpayable', 451 | type: 'function', 452 | }, 453 | { 454 | inputs: [ 455 | { 456 | internalType: 'address', 457 | name: 'newOwner', 458 | type: 'address', 459 | }, 460 | ], 461 | name: 'transferOwnership', 462 | outputs: [], 463 | stateMutability: 'nonpayable', 464 | type: 'function', 465 | }, 466 | { 467 | inputs: [ 468 | { 469 | internalType: 'address', 470 | name: 'addr', 471 | type: 'address', 472 | }, 473 | { 474 | internalType: 'bool', 475 | name: 'isWhitelisted', 476 | type: 'bool', 477 | }, 478 | ], 479 | name: 'updateWitnessWhitelist', 480 | outputs: [], 481 | stateMutability: 'nonpayable', 482 | type: 'function', 483 | }, 484 | { 485 | inputs: [ 486 | { 487 | internalType: 'address', 488 | name: 'newImplementation', 489 | type: 'address', 490 | }, 491 | ], 492 | name: 'upgradeTo', 493 | outputs: [], 494 | stateMutability: 'nonpayable', 495 | type: 'function', 496 | }, 497 | { 498 | inputs: [ 499 | { 500 | internalType: 'address', 501 | name: 'newImplementation', 502 | type: 'address', 503 | }, 504 | { 505 | internalType: 'bytes', 506 | name: 'data', 507 | type: 'bytes', 508 | }, 509 | ], 510 | name: 'upgradeToAndCall', 511 | outputs: [], 512 | stateMutability: 'payable', 513 | type: 'function', 514 | }, 515 | { 516 | inputs: [ 517 | { 518 | internalType: 'uint256', 519 | name: '', 520 | type: 'uint256', 521 | }, 522 | ], 523 | name: 'witnesses', 524 | outputs: [ 525 | { 526 | internalType: 'address', 527 | name: 'addr', 528 | type: 'address', 529 | }, 530 | { 531 | internalType: 'string', 532 | name: 'host', 533 | type: 'string', 534 | }, 535 | ], 536 | stateMutability: 'view', 537 | type: 'function', 538 | }, 539 | ] as const; 540 | 541 | export class Reclaim__factory { 542 | static readonly abi = _abi; 543 | 544 | static connect(address: string, signerOrProvider: any): Contract { 545 | return new Contract(address, _abi, signerOrProvider); 546 | } 547 | } 548 | -------------------------------------------------------------------------------- /src/contract-types/contracts/factories/index.ts: -------------------------------------------------------------------------------- 1 | export { Reclaim__factory } from './Reclaim__factory'; 2 | -------------------------------------------------------------------------------- /src/contract-types/contracts/index.ts: -------------------------------------------------------------------------------- 1 | /* Autogenerated file. Do not edit manually. */ 2 | /* tslint:disable */ 3 | export { Reclaim__factory } from './factories/Reclaim__factory'; 4 | -------------------------------------------------------------------------------- /src/contract-types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './contracts'; 2 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Reclaim'; 2 | export * from './utils/interfaces'; 3 | export { ClaimCreationType, ModalOptions, DeviceType } from './utils/types'; 4 | // Export device detection utilities for debugging (optional) 5 | export { 6 | getDeviceType, 7 | getMobileDeviceType, 8 | isMobileDevice, 9 | isDesktopDevice, 10 | clearDeviceCache 11 | } from './utils/device'; -------------------------------------------------------------------------------- /src/smart-contract.ts: -------------------------------------------------------------------------------- 1 | import type { Beacon, BeaconState } from './utils/interfaces'; 2 | import { Reclaim__factory as ReclaimFactory } from './contract-types'; 3 | import CONTRACTS_CONFIG from './contract-types/config.json'; 4 | import { Contract, ethers } from 'ethers'; 5 | 6 | const DEFAULT_CHAIN_ID = 11155420; 7 | 8 | export function makeBeacon(chainId?: number): Beacon | undefined { 9 | chainId = chainId || DEFAULT_CHAIN_ID; 10 | const contract = getContract(chainId); 11 | if (contract) { 12 | return makeBeaconCacheable({ 13 | async getState(epochId: number | undefined): Promise { 14 | //@ts-ignore 15 | const epoch = await contract.fetchEpoch(epochId || 0); 16 | if (!epoch.id) { 17 | throw new Error(`Invalid epoch ID: ${epochId}`); 18 | } 19 | 20 | return { 21 | epoch: epoch.id, 22 | witnesses: epoch.witnesses.map((w: any) => ({ 23 | id: w.addr.toLowerCase(), 24 | url: w.host, 25 | })), 26 | witnessesRequiredForClaim: epoch.minimumWitnessesForClaimCreation, 27 | nextEpochTimestampS: epoch.timestampEnd, 28 | }; 29 | }, 30 | }); 31 | } else { 32 | return undefined; 33 | } 34 | } 35 | 36 | export function makeBeaconCacheable(beacon: Beacon): Beacon { 37 | const cache: { [epochId: number]: Promise } = {}; 38 | 39 | return { 40 | ...beacon, 41 | async getState(epochId: number | undefined): Promise { 42 | if (!epochId) { 43 | // TODO: add cache here 44 | const state = await beacon.getState(); 45 | return state; 46 | } 47 | 48 | const key = epochId; 49 | 50 | if (!cache[key]) { 51 | cache[key] = beacon.getState(epochId); 52 | } 53 | 54 | return cache[key] as unknown as BeaconState; 55 | }, 56 | }; 57 | } 58 | 59 | const existingContractsMap: { [chain: string]: Contract } = {}; 60 | 61 | function getContract(chainId: number): Contract { 62 | const chainKey = `0x${chainId.toString(16)}`; 63 | if (!existingContractsMap[chainKey]) { 64 | const contractData = 65 | CONTRACTS_CONFIG[chainKey as keyof typeof CONTRACTS_CONFIG]; 66 | if (!contractData) { 67 | throw new Error(`Unsupported chain: "${chainKey}"`); 68 | } 69 | 70 | const rpcProvider = new ethers.JsonRpcProvider(contractData.rpcUrl); 71 | existingContractsMap[chainKey] = ReclaimFactory.connect( 72 | contractData.address, 73 | rpcProvider 74 | ); 75 | } 76 | 77 | return existingContractsMap[chainKey] as Contract; 78 | } 79 | -------------------------------------------------------------------------------- /src/utils/__tests__/helper.test.ts: -------------------------------------------------------------------------------- 1 | import { escapeRegExp, replaceAll } from '../helper'; 2 | 3 | describe('escapeRegExp', () => { 4 | it('should escape special characters in a string', () => { 5 | const input = '.*+?^${}()|[]\\'; 6 | const expectedOutput = '\\.\\*\\+\\?\\^\\$\\{\\}\\(\\)\\|\\[\\]\\\\'; 7 | expect(escapeRegExp(input)).toBe(expectedOutput); 8 | }); 9 | 10 | it('should return the same string if there are no special characters', () => { 11 | const input = 'abc123'; 12 | const expectedOutput = 'abc123'; 13 | expect(escapeRegExp(input)).toBe(expectedOutput); 14 | }); 15 | 16 | it('should return an empty string if the input is empty', () => { 17 | const input = ''; 18 | const expectedOutput = ''; 19 | expect(escapeRegExp(input)).toBe(expectedOutput); 20 | }); 21 | }); 22 | 23 | describe('replaceAll', () => { 24 | it('should replace all occurrences of a substring in a string', () => { 25 | const str = 'hello world, hello universe'; 26 | const find = 'hello'; 27 | const replace = 'hi'; 28 | const expectedOutput = 'hi world, hi universe'; 29 | expect(replaceAll(str, find, replace)).toBe(expectedOutput); 30 | }); 31 | 32 | it('should return the same string if the substring to find is not present', () => { 33 | const str = 'hello world'; 34 | const find = 'bye'; 35 | const replace = 'hi'; 36 | const expectedOutput = 'hello world'; 37 | expect(replaceAll(str, find, replace)).toBe(expectedOutput); 38 | }); 39 | 40 | it('should return the same string if the substring to find is empty', () => { 41 | const str = 'hello world'; 42 | const find = ''; 43 | const replace = 'hi'; 44 | const expectedOutput = 'hello world'; 45 | expect(replaceAll(str, find, replace)).toBe(expectedOutput); 46 | }); 47 | 48 | it('should replace all occurrences of a substring containing special regex characters', () => { 49 | const str = 'hello.world.hello.world'; 50 | const find = '.'; 51 | const replace = '!'; 52 | const expectedOutput = 'hello!world!hello!world'; 53 | expect(replaceAll(str, find, replace)).toBe(expectedOutput); 54 | }); 55 | 56 | it('should return an empty string if the input string is empty', () => { 57 | const str = ''; 58 | const find = 'hello'; 59 | const replace = 'hi'; 60 | const expectedOutput = ''; 61 | expect(replaceAll(str, find, replace)).toBe(expectedOutput); 62 | }); 63 | 64 | it('should handle overlapping substrings correctly', () => { 65 | const str = 'aaa'; 66 | const find = 'aa'; 67 | const replace = 'b'; 68 | const expectedOutput = 'ba'; 69 | expect(replaceAll(str, find, replace)).toBe(expectedOutput); 70 | }); 71 | 72 | it('should replace the substring with an empty string', () => { 73 | const str = 'hello world'; 74 | const find = 'world'; 75 | const replace = ''; 76 | const expectedOutput = 'hello '; 77 | expect(replaceAll(str, find, replace)).toBe(expectedOutput); 78 | }); 79 | }); -------------------------------------------------------------------------------- /src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | // Base URL for the backend API 2 | export let BACKEND_BASE_URL = "https://api.reclaimprotocol.org"; 3 | 4 | export function setBackendBaseUrl(url: string) { 5 | BACKEND_BASE_URL = url; 6 | } 7 | 8 | // Constant values used throughout the application 9 | export const constants = { 10 | // Default callback URL for Reclaim protocol 11 | get DEFAULT_RECLAIM_CALLBACK_URL() { 12 | return `${BACKEND_BASE_URL}/api/sdk/callback?callbackId=`; 13 | }, 14 | 15 | // Default status URL for Reclaim sessions 16 | get DEFAULT_RECLAIM_STATUS_URL() { 17 | return `${BACKEND_BASE_URL}/api/sdk/session/`; 18 | }, 19 | 20 | // URL for sharing Reclaim templates 21 | RECLAIM_SHARE_URL: 'https://share.reclaimprotocol.org/verifier/?template=', 22 | 23 | // Chrome extension URL for Reclaim Protocol 24 | CHROME_EXTENSION_URL: 'https://chromewebstore.google.com/detail/reclaim-extension/oafieibbbcepkmenknelhmgaoahamdeh', 25 | 26 | // QR Code API base URL 27 | QR_CODE_API_URL: 'https://api.qrserver.com/v1/create-qr-code/' 28 | }; 29 | -------------------------------------------------------------------------------- /src/utils/device.ts: -------------------------------------------------------------------------------- 1 | import { DeviceType } from "./types"; 2 | 3 | const navigatorDefined = typeof navigator !== 'undefined'; 4 | const windowDefined = typeof window !== 'undefined'; 5 | 6 | const userAgent = navigatorDefined ? navigator.userAgent.toLowerCase() : ''; 7 | const userAgentData = navigatorDefined ? (navigator as Navigator & { 8 | userAgentData?: { 9 | platform: string; 10 | brands?: { brand: string; version: string }[]; 11 | } 12 | }).userAgentData : undefined; 13 | 14 | // Cache for device detection results 15 | let cachedDeviceType: DeviceType.DESKTOP | DeviceType.MOBILE | null = null; 16 | let cachedMobileType: DeviceType.ANDROID | DeviceType.IOS | null = null; 17 | 18 | /** 19 | * Safe wrapper for window.matchMedia 20 | */ 21 | function safeMatchMedia(query: string): boolean { 22 | try { 23 | return window.matchMedia?.(query)?.matches || false; 24 | } catch { 25 | return false; 26 | } 27 | } 28 | 29 | /** 30 | * Safe wrapper for CSS.supports 31 | */ 32 | function safeCSSSupports(property: string, value: string): boolean { 33 | try { 34 | return CSS?.supports?.(property, value) || false; 35 | } catch { 36 | return false; 37 | } 38 | } 39 | 40 | /** 41 | * Safe wrapper for document.querySelector 42 | */ 43 | function safeQuerySelector(selector: string): boolean { 44 | try { 45 | return document?.querySelector?.(selector) !== null; 46 | } catch { 47 | return false; 48 | } 49 | } 50 | 51 | /** 52 | * Highly accurate device type detection - returns only 'desktop' or 'mobile' 53 | * Uses multiple detection methods and scoring system for maximum accuracy 54 | * @returns {DeviceType.DESKTOP | DeviceType.MOBILE} The detected device type 55 | */ 56 | export function getDeviceType(): DeviceType.DESKTOP | DeviceType.MOBILE { 57 | // Return cached result if available 58 | if (cachedDeviceType !== null) { 59 | return cachedDeviceType; 60 | } 61 | 62 | // Early return for server-side rendering - assume desktop 63 | if (!navigatorDefined || !windowDefined) { 64 | return DeviceType.DESKTOP; 65 | } 66 | 67 | let mobileScore = 0; 68 | const CONFIDENCE_THRESHOLD = 3; // Need at least 3 points to be considered mobile 69 | 70 | // ====== Device Characteristics ====== 71 | 72 | // Screen dimensions 73 | const screenWidth = window.innerWidth || window.screen?.width || 0; 74 | const screenHeight = window.innerHeight || window.screen?.height || 0; 75 | const hasSmallScreen = screenWidth <= 480 || screenHeight <= 480; 76 | const hasLargeScreen = screenWidth > 1024 && screenHeight > 768; 77 | 78 | // Touch capabilities 79 | const hasTouch = 'ontouchstart' in window || 80 | (navigatorDefined && navigator.maxTouchPoints > 0); 81 | const hasPreciseMouse = safeMatchMedia('(pointer: fine)'); 82 | const canHover = safeMatchMedia('(hover: hover)'); 83 | const hasMouseAndTouch = hasTouch && hasPreciseMouse; // Touchscreen laptop 84 | 85 | // Windows touch laptop detection (used for exceptions) 86 | const isWindowsTouchLaptop = /Windows/i.test(userAgent) && 87 | hasPreciseMouse && 88 | hasTouch; 89 | 90 | // ====== Mobile Indicators (Add Points) ====== 91 | 92 | // Touch without mouse = likely mobile (+2 points) 93 | // Touch with mouse = touchscreen laptop (+1 point) 94 | if (hasTouch && !hasMouseAndTouch) { 95 | mobileScore += 2; 96 | } else if (hasMouseAndTouch) { 97 | mobileScore += 1; 98 | } 99 | 100 | // Small screen is mobile indicator (+2 points) 101 | // Exception: Windows touch laptops with precise mouse should not be penalized for small screens 102 | if (hasSmallScreen && !isWindowsTouchLaptop) { 103 | mobileScore += 2; 104 | } 105 | 106 | // Mobile user agent is strong indicator (+3 points) 107 | const hasMobileUserAgent = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini|mobile/i.test(userAgent); 108 | if (hasMobileUserAgent) { 109 | mobileScore += 3; 110 | } 111 | 112 | // Mobile APIs only count if combined with other mobile signs (+2 points) 113 | // Exception: Desktop Safari and Windows touch laptops have mobile APIs but should not be considered mobile 114 | const hasMobileAPIs = 'orientation' in window || 115 | 'DeviceMotionEvent' in window || 116 | 'DeviceOrientationEvent' in window; 117 | const isDesktopSafari = /Safari/i.test(userAgent) && 118 | !/Mobile/i.test(userAgent) && 119 | /Mac|Intel/i.test(userAgent); 120 | if (hasMobileAPIs && (hasSmallScreen || hasMobileUserAgent) && !isDesktopSafari && !isWindowsTouchLaptop) { 121 | mobileScore += 2; 122 | } 123 | 124 | // High DPI with small screen = mobile (+1 point) 125 | const hasHighDPI = window.devicePixelRatio > 1.5; 126 | if (hasHighDPI && hasSmallScreen) { 127 | mobileScore += 1; 128 | } 129 | 130 | // Viewport meta tag with small screen = mobile optimized (+1 point) 131 | const hasViewportMeta = safeQuerySelector('meta[name="viewport"]'); 132 | if (hasViewportMeta && hasSmallScreen) { 133 | mobileScore += 1; 134 | } 135 | 136 | // iPad Pro special case: Mac user agent with touch (+2 points) 137 | const isPadProInDesktopMode = userAgent.includes('macintosh') && hasTouch; 138 | if (isPadProInDesktopMode) { 139 | mobileScore += 2; 140 | } 141 | 142 | // ====== Desktop Indicators (Subtract Points) ====== 143 | 144 | // Large screen with mouse = desktop (-3 points) 145 | if (hasLargeScreen && hasPreciseMouse) { 146 | mobileScore -= 3; 147 | } 148 | // Large screen without touch = desktop (-2 points) 149 | else if (hasLargeScreen && !hasTouch) { 150 | mobileScore -= 2; 151 | } 152 | 153 | // Can hover with precise pointer = has real mouse (-2 points) 154 | if (hasPreciseMouse && canHover) { 155 | mobileScore -= 2; 156 | } 157 | 158 | // Windows user agent = strong desktop indicator (-3 points) 159 | const isWindowsDesktop = /Windows/i.test(userAgent) && !hasMobileUserAgent; 160 | if (isWindowsDesktop) { 161 | mobileScore -= 3; 162 | } 163 | 164 | // Cache and return the result 165 | cachedDeviceType = mobileScore >= CONFIDENCE_THRESHOLD ? DeviceType.MOBILE : DeviceType.DESKTOP; 166 | return cachedDeviceType; 167 | } 168 | 169 | /** 170 | * Highly accurate mobile device type detection - returns only 'android' or 'ios' 171 | * Should only be called when getDeviceType() returns 'mobile' 172 | * @returns {DeviceType.ANDROID | DeviceType.IOS} The detected mobile device type 173 | */ 174 | export function getMobileDeviceType(): DeviceType.ANDROID | DeviceType.IOS { 175 | // Return cached result if available 176 | if (cachedMobileType !== null) { 177 | return cachedMobileType; 178 | } 179 | 180 | // Early return for server-side rendering - default to Android 181 | if (!navigatorDefined || !windowDefined) { 182 | return DeviceType.ANDROID; 183 | } 184 | 185 | const ua = navigator.userAgent; 186 | 187 | // ====== iOS Detection ====== 188 | 189 | // Direct iOS device detection 190 | const hasIOSDeviceName = /iPad|iPhone|iPod/i.test(ua); 191 | if (hasIOSDeviceName) { 192 | cachedMobileType = DeviceType.IOS; 193 | return cachedMobileType; 194 | } 195 | 196 | // iPad Pro detection (reports as Mac but has touch) 197 | const isMacWithTouch = /Macintosh|MacIntel/i.test(ua) && 'ontouchstart' in window; 198 | const isMacOSWithTouch = userAgentData?.platform === 'macOS' && 'ontouchstart' in window; 199 | if (isMacWithTouch || isMacOSWithTouch) { 200 | cachedMobileType = DeviceType.IOS; 201 | return cachedMobileType; 202 | } 203 | 204 | // iOS-specific APIs 205 | const hasIOSPermissionAPI = typeof (window as any).DeviceMotionEvent?.requestPermission === 'function'; 206 | const hasIOSTouchCallout = safeCSSSupports('-webkit-touch-callout', 'none'); 207 | if (hasIOSPermissionAPI || hasIOSTouchCallout) { 208 | cachedMobileType = DeviceType.IOS; 209 | return cachedMobileType; 210 | } 211 | 212 | // Safari without Chrome (iOS WebKit) - but not desktop Safari 213 | const isIOSWebKit = /WebKit/i.test(ua) && 214 | !/Chrome|CriOS|Android/i.test(ua) && 215 | !/Macintosh|MacIntel/i.test(ua); 216 | if (isIOSWebKit) { 217 | cachedMobileType = DeviceType.IOS; 218 | return cachedMobileType; 219 | } 220 | 221 | // ====== Android Detection ====== 222 | 223 | // Direct Android detection 224 | const hasAndroidKeyword = /Android/i.test(ua); 225 | if (hasAndroidKeyword) { 226 | cachedMobileType = DeviceType.ANDROID; 227 | return cachedMobileType; 228 | } 229 | 230 | // Mobile Chrome (usually Android) 231 | const isChromeOnMobile = (window as any).chrome && /Mobile/i.test(ua); 232 | if (isChromeOnMobile) { 233 | cachedMobileType = DeviceType.ANDROID; 234 | return cachedMobileType; 235 | } 236 | 237 | // Default fallback - Android is more common globally 238 | cachedMobileType = DeviceType.ANDROID; 239 | return cachedMobileType; 240 | } 241 | 242 | /** 243 | * Convenience method to check if current device is mobile 244 | * @returns {boolean} True if device is mobile 245 | */ 246 | export function isMobileDevice(): boolean { 247 | return getDeviceType() === DeviceType.MOBILE; 248 | } 249 | 250 | /** 251 | * Convenience method to check if current device is desktop 252 | * @returns {boolean} True if device is desktop 253 | */ 254 | export function isDesktopDevice(): boolean { 255 | return getDeviceType() === DeviceType.DESKTOP; 256 | } 257 | 258 | /** 259 | * Clear cached device detection results (useful for testing) 260 | */ 261 | export function clearDeviceCache(): void { 262 | cachedDeviceType = null; 263 | cachedMobileType = null; 264 | } 265 | 266 | // Export safe wrappers for testing 267 | export { safeMatchMedia, safeCSSSupports, safeQuerySelector }; -------------------------------------------------------------------------------- /src/utils/errors.ts: -------------------------------------------------------------------------------- 1 | function createErrorClass(name: string) { 2 | return class extends Error { 3 | constructor(message?: string, public innerError?: Error) { 4 | // Include inner error message in the main message if available 5 | const fullMessage = innerError 6 | ? `${message || ''} caused by ${innerError.name}: ${innerError.message}` 7 | : message; 8 | 9 | super(fullMessage); 10 | this.name = name; 11 | if (innerError) { 12 | this.stack += `\nCaused by: ${innerError.stack}`; 13 | } 14 | } 15 | }; 16 | } 17 | 18 | export const TimeoutError = createErrorClass('TimeoutError'); 19 | export const ProofNotVerifiedError = createErrorClass('ProofNotVerifiedError'); 20 | export const SessionNotStartedError = createErrorClass('SessionNotStartedError'); 21 | export const ProviderNotFoundError = createErrorClass('ProviderNotFoundError'); 22 | export const SignatureGeneratingError = createErrorClass('SignatureGeneratingError'); 23 | export const SignatureNotFoundError = createErrorClass('SignatureNotFoundError'); 24 | export const InvalidSignatureError = createErrorClass('InvalidSignatureError'); 25 | export const UpdateSessionError = createErrorClass('UpdateSessionError'); 26 | export const InitSessionError = createErrorClass('InitSessionError'); 27 | export const ProviderFailedError = createErrorClass('ProviderFailedError'); 28 | export const InvalidParamError = createErrorClass('InvalidParamError'); 29 | export const ApplicationError = createErrorClass('ApplicationError'); 30 | export const InitError = createErrorClass('InitError'); 31 | export const BackendServerError = createErrorClass('BackendServerError'); 32 | export const GetStatusUrlError = createErrorClass('GetStatusUrlError'); 33 | export const NoProviderParamsError = createErrorClass('NoProviderParamsError'); 34 | export const SetParamsError = createErrorClass('SetParamsError'); 35 | export const AddContextError = createErrorClass('AddContextError'); 36 | export const SetSignatureError = createErrorClass('SetSignatureError'); 37 | export const GetAppCallbackUrlError = createErrorClass("GetAppCallbackUrlError"); 38 | export const StatusUrlError = createErrorClass('StatusUrlError'); 39 | export const InavlidParametersError = createErrorClass('InavlidParametersError'); 40 | export const ProofSubmissionFailedError = createErrorClass('ProofSubmissionFailedError'); -------------------------------------------------------------------------------- /src/utils/helper.ts: -------------------------------------------------------------------------------- 1 | import { OnError } from './types' 2 | import { TimeoutError } from './errors' 3 | import loggerModule from './logger' 4 | const logger = loggerModule.logger 5 | 6 | /** 7 | * Escapes special characters in a string for use in a regular expression 8 | * @param string - The input string to escape 9 | * @returns The input string with special regex characters escaped 10 | */ 11 | export function escapeRegExp(string: string): string { 12 | return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string 13 | } 14 | 15 | /** 16 | * Replaces all occurrences of a substring in a string 17 | * @param str - The original string 18 | * @param find - The substring to find 19 | * @param replace - The string to replace the found substrings with 20 | * @returns A new string with all occurrences of 'find' replaced by 'replace' 21 | */ 22 | export function replaceAll(str: string, find: string, replace: string): string { 23 | if (find === '') return str; 24 | return str.replace(new RegExp(escapeRegExp(find), 'g'), replace); 25 | } 26 | 27 | /** 28 | * Schedules a task to end an interval after a specified timeout 29 | * @param sessionId - The ID of the current session 30 | * @param intervals - A Map containing the intervals 31 | * @param onFailureCallback - Callback function to be called on failure 32 | * @param timeout - Timeout in milliseconds (default: 10 minutes) 33 | */ 34 | export function scheduleIntervalEndingTask( 35 | sessionId: string, 36 | intervals: Map, 37 | onFailureCallback: OnError, 38 | timeout: number = 1000 * 60 * 10 39 | ): void { 40 | setTimeout(() => { 41 | if (intervals.has(sessionId)) { 42 | const message = 'Interval ended without receiving proofs' 43 | onFailureCallback(new TimeoutError(message)) 44 | logger.info(message) 45 | clearInterval(intervals.get(sessionId) as NodeJS.Timeout) 46 | intervals.delete(sessionId) 47 | } 48 | }, timeout) 49 | } 50 | -------------------------------------------------------------------------------- /src/utils/interfaces.ts: -------------------------------------------------------------------------------- 1 | // Proof-related interfaces 2 | export interface Proof { 3 | identifier: string; 4 | claimData: ProviderClaimData; 5 | signatures: string[]; 6 | witnesses: WitnessData[]; 7 | extractedParameterValues: any; 8 | publicData?: { [key: string]: string }; 9 | taskId?: number; 10 | } 11 | 12 | // Extension Interactions 13 | export const RECLAIM_EXTENSION_ACTIONS = { 14 | CHECK_EXTENSION: 'RECLAIM_EXTENSION_CHECK', 15 | EXTENSION_RESPONSE: 'RECLAIM_EXTENSION_RESPONSE', 16 | START_VERIFICATION: 'RECLAIM_START_VERIFICATION', 17 | STATUS_UPDATE: 'RECLAIM_STATUS_UPDATE', 18 | }; 19 | 20 | export interface ExtensionMessage { 21 | action: string; 22 | messageId: string; 23 | data?: any; 24 | extensionID?: string; 25 | } 26 | 27 | export interface WitnessData { 28 | id: string; 29 | url: string; 30 | } 31 | 32 | export interface ProviderClaimData { 33 | provider: string; 34 | parameters: string; 35 | owner: string; 36 | timestampS: number; 37 | context: string; 38 | identifier: string; 39 | epoch: number; 40 | } 41 | 42 | // Context and Beacon interfaces 43 | export interface Context { 44 | contextAddress: string; 45 | contextMessage: string; 46 | } 47 | 48 | export interface Beacon { 49 | getState(epoch?: number): Promise; 50 | close?(): Promise; 51 | } 52 | 53 | export type BeaconState = { 54 | witnesses: WitnessData[]; 55 | epoch: number; 56 | witnessesRequiredForClaim: number; 57 | nextEpochTimestampS: number; 58 | }; 59 | 60 | 61 | -------------------------------------------------------------------------------- /src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | // Define the possible log levels 2 | export type LogLevel = 'info' | 'warn' | 'error' | 'silent'; 3 | 4 | // Define a simple logger class 5 | class SimpleLogger { 6 | private level: LogLevel = 'info'; 7 | 8 | setLevel(level: LogLevel) { 9 | this.level = level; 10 | } 11 | 12 | private shouldLog(messageLevel: LogLevel): boolean { 13 | const levels: LogLevel[] = ['error', 'warn', 'info', 'silent']; 14 | return levels.indexOf(this.level) >= levels.indexOf(messageLevel); 15 | } 16 | 17 | private log(level: LogLevel, message: string, ...args: any[]) { 18 | if (this.shouldLog(level) && this.level !== 'silent') { 19 | const logFunction = this.getLogFunction(level); 20 | console.log('current level', this.level); 21 | logFunction(`[${level.toUpperCase()}]`, message, ...args); 22 | } 23 | } 24 | 25 | private getLogFunction(level: LogLevel): (message?: any, ...optionalParams: any[]) => void { 26 | switch (level) { 27 | case 'error': 28 | return console.error; 29 | case 'warn': 30 | return console.warn; 31 | case 'info': 32 | return console.info; 33 | default: 34 | return () => {}; // No-op for 'silent' 35 | } 36 | } 37 | 38 | info(message: string, ...args: any[]) { 39 | this.log('info', message, ...args); 40 | } 41 | 42 | warn(message: string, ...args: any[]) { 43 | this.log('warn', message, ...args); 44 | } 45 | 46 | error(message: string, ...args: any[]) { 47 | this.log('error', message, ...args); 48 | } 49 | } 50 | 51 | // Create the logger instance 52 | const logger = new SimpleLogger(); 53 | 54 | // Function to set the log level 55 | export function setLogLevel(level: LogLevel) { 56 | logger.setLevel(level); 57 | } 58 | 59 | // Export the logger instance and the setLogLevel function 60 | export default { 61 | logger, 62 | setLogLevel 63 | }; 64 | -------------------------------------------------------------------------------- /src/utils/modalUtils.ts: -------------------------------------------------------------------------------- 1 | import loggerModule from './logger'; 2 | import { ModalOptions } from './types'; 3 | import { constants } from './constants'; 4 | const logger = loggerModule.logger; 5 | 6 | export class QRCodeModal { 7 | private modalId: string; 8 | private options: ModalOptions; 9 | private autoCloseTimer?: NodeJS.Timeout; 10 | private countdownTimer?: NodeJS.Timeout; 11 | private countdownSeconds: number = 60; 12 | 13 | constructor(options: ModalOptions = {}) { 14 | this.modalId = 'reclaim-qr-modal'; 15 | this.options = { 16 | title: 'Verify with Reclaim', 17 | description: 'Scan the QR code with your mobile device to complete verification', 18 | extensionUrl: constants.CHROME_EXTENSION_URL, 19 | darkTheme: false, 20 | modalPopupTimer: 1, // default to 1 minute 21 | showExtensionInstallButton: false, // default to false 22 | ...options 23 | }; 24 | } 25 | 26 | async show(requestUrl: string): Promise { 27 | try { 28 | // Remove existing modal if present 29 | this.close(); 30 | 31 | // Create modal HTML 32 | const modalHTML = this.createModalHTML(); 33 | 34 | // Add modal to DOM 35 | document.body.insertAdjacentHTML('beforeend', modalHTML); 36 | 37 | // Generate QR code 38 | await this.generateQRCode(requestUrl, 'reclaim-qr-code'); 39 | 40 | // Add event listeners 41 | this.addEventListeners(); 42 | 43 | // Start auto-close timer 44 | this.startAutoCloseTimer(); 45 | 46 | } catch (error) { 47 | logger.info('Error showing QR code modal:', error); 48 | throw error; 49 | } 50 | } 51 | 52 | close(): void { 53 | // Clear timers 54 | if (this.autoCloseTimer) { 55 | clearTimeout(this.autoCloseTimer); 56 | this.autoCloseTimer = undefined; 57 | } 58 | if (this.countdownTimer) { 59 | clearInterval(this.countdownTimer); 60 | this.countdownTimer = undefined; 61 | } 62 | 63 | const modal = document.getElementById(this.modalId); 64 | if (modal) { 65 | modal.remove(); 66 | } 67 | if (this.options.onClose) { 68 | this.options.onClose(); 69 | } 70 | } 71 | 72 | private getThemeStyles() { 73 | const isDark = this.options.darkTheme; 74 | 75 | return { 76 | modalBackground: isDark ? 'rgba(0, 0, 0, 0.8)' : 'rgba(0, 0, 0, 0.5)', 77 | cardBackground: isDark ? '#1f2937' : 'white', 78 | titleColor: isDark ? '#f9fafb' : '#1f2937', 79 | textColor: isDark ? '#d1d5db' : '#6b7280', 80 | qrBackground: isDark ? '#374151' : '#f9fafb', 81 | tipBackground: isDark ? '#1e40af' : '#f0f9ff', 82 | tipBorder: isDark ? '#1e40af' : '#e0f2fe', 83 | tipTextColor: isDark ? '#dbeafe' : '#0369a1', 84 | buttonBackground: isDark ? '#374151' : '#f3f4f6', 85 | buttonColor: isDark ? '#f9fafb' : '#374151', 86 | buttonHoverBackground: isDark ? '#4b5563' : '#e5e7eb', 87 | countdownColor: isDark ? '#6b7280' : '#9ca3af', 88 | progressBackground: isDark ? '#4b5563' : '#e5e7eb', 89 | progressGradient: isDark 90 | ? 'linear-gradient(90deg, #3b82f6 0%, #2563eb 50%, #1d4ed8 100%)' 91 | : 'linear-gradient(90deg, #2563eb 0%, #1d4ed8 50%, #1e40af 100%)', 92 | linkColor: isDark ? '#60a5fa' : '#2563eb', 93 | extensionButtonBackground: isDark ? '#1e40af' : '#2563eb', 94 | extensionButtonHover: isDark ? '#1d4ed8' : '#1d4ed8' 95 | }; 96 | } 97 | 98 | private createModalHTML(): string { 99 | const styles = this.getThemeStyles(); 100 | 101 | return ` 102 |
115 |
125 | 148 | 149 |

${this.options.title}

155 | 156 |

${this.options.description}

162 | 163 |
169 | 170 | ${this.options.showExtensionInstallButton ? ` 171 |
178 |

💡 For a better experience

184 |

Install our browser extension for seamless verification without QR codes

190 | 205 | Install Extension 206 | 207 |
` : ''} 208 | 209 |
210 |
Auto-close in 1:00
216 | 217 |
224 |
231 |
232 |
233 |
234 |
235 | ` 236 | } 237 | 238 | private async generateQRCode(text: string, containerId: string): Promise { 239 | try { 240 | // Simple QR code generation using a public API 241 | // In production, you might want to use a proper QR code library 242 | const qrCodeUrl = `${constants.QR_CODE_API_URL}?size=200x200&data=${encodeURIComponent(text)}`; 243 | 244 | const container = document.getElementById(containerId); 245 | const styles = this.getThemeStyles(); 246 | 247 | if (container) { 248 | container.innerHTML = ` 249 | QR Code for Reclaim verification 253 |
254 | QR code could not be loaded.
255 | 256 | Click here to open verification link 257 | 258 |
259 | `; 260 | } 261 | } catch (error) { 262 | logger.info('Error generating QR code:', error); 263 | // Fallback to text link 264 | const container = document.getElementById(containerId); 265 | const styles = this.getThemeStyles(); 266 | 267 | if (container) { 268 | container.innerHTML = ` 269 | 274 | `; 275 | } 276 | } 277 | } 278 | 279 | private addEventListeners(): void { 280 | const closeButton = document.getElementById('reclaim-close-modal'); 281 | const modal = document.getElementById(this.modalId); 282 | 283 | const closeModal = () => { 284 | this.close(); 285 | }; 286 | 287 | if (closeButton) { 288 | closeButton.addEventListener('click', closeModal); 289 | } 290 | 291 | // Close on backdrop click 292 | if (modal) { 293 | modal.addEventListener('click', (e) => { 294 | if (e.target === modal) { 295 | closeModal(); 296 | } 297 | }); 298 | } 299 | 300 | // Close on escape key 301 | const handleEscape = (e: KeyboardEvent) => { 302 | if (e.key === 'Escape') { 303 | closeModal(); 304 | document.removeEventListener('keydown', handleEscape); 305 | } 306 | }; 307 | document.addEventListener('keydown', handleEscape); 308 | } 309 | 310 | private startAutoCloseTimer(): void { 311 | this.countdownSeconds = (this.options.modalPopupTimer || 1) * 60; // default to 1 minute 312 | 313 | // Update countdown display immediately 314 | this.updateCountdownDisplay(); 315 | 316 | // Start countdown timer (updates every second) 317 | this.countdownTimer = setInterval(() => { 318 | this.countdownSeconds--; 319 | this.updateCountdownDisplay(); 320 | 321 | if (this.countdownSeconds <= 0) { 322 | this.close(); 323 | } 324 | }, 1000); 325 | 326 | // Set auto-close timer for the number of minutes specified in the options in milliseconds 327 | const autoCloseMs = (this.options.modalPopupTimer || 1) * 60 * 1000; 328 | this.autoCloseTimer = setTimeout(() => { 329 | this.close(); 330 | }, autoCloseMs); 331 | } 332 | 333 | private updateCountdownDisplay(): void { 334 | const countdownElement = document.getElementById('reclaim-countdown'); 335 | const progressBar = document.getElementById('reclaim-progress-bar'); 336 | 337 | if (countdownElement) { 338 | const minutes = Math.floor(this.countdownSeconds / 60); 339 | const seconds = this.countdownSeconds % 60; 340 | const timeString = `${minutes}:${seconds.toString().padStart(2, '0')}`; 341 | countdownElement.textContent = `Auto-close in ${timeString}`; 342 | } 343 | 344 | if (progressBar) { 345 | // Calculate progress percentage (reverse: starts at 100%, goes to 0%) 346 | const totalSeconds = (this.options.modalPopupTimer || 1) * 60; 347 | const progressPercentage = (this.countdownSeconds / totalSeconds) * 100; 348 | progressBar.style.width = `${progressPercentage}%`; 349 | } 350 | } 351 | } -------------------------------------------------------------------------------- /src/utils/proofUtils.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "ethers"; 2 | import { WitnessData } from "./interfaces"; 3 | import { SignedClaim, TemplateData } from "./types"; 4 | import { createSignDataForClaim, fetchWitnessListForClaim } from "../witness"; 5 | import { BACKEND_BASE_URL, constants } from "./constants"; 6 | import { replaceAll } from "./helper"; 7 | import { validateURL } from "./validationUtils"; 8 | import { makeBeacon } from "../smart-contract"; 9 | import { ProofNotVerifiedError } from "./errors"; 10 | import loggerModule from './logger'; 11 | const logger = loggerModule.logger; 12 | 13 | 14 | /** 15 | * Retrieves a shortened URL for the given URL 16 | * @param url - The URL to be shortened 17 | * @returns A promise that resolves to the shortened URL, or the original URL if shortening fails 18 | */ 19 | export async function getShortenedUrl(url: string): Promise { 20 | logger.info(`Attempting to shorten URL: ${url}`); 21 | try { 22 | validateURL(url, 'getShortenedUrl') 23 | const response = await fetch(`${BACKEND_BASE_URL}/api/sdk/shortener`, { 24 | method: 'POST', 25 | headers: { 'Content-Type': 'application/json' }, 26 | body: JSON.stringify({ fullUrl: url }) 27 | }) 28 | const res = await response.json() 29 | if (!response.ok) { 30 | logger.info(`Failed to shorten URL: ${url}, Response: ${JSON.stringify(res)}`); 31 | return url; 32 | } 33 | const shortenedVerificationUrl = res.result.shortUrl 34 | return shortenedVerificationUrl 35 | } catch (err) { 36 | logger.info(`Error shortening URL: ${url}, Error: ${err}`); 37 | return url 38 | } 39 | } 40 | 41 | /** 42 | * Creates a link with embedded template data 43 | * @param templateData - The data to be embedded in the link 44 | * @param sharePagePath - The path to the share page (optional) 45 | * @returns A promise that resolves to the created link (shortened if possible) 46 | */ 47 | export async function createLinkWithTemplateData(templateData: TemplateData, sharePagePath?: string): Promise { 48 | let template = encodeURIComponent(JSON.stringify(templateData)) 49 | template = replaceAll(template, '(', '%28') 50 | template = replaceAll(template, ')', '%29') 51 | const fullLink = sharePagePath ? `${sharePagePath}/?template=${template}` : `${constants.RECLAIM_SHARE_URL}${template}` 52 | try { 53 | const shortenedLink = await getShortenedUrl(fullLink) 54 | return shortenedLink; 55 | } catch (err) { 56 | logger.info(`Error creating link for sessionId: ${templateData.sessionId}, Error: ${err}`); 57 | return fullLink; 58 | } 59 | } 60 | 61 | /** 62 | * Retrieves the list of witnesses for a given claim 63 | * @param epoch - The epoch number 64 | * @param identifier - The claim identifier 65 | * @param timestampS - The timestamp in seconds 66 | * @returns A promise that resolves to an array of witness addresses 67 | * @throws Error if no beacon is available 68 | */ 69 | export async function getWitnessesForClaim( 70 | epoch: number, 71 | identifier: string, 72 | timestampS: number 73 | ): Promise { 74 | const beacon = makeBeacon() 75 | if (!beacon) { 76 | logger.info('No beacon available for getting witnesses'); 77 | throw new Error('No beacon available'); 78 | } 79 | const state = await beacon.getState(epoch) 80 | const witnessList = fetchWitnessListForClaim(state, identifier, timestampS) 81 | const witnesses = witnessList.map((w: WitnessData) => w.id.toLowerCase()) 82 | return witnesses; 83 | } 84 | 85 | /** 86 | * Recovers the signers' addresses from a signed claim 87 | * @param claim - The signed claim object 88 | * @param signatures - The signatures associated with the claim 89 | * @returns An array of recovered signer addresses 90 | */ 91 | export function recoverSignersOfSignedClaim({ 92 | claim, 93 | signatures 94 | }: SignedClaim): string[] { 95 | const dataStr = createSignDataForClaim({ ...claim }) 96 | const signers = signatures.map(signature => 97 | ethers.verifyMessage(dataStr, ethers.hexlify(signature)).toLowerCase() 98 | ) 99 | return signers; 100 | } 101 | 102 | /** 103 | * Asserts that a signed claim is valid by checking if all expected witnesses have signed 104 | * @param claim - The signed claim to validate 105 | * @param expectedWitnessAddresses - An array of expected witness addresses 106 | * @throws ProofNotVerifiedError if any expected witness signature is missing 107 | */ 108 | export function assertValidSignedClaim( 109 | claim: SignedClaim, 110 | expectedWitnessAddresses: string[] 111 | ): void { 112 | const witnessAddresses = recoverSignersOfSignedClaim(claim) 113 | const witnessesNotSeen = new Set(expectedWitnessAddresses) 114 | for (const witness of witnessAddresses) { 115 | if (witnessesNotSeen.has(witness)) { 116 | witnessesNotSeen.delete(witness) 117 | } 118 | } 119 | 120 | if (witnessesNotSeen.size > 0) { 121 | const missingWitnesses = Array.from(witnessesNotSeen).join(', '); 122 | logger.info(`Claim validation failed. Missing signatures from: ${missingWitnesses}`); 123 | throw new ProofNotVerifiedError( 124 | `Missing signatures from ${missingWitnesses}` 125 | ) 126 | } 127 | } -------------------------------------------------------------------------------- /src/utils/sessionUtils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | InitSessionError, 3 | UpdateSessionError, 4 | StatusUrlError 5 | } from "./errors"; 6 | import { InitSessionResponse, SessionStatus, StatusUrlResponse } from "./types"; 7 | import { validateFunctionParams } from "./validationUtils"; 8 | import { BACKEND_BASE_URL, constants } from './constants'; 9 | import loggerModule from './logger'; 10 | const logger = loggerModule.logger; 11 | 12 | /** 13 | * Initializes a session with the provided parameters 14 | * @param providerId - The ID of the provider 15 | * @param appId - The ID of the application 16 | * @param timestamp - The timestamp of the request 17 | * @param signature - The signature for authentication 18 | * @returns A promise that resolves to an InitSessionResponse 19 | * @throws InitSessionError if the session initialization fails 20 | */ 21 | export async function initSession( 22 | providerId: string, 23 | appId: string, 24 | timestamp: string, 25 | signature: string, 26 | versionNumber?: string 27 | ): Promise { 28 | logger.info(`Initializing session for providerId: ${providerId}, appId: ${appId}`); 29 | try { 30 | const response = await fetch(`${BACKEND_BASE_URL}/api/sdk/init/session/`, { 31 | method: 'POST', 32 | headers: { 'Content-Type': 'application/json' }, 33 | body: JSON.stringify({ providerId, appId, timestamp, signature, versionNumber }) 34 | }); 35 | 36 | const res = await response.json(); 37 | 38 | if (!response.ok) { 39 | logger.info(`Session initialization failed: ${res.message || 'Unknown error'}`); 40 | throw new InitSessionError(res.message || `Error initializing session with providerId: ${providerId}`); 41 | } 42 | 43 | return res as InitSessionResponse; 44 | } catch (err) { 45 | logger.info(`Failed to initialize session for providerId: ${providerId}, appId: ${appId}`, err); 46 | throw err; 47 | } 48 | } 49 | 50 | /** 51 | * Updates the status of an existing session 52 | * @param sessionId - The ID of the session to update 53 | * @param status - The new status of the session 54 | * @returns A promise that resolves to the update response 55 | * @throws UpdateSessionError if the session update fails 56 | */ 57 | export async function updateSession(sessionId: string, status: SessionStatus) { 58 | logger.info(`Updating session status for sessionId: ${sessionId}, new status: ${status}`); 59 | validateFunctionParams( 60 | [{ input: sessionId, paramName: 'sessionId', isString: true }], 61 | 'updateSession' 62 | ); 63 | 64 | try { 65 | const response = await fetch(`${BACKEND_BASE_URL}/api/sdk/update/session/`, { 66 | method: 'POST', 67 | headers: { 'Content-Type': 'application/json' }, 68 | body: JSON.stringify({ sessionId, status }) 69 | }); 70 | 71 | const res = await response.json(); 72 | 73 | if (!response.ok) { 74 | const errorMessage = `Error updating session with sessionId: ${sessionId}. Status Code: ${response.status}`; 75 | logger.info(errorMessage, res); 76 | throw new UpdateSessionError(errorMessage); 77 | } 78 | 79 | logger.info(`Session status updated successfully for sessionId: ${sessionId}`); 80 | return res; 81 | } catch (err) { 82 | const errorMessage = `Failed to update session with sessionId: ${sessionId}`; 83 | logger.info(errorMessage, err); 84 | throw new UpdateSessionError(`Error updating session with sessionId: ${sessionId}`); 85 | } 86 | } 87 | 88 | /** 89 | * Fetches the status URL for a given session ID 90 | * @param sessionId - The ID of the session to fetch the status URL for 91 | * @returns A promise that resolves to a StatusUrlResponse 92 | * @throws StatusUrlError if the status URL fetch fails 93 | */ 94 | export async function fetchStatusUrl(sessionId: string): Promise { 95 | validateFunctionParams( 96 | [{ input: sessionId, paramName: 'sessionId', isString: true }], 97 | 'fetchStatusUrl' 98 | ); 99 | 100 | try { 101 | const response = await fetch(`${constants.DEFAULT_RECLAIM_STATUS_URL}${sessionId}`, { 102 | method: 'GET', 103 | headers: { 'Content-Type': 'application/json' } 104 | }); 105 | 106 | const res = await response.json(); 107 | 108 | if (!response.ok) { 109 | const errorMessage = `Error fetching status URL for sessionId: ${sessionId}. Status Code: ${response.status}`; 110 | logger.info(errorMessage, res); 111 | throw new StatusUrlError(errorMessage); 112 | } 113 | 114 | return res as StatusUrlResponse; 115 | } catch (err) { 116 | const errorMessage = `Failed to fetch status URL for sessionId: ${sessionId}`; 117 | logger.info(errorMessage, err); 118 | throw new StatusUrlError(`Error fetching status URL for sessionId: ${sessionId}`); 119 | } 120 | } 121 | 122 | 123 | -------------------------------------------------------------------------------- /src/utils/types.ts: -------------------------------------------------------------------------------- 1 | import type { Context, Proof, ProviderClaimData } from './interfaces'; 2 | 3 | // Claim-related types 4 | export type ClaimID = ProviderClaimData['identifier']; 5 | 6 | export type ClaimInfo = Pick; 7 | 8 | export type AnyClaimInfo = ClaimInfo | { identifier: ClaimID }; 9 | 10 | export type CompleteClaimData = Pick & AnyClaimInfo; 11 | 12 | export type SignedClaim = { 13 | claim: CompleteClaimData; 14 | signatures: Uint8Array[]; 15 | }; 16 | 17 | // Request and session-related types 18 | export type CreateVerificationRequest = { 19 | providerIds: string[]; 20 | applicationSecret?: string; 21 | }; 22 | 23 | export type StartSessionParams = { 24 | onSuccess: OnSuccess; 25 | onError: OnError; 26 | }; 27 | 28 | export type OnSuccess = (proof?: Proof | Proof[] | string) => void; 29 | export type OnError = (error: Error) => void; 30 | 31 | export type ProofRequestOptions = { 32 | log?: boolean; 33 | acceptAiProviders?: boolean; 34 | useAppClip?: boolean; 35 | device?: string; 36 | envUrl?: string; 37 | useBrowserExtension?: boolean; 38 | extensionID?: string; 39 | providerVersion?: string; 40 | customSharePageUrl?: string; 41 | customAppClipUrl?: string 42 | }; 43 | 44 | // Modal customization options 45 | export type ModalOptions = { 46 | title?: string; 47 | description?: string; 48 | extensionUrl?: string; 49 | darkTheme?: boolean; 50 | modalPopupTimer?: number; 51 | showExtensionInstallButton?: boolean; 52 | onClose?: () => void; 53 | }; 54 | 55 | // JSON-safe modal options (excludes non-serializable functions) 56 | export type SerializableModalOptions = Omit; 57 | 58 | // Claim creation type enum 59 | export enum ClaimCreationType { 60 | STANDALONE = 'createClaim', 61 | ON_ME_CHAIN = 'createClaimOnMechain' 62 | } 63 | 64 | // Device type enum 65 | export enum DeviceType { 66 | ANDROID = 'android', 67 | IOS = 'ios', 68 | DESKTOP = 'desktop', 69 | MOBILE = 'mobile' 70 | } 71 | 72 | 73 | // Session and response types 74 | export type InitSessionResponse = { 75 | sessionId: string; 76 | resolvedProviderVersion: string; 77 | }; 78 | 79 | export interface UpdateSessionResponse { 80 | success: boolean; 81 | message?: string; 82 | }; 83 | 84 | export enum SessionStatus { 85 | SESSION_INIT = 'SESSION_INIT', 86 | SESSION_STARTED = 'SESSION_STARTED', 87 | USER_INIT_VERIFICATION = 'USER_INIT_VERIFICATION', 88 | USER_STARTED_VERIFICATION = 'USER_STARTED_VERIFICATION', 89 | PROOF_GENERATION_STARTED = 'PROOF_GENERATION_STARTED', 90 | PROOF_GENERATION_SUCCESS = 'PROOF_GENERATION_SUCCESS', 91 | PROOF_GENERATION_FAILED = 'PROOF_GENERATION_FAILED', 92 | PROOF_SUBMITTED = 'PROOF_SUBMITTED', 93 | AI_PROOF_SUBMITTED = 'AI_PROOF_SUBMITTED', 94 | PROOF_SUBMISSION_FAILED = 'PROOF_SUBMISSION_FAILED', 95 | PROOF_MANUAL_VERIFICATION_SUBMITED = 'PROOF_MANUAL_VERIFICATION_SUBMITED', 96 | }; 97 | 98 | // JSON and template-related types 99 | export type ProofPropertiesJSON = { 100 | applicationId: string; 101 | providerId: string; 102 | sessionId: string; 103 | context: Context; 104 | signature: string; 105 | redirectUrl?: string; 106 | parameters: { [key: string]: string }; 107 | timeStamp: string; 108 | appCallbackUrl?: string; 109 | claimCreationType?: ClaimCreationType; 110 | options?: ProofRequestOptions; 111 | sdkVersion: string; 112 | jsonProofResponse?: boolean; 113 | resolvedProviderVersion: string; 114 | modalOptions?: SerializableModalOptions; 115 | }; 116 | 117 | export type TemplateData = { 118 | sessionId: string; 119 | providerId: string; 120 | applicationId: string; 121 | signature: string; 122 | timestamp: string; 123 | callbackUrl: string; 124 | context: string; 125 | parameters: { [key: string]: string }; 126 | redirectUrl: string; 127 | acceptAiProviders: boolean; 128 | sdkVersion: string; 129 | jsonProofResponse?: boolean; 130 | providerVersion?: string; 131 | resolvedProviderVersion: string; 132 | }; 133 | 134 | // Add the new StatusUrlResponse type 135 | export type StatusUrlResponse = { 136 | message: string; 137 | session?: { 138 | id: string; 139 | appId: string; 140 | httpProviderId: string[]; 141 | sessionId: string; 142 | proofs?: Proof[]; 143 | statusV2: string; 144 | }; 145 | providerId?: string; 146 | }; -------------------------------------------------------------------------------- /src/utils/validationUtils.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "ethers"; 2 | import { InavlidParametersError, InvalidParamError, InvalidSignatureError } from "./errors"; 3 | import canonicalize from 'canonicalize' 4 | import { Context } from "./interfaces"; 5 | import loggerModule from './logger'; 6 | import { ProofRequestOptions, ModalOptions } from "./types"; 7 | const logger = loggerModule.logger; 8 | 9 | /** 10 | * Validates function parameters based on specified criteria 11 | * @param params - An array of objects containing input, paramName, and optional isString flag 12 | * @param functionName - The name of the function being validated 13 | * @throws InvalidParamError if any parameter fails validation 14 | */ 15 | export function validateFunctionParams(params: { input: any, paramName: string, isString?: boolean }[], functionName: string): void { 16 | params.forEach(({ input, paramName, isString }) => { 17 | if (input == null) { 18 | logger.info(`Validation failed: ${paramName} in ${functionName} is null or undefined`); 19 | throw new InvalidParamError(`${paramName} passed to ${functionName} must not be null or undefined.`); 20 | } 21 | if (isString && typeof input !== 'string') { 22 | logger.info(`Validation failed: ${paramName} in ${functionName} is not a string`); 23 | throw new InvalidParamError(`${paramName} passed to ${functionName} must be a string.`); 24 | } 25 | if (isString && input.trim() === '') { 26 | logger.info(`Validation failed: ${paramName} in ${functionName} is an empty string`); 27 | throw new InvalidParamError(`${paramName} passed to ${functionName} must not be an empty string.`); 28 | } 29 | }); 30 | } 31 | 32 | 33 | // validate the parameters 34 | /** 35 | * Validates the parameters object 36 | * @param parameters - The parameters object to validate 37 | * @throws InavlidParametersError if the parameters object is not valid 38 | */ 39 | export function validateParameters(parameters: { [key: string]: string }): void { 40 | try { 41 | // check if the parameters is an object of key value pairs of string and string 42 | if (typeof parameters !== 'object' || parameters === null) { 43 | logger.info(`Parameters validation failed: Provided parameters is not an object`); 44 | throw new InavlidParametersError(`The provided parameters is not an object`); 45 | } 46 | // check each key and value in the parameters object 47 | for (const [key, value] of Object.entries(parameters)) { 48 | if (typeof key !== 'string' || typeof value !== 'string') { 49 | logger.info(`Parameters validation failed: Provided parameters is not an object of key value pairs of string and string`); 50 | throw new InavlidParametersError(`The provided parameters is not an object of key value pairs of string and string`); 51 | } 52 | } 53 | } catch (e) { 54 | logger.info(`Parameters validation failed: ${(e as Error).message}`); 55 | throw new InavlidParametersError(`Invalid parameters passed to validateParameters.`, e as Error); 56 | } 57 | } 58 | 59 | 60 | /** 61 | * Validates a URL string 62 | * @param url - The URL to validate 63 | * @param functionName - The name of the function calling this validation 64 | * @throws InvalidParamError if the URL is invalid or empty 65 | */ 66 | export function validateURL(url: string, functionName: string): void { 67 | try { 68 | new URL(url); 69 | } catch (e) { 70 | logger.info(`URL validation failed for ${url} in ${functionName}: ${(e as Error).message}`); 71 | throw new InvalidParamError(`Invalid URL format ${url} passed to ${functionName}.`, e as Error); 72 | } 73 | } 74 | 75 | /** 76 | * Validates a signature against the provided application ID 77 | * @param providerId - The ID of the provider 78 | * @param signature - The signature to validate 79 | * @param applicationId - The expected application ID 80 | * @param timestamp - The timestamp of the signature 81 | * @throws InvalidSignatureError if the signature is invalid or doesn't match the application ID 82 | */ 83 | export function validateSignature(providerId: string, signature: string, applicationId: string, timestamp: string): void { 84 | try { 85 | logger.info(`Starting signature validation for providerId: ${providerId}, applicationId: ${applicationId}, timestamp: ${timestamp}`); 86 | 87 | const message = canonicalize({ providerId, timestamp }); 88 | if (!message) { 89 | logger.info('Failed to canonicalize message for signature validation'); 90 | throw new Error('Failed to canonicalize message'); 91 | } 92 | const messageHash = ethers.keccak256(new TextEncoder().encode(message)); 93 | let appId = ethers.verifyMessage( 94 | ethers.getBytes(messageHash), 95 | ethers.hexlify(signature) 96 | ).toLowerCase(); 97 | 98 | if (ethers.getAddress(appId) !== ethers.getAddress(applicationId)) { 99 | logger.info(`Signature validation failed: Mismatch between derived appId (${appId}) and provided applicationId (${applicationId})`); 100 | throw new InvalidSignatureError(`Signature does not match the application id: ${appId}`); 101 | } 102 | 103 | logger.info(`Signature validated successfully for applicationId: ${applicationId}`); 104 | } catch (err) { 105 | logger.info(`Signature validation failed: ${(err as Error).message}`); 106 | if (err instanceof InvalidSignatureError) { 107 | throw err; 108 | } 109 | throw new InvalidSignatureError(`Failed to validate signature: ${(err as Error).message}`); 110 | } 111 | } 112 | 113 | 114 | /** 115 | * Validates the context object 116 | * @param context - The context object to validate 117 | * @throws InvalidParamError if the context object is not valid 118 | */ 119 | export function validateContext(context: Context): void { 120 | if (!context.contextAddress) { 121 | logger.info(`Context validation failed: Provided context address in context is not valid`); 122 | throw new InvalidParamError(`The provided context address in context is not valid`); 123 | } 124 | 125 | if (!context.contextMessage) { 126 | logger.info(`Context validation failed: Provided context message in context is not valid`); 127 | throw new InvalidParamError(`The provided context message in context is not valid`); 128 | } 129 | 130 | validateFunctionParams([ 131 | { input: context.contextAddress, paramName: 'contextAddress', isString: true }, 132 | { input: context.contextMessage, paramName: 'contextMessage', isString: true } 133 | ], 'validateContext'); 134 | } 135 | 136 | /** 137 | * Validates the options object 138 | * @param options - The options object to validate 139 | * @throws InvalidParamError if the options object is not valid 140 | */ 141 | export function validateOptions(options: ProofRequestOptions): void { 142 | if (options.acceptAiProviders && typeof options.acceptAiProviders !== 'boolean') { 143 | logger.info(`Options validation failed: Provided acceptAiProviders in options is not valid`); 144 | throw new InvalidParamError(`The provided acceptAiProviders in options is not valid`); 145 | } 146 | 147 | if (options.log && typeof options.log !== 'boolean') { 148 | logger.info(`Options validation failed: Provided log in options is not valid`); 149 | throw new InvalidParamError(`The provided log in options is not valid`); 150 | } 151 | 152 | if (options.providerVersion && typeof options.providerVersion !== 'string') { 153 | logger.info(`Options validation failed: Provided providerVersion in options is not valid`); 154 | throw new InvalidParamError(`The provided providerVersion in options is not valid`); 155 | } 156 | } 157 | 158 | /** 159 | * Validates the modalOptions object 160 | * @param modalOptions - The modalOptions object to validate 161 | * @param functionName - The name of the function calling this validation 162 | * @param paramPrefix - Optional prefix for parameter names (e.g., 'modalOptions.') 163 | * @throws InvalidParamError if the modalOptions object is not valid 164 | */ 165 | export function validateModalOptions(modalOptions: ModalOptions, functionName: string, paramPrefix: string = ''): void { 166 | if (modalOptions.title !== undefined) { 167 | validateFunctionParams([ 168 | { input: modalOptions.title, paramName: `${paramPrefix}title`, isString: true } 169 | ], functionName); 170 | } 171 | 172 | if (modalOptions.description !== undefined) { 173 | validateFunctionParams([ 174 | { input: modalOptions.description, paramName: `${paramPrefix}description`, isString: true } 175 | ], functionName); 176 | } 177 | 178 | if (modalOptions.extensionUrl !== undefined) { 179 | validateURL(modalOptions.extensionUrl, functionName); 180 | validateFunctionParams([ 181 | { input: modalOptions.extensionUrl, paramName: `${paramPrefix}extensionUrl`, isString: true } 182 | ], functionName); 183 | } 184 | 185 | if (modalOptions.darkTheme !== undefined) { 186 | if (typeof modalOptions.darkTheme !== 'boolean') { 187 | throw new InvalidParamError(`${paramPrefix}darkTheme prop must be a boolean`); 188 | } 189 | validateFunctionParams([ 190 | { input: modalOptions.darkTheme, paramName: `${paramPrefix}darkTheme` } 191 | ], functionName); 192 | } 193 | 194 | if (modalOptions.modalPopupTimer !== undefined) { 195 | if (typeof modalOptions.modalPopupTimer !== 'number' || modalOptions.modalPopupTimer <= 0 || !Number.isInteger(modalOptions.modalPopupTimer)) { 196 | throw new InvalidParamError(`${paramPrefix}modalPopupTimer prop must be a valid time in minutes`); 197 | } 198 | validateFunctionParams([ 199 | { input: modalOptions.modalPopupTimer, paramName: `${paramPrefix}modalPopupTimer` } 200 | ], functionName); 201 | } 202 | 203 | if (modalOptions.showExtensionInstallButton !== undefined) { 204 | if (typeof modalOptions.showExtensionInstallButton !== 'boolean') { 205 | throw new InvalidParamError(`${paramPrefix}showExtensionInstallButton prop must be a boolean`); 206 | } 207 | validateFunctionParams([ 208 | { input: modalOptions.showExtensionInstallButton, paramName: `${paramPrefix}showExtensionInstallButton` } 209 | ], functionName); 210 | } 211 | } 212 | 213 | 214 | 215 | -------------------------------------------------------------------------------- /src/witness.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from 'ethers'; 2 | import type { WitnessData } from './utils/interfaces'; 3 | import type { ClaimID, ClaimInfo, CompleteClaimData } from './utils/types'; 4 | 5 | type BeaconState = { 6 | witnesses: WitnessData[]; 7 | epoch: number; 8 | witnessesRequiredForClaim: number; 9 | nextEpochTimestampS: number; 10 | }; 11 | 12 | export function fetchWitnessListForClaim( 13 | { witnesses, witnessesRequiredForClaim, epoch }: BeaconState, 14 | params: string | ClaimInfo, 15 | timestampS: number 16 | ): WitnessData[] { 17 | const identifier: ClaimID = 18 | typeof params === 'string' ? params : getIdentifierFromClaimInfo(params); 19 | const completeInput: string = [ 20 | identifier, 21 | epoch.toString(), 22 | witnessesRequiredForClaim.toString(), 23 | timestampS.toString(), 24 | ].join('\n'); 25 | const completeHashStr: string = ethers.keccak256(strToUint8Array(completeInput)); 26 | const completeHash: Uint8Array = ethers.getBytes(completeHashStr); 27 | const completeHashView: DataView = uint8ArrayToDataView(completeHash); 28 | const witnessesLeft: WitnessData[] = [...witnesses]; 29 | const selectedWitnesses: WitnessData[] = []; 30 | let byteOffset: number = 0; 31 | for (let i = 0; i < witnessesRequiredForClaim; i++) { 32 | const randomSeed: number = completeHashView.getUint32(byteOffset); 33 | const witnessIndex: number = randomSeed % witnessesLeft.length; 34 | const witness: WitnessData = witnessesLeft[witnessIndex]; 35 | selectedWitnesses.push(witness); 36 | 37 | witnessesLeft[witnessIndex] = witnessesLeft[witnessesLeft.length - 1]; 38 | witnessesLeft.pop(); 39 | byteOffset = (byteOffset + 4) % completeHash.length; 40 | } 41 | 42 | return selectedWitnesses; 43 | } 44 | 45 | export function getIdentifierFromClaimInfo(info: ClaimInfo): ClaimID { 46 | const str: string = `${info.provider}\n${info.parameters}\n${info.context || ''}`; 47 | return ethers.keccak256(strToUint8Array(str)).toLowerCase(); 48 | } 49 | 50 | export function strToUint8Array(str: string): Uint8Array { 51 | return new TextEncoder().encode(str); 52 | } 53 | 54 | export function uint8ArrayToDataView(arr: Uint8Array): DataView { 55 | return new DataView(arr.buffer, arr.byteOffset, arr.byteLength); 56 | } 57 | 58 | export function createSignDataForClaim(data: CompleteClaimData): string { 59 | const identifier: ClaimID = 60 | 'identifier' in data ? data.identifier : getIdentifierFromClaimInfo(data); 61 | const lines: string[] = [ 62 | identifier, 63 | data.owner.toLowerCase(), 64 | data.timestampS.toString(), 65 | data.epoch.toString(), 66 | ]; 67 | 68 | return lines.join('\n'); 69 | } 70 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | "outDir": "./dist", 5 | "declaration": true, 6 | "emitDeclarationOnly": false, 7 | "declarationMap": true, 8 | 9 | /* Language and Environment */ 10 | "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, 11 | 12 | /* Modules */ 13 | "module": "commonjs" /* Specify what module code is generated. */, 14 | "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, 15 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 16 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, 17 | "resolveJsonModule": true, 18 | /* Type Checking */ 19 | "strict": true /* Enable all strict type-checking options. */, 20 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 21 | }, 22 | "exclude": ["example"] 23 | } 24 | --------------------------------------------------------------------------------