Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Change log

## 1.1.0

* Added: `Account.createRecoveryOTP` and `Account.updateRecoveryOTP` for code-based password recovery
* Updated: SDK now targets Appwrite 2.3 (`X-Appwrite-Response-Format: 2.3.0`)
* Updated: requests always send credentials now that dev keys are no longer supported

## 1.0.0

* Stable: first stable release of the React Native SDK, out of beta
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# Appwrite React Native SDK

![License](https://img.shields.io/github/license/appwrite/sdk-for-react-native.svg?style=flat-square)
![Version](https://img.shields.io/badge/api%20version-2.2.0-blue.svg?style=flat-square)
![Version](https://img.shields.io/badge/api%20version-2.3.0-blue.svg?style=flat-square)
[![Build Status](https://img.shields.io/travis/com/appwrite/sdk-generator?style=flat-square)](https://travis-ci.com/appwrite/sdk-generator)
[![Twitter Account](https://img.shields.io/twitter/follow/appwrite?color=00acee&label=twitter&style=flat-square)](https://twitter.com/appwrite)
[![Discord](https://img.shields.io/discord/564160730845151244?label=discord&style=flat-square)](https://appwrite.io/discord)

**This SDK targets Appwrite server version 2.2.x as shipped on Appwrite Cloud.** Self-hosted releases can lag behind Cloud — if you run an older self-hosted build, use a matching older SDK from [previous releases](https://github.com/appwrite/sdk-for-react-native/releases) when APIs differ.
**This SDK targets Appwrite server version 2.3.x as shipped on Appwrite Cloud.** Self-hosted releases can lag behind Cloud — if you run an older self-hosted build, use a matching older SDK from [previous releases](https://github.com/appwrite/sdk-for-react-native/releases) when APIs differ.

Appwrite is an open-source backend as a service server that abstracts and simplifies complex and repetitive development tasks behind a very simple to use REST API. Appwrite aims to help you develop your apps faster and in a more secure way. Use the React Native SDK to integrate your app with the Appwrite server to easily start interacting with all of Appwrite backend APIs and tools. For full API documentation and tutorials go to [https://appwrite.io/docs](https://appwrite.io/docs)

Expand Down
16 changes: 16 additions & 0 deletions docs/examples/account/create-recovery-otp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
```javascript
import { Client, Account } from 'react-native-appwrite';

const client = new Client()
.setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
.setProject('<YOUR_PROJECT_ID>'); // Your project ID

const account = new Account(client);

const result = await account.createRecoveryOTP({
email: 'email@example.com',
phrase: false, // optional
});

console.log(result);
```
17 changes: 17 additions & 0 deletions docs/examples/account/update-recovery-otp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
```javascript
import { Client, Account } from 'react-native-appwrite';

const client = new Client()
.setEndpoint('https://<REGION>.cloud.appwrite.io/v1') // Your API Endpoint
.setProject('<YOUR_PROJECT_ID>'); // Your project ID

const account = new Account(client);

const result = await account.updateRecoveryOTP({
userId: '<USER_ID>',
secret: '<SECRET>',
password: 'password',
});

console.log(result);
```
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "react-native-appwrite",
"homepage": "https://appwrite.io/support",
"description": "Appwrite is an open-source self-hosted backend server that abstracts and simplifies complex and repetitive development tasks behind a very simple REST API",
"version": "1.0.0",
"version": "1.1.0",
"license": "BSD-3-Clause",
"main": "dist/cjs/sdk.js",
"exports": {
Expand Down
9 changes: 3 additions & 6 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -208,8 +208,8 @@ class Client {
'x-sdk-name': 'React Native',
'x-sdk-platform': 'client',
'x-sdk-language': 'reactnative',
'x-sdk-version': '1.0.0',
'X-Appwrite-Response-Format': '2.2.0',
'x-sdk-version': '1.1.0',
'X-Appwrite-Response-Format': '2.3.0',
};

/**
Expand Down Expand Up @@ -787,12 +787,9 @@ class Client {
const options: RequestInit = {
method,
headers,
credentials: 'include',
};

if (headers['X-Appwrite-Dev-Key'] === undefined) {
options.credentials = 'include';
}

if (method === 'GET') {
for (const [key, value] of Object.entries(
Service.flatten(params),
Expand Down
187 changes: 185 additions & 2 deletions src/services/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2358,6 +2358,189 @@ export class Account extends Service {
);
}

/**
* Use this endpoint to send a 6-digit password recovery code to the user's email address. Unlike [createRecovery](https://appwrite.io/docs/references/cloud/client-web/account#createRecovery), this method requires no redirect URL, which makes it suitable for mobile and desktop apps that cannot host a recovery page. Learn more about how to [complete the recovery process](https://appwrite.io/docs/references/cloud/client-web/account#updateRecoveryOTP). The code sent to the user's email address is valid for 15 minutes.
*
* Enable the **phrase** parameter to include a randomly generated security phrase in both the email and the response. Showing that phrase in your app lets the user confirm the email genuinely came from your request, which helps protect against phishing.
*
*
* @param {string} params.email - User email.
* @param {boolean} params.phrase - Toggle for security phrase. If enabled, email will be sent with a randomly generated phrase and the phrase will also be included in the response. Confirming phrases match increases the security of your authentication flow.
* @throws {AppwriteException}
* @returns {Promise}
*/
createRecoveryOTP(params: {
email: string;
phrase?: boolean;
}): Promise<Models.Token>;
/**
* Use this endpoint to send a 6-digit password recovery code to the user's email address. Unlike [createRecovery](https://appwrite.io/docs/references/cloud/client-web/account#createRecovery), this method requires no redirect URL, which makes it suitable for mobile and desktop apps that cannot host a recovery page. Learn more about how to [complete the recovery process](https://appwrite.io/docs/references/cloud/client-web/account#updateRecoveryOTP). The code sent to the user's email address is valid for 15 minutes.
*
* Enable the **phrase** parameter to include a randomly generated security phrase in both the email and the response. Showing that phrase in your app lets the user confirm the email genuinely came from your request, which helps protect against phishing.
*
*
* @param {string} email - User email.
* @param {boolean} phrase - Toggle for security phrase. If enabled, email will be sent with a randomly generated phrase and the phrase will also be included in the response. Confirming phrases match increases the security of your authentication flow.
* @throws {AppwriteException}
* @returns {Promise<Models.Token>}
* @deprecated Use the object parameter style method for a better developer experience.
*/
createRecoveryOTP(email: string, phrase?: boolean): Promise<Models.Token>;
createRecoveryOTP(
paramsOrFirst: { email: string; phrase?: boolean } | string,
...rest: [boolean?]
): Promise<Models.Token> {
let params: { email: string; phrase?: boolean };

if (
paramsOrFirst &&
typeof paramsOrFirst === 'object' &&
!Array.isArray(paramsOrFirst)
) {
params = (paramsOrFirst || {}) as {
email: string;
phrase?: boolean;
};
} else {
params = {
email: paramsOrFirst as string,
phrase: rest[0] as boolean,
};
}

const email = params.email;
const phrase = params.phrase;

if (typeof email === 'undefined') {
throw new AppwriteException('Missing required parameter: "email"');
}

const apiPath = '/account/recovery/otp';
const payload: Payload = {};

if (typeof email !== 'undefined') {
payload['email'] = email;
}

if (typeof phrase !== 'undefined') {
payload['phrase'] = phrase;
}

const uri = new URL(this.client.config.endpoint + apiPath);
return this.client.call(
'post',
uri,
{
'X-Appwrite-Project': this.client.config.project,
'content-type': 'application/json',
accept: 'application/json',
},
payload,
);
Comment on lines +2418 to +2439

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Recovery flow lacks tests

The new OTP initiation and completion requests have no observable-behavior tests. A regression in either step could go unnoticed until a user cannot reset their password. Please test successful recovery and rejected or expired codes through the SDK, rather than asserting that source code, headers, or version strings match fixed values.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/services/account.ts
Line: 2418-2439

Comment:
**Recovery flow lacks tests**

The new OTP initiation and completion requests have no observable-behavior tests. A regression in either step could go unnoticed until a user cannot reset their password. Please test successful recovery and rejected or expired codes through the SDK, rather than asserting that source code, headers, or version strings match fixed values.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code Fix in Codex

}

/**
* Use this endpoint to complete the user password recovery process using the 6-digit code that was emailed by [createRecoveryOTP](https://appwrite.io/docs/references/cloud/client-web/account#createRecoveryOTP). Pass the **userId** of the user along with the **secret** code from the email and the new **password** to set. If confirmed, this route will return a 200 status code, the code is consumed and the user's password is updated.
*
*
* @param {string} params.userId - User ID.
* @param {string} params.secret - Valid recovery OTP code.
* @param {string} params.password - New user password. Must be between 8 and 256 chars.
* @throws {AppwriteException}
* @returns {Promise}
*/
updateRecoveryOTP(params: {
userId: string;
secret: string;
password: string;
}): Promise<Models.Token>;
/**
* Use this endpoint to complete the user password recovery process using the 6-digit code that was emailed by [createRecoveryOTP](https://appwrite.io/docs/references/cloud/client-web/account#createRecoveryOTP). Pass the **userId** of the user along with the **secret** code from the email and the new **password** to set. If confirmed, this route will return a 200 status code, the code is consumed and the user's password is updated.
*
*
* @param {string} userId - User ID.
* @param {string} secret - Valid recovery OTP code.
* @param {string} password - New user password. Must be between 8 and 256 chars.
* @throws {AppwriteException}
* @returns {Promise<Models.Token>}
* @deprecated Use the object parameter style method for a better developer experience.
*/
updateRecoveryOTP(
userId: string,
secret: string,
password: string,
): Promise<Models.Token>;
updateRecoveryOTP(
paramsOrFirst:
{ userId: string; secret: string; password: string } | string,
...rest: [string?, string?]
): Promise<Models.Token> {
let params: { userId: string; secret: string; password: string };

if (
paramsOrFirst &&
typeof paramsOrFirst === 'object' &&
!Array.isArray(paramsOrFirst)
) {
params = (paramsOrFirst || {}) as {
userId: string;
secret: string;
password: string;
};
} else {
params = {
userId: paramsOrFirst as string,
secret: rest[0] as string,
password: rest[1] as string,
};
}

const userId = params.userId;
const secret = params.secret;
const password = params.password;

if (typeof userId === 'undefined') {
throw new AppwriteException('Missing required parameter: "userId"');
}

if (typeof secret === 'undefined') {
throw new AppwriteException('Missing required parameter: "secret"');
}

if (typeof password === 'undefined') {
throw new AppwriteException(
'Missing required parameter: "password"',
);
}

const apiPath = '/account/recovery/otp';
const payload: Payload = {};

if (typeof userId !== 'undefined') {
payload['userId'] = userId;
}

if (typeof secret !== 'undefined') {
payload['secret'] = secret;
}

if (typeof password !== 'undefined') {
payload['password'] = password;
}

const uri = new URL(this.client.config.endpoint + apiPath);
return this.client.call(
'put',
uri,
{
'X-Appwrite-Project': this.client.config.project,
'content-type': 'application/json',
accept: 'application/json',
},
payload,
);
}

/**
* Get the list of active sessions across different devices for the currently logged in user.
*
Expand Down Expand Up @@ -3259,7 +3442,7 @@ export class Account extends Service {
}

/**
* Use this endpoint to register a device for push notifications. Provide a target ID (custom or generated using ID.unique()), a device identifier (usually a device token), and optionally specify which provider should send notifications to this target. The target is automatically linked to the current session and includes device information like brand and model.
* Use this endpoint to register a device for push notifications. Provide a target ID (custom or generated using ID.unique()), a device identifier (usually a device token), and optionally specify which provider should send notifications to this target. The target is automatically linked to the current session and includes device information like brand and model. A session holds one push target per provider, so if one already exists this endpoint updates and returns that target instead of creating a second one, and a device that rotates its token is never notified twice.
*
* @param {string} params.targetId - Target ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can't start with a special char. Max length is 36 chars.
* @param {string} params.identifier - The target identifier (token, email, phone etc.)
Expand All @@ -3273,7 +3456,7 @@ export class Account extends Service {
providerId?: string;
}): Promise<Models.Target>;
/**
* Use this endpoint to register a device for push notifications. Provide a target ID (custom or generated using ID.unique()), a device identifier (usually a device token), and optionally specify which provider should send notifications to this target. The target is automatically linked to the current session and includes device information like brand and model.
* Use this endpoint to register a device for push notifications. Provide a target ID (custom or generated using ID.unique()), a device identifier (usually a device token), and optionally specify which provider should send notifications to this target. The target is automatically linked to the current session and includes device information like brand and model. A session holds one push target per provider, so if one already exists this endpoint updates and returns that target instead of creating a second one, and a device that rotates its token is never notified twice.
*
* @param {string} targetId - Target ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can't start with a special char. Max length is 36 chars.
* @param {string} identifier - The target identifier (token, email, phone etc.)
Expand Down