@capawesome/capacitor-passkeys 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/CapawesomeCapacitorPasskeys.podspec +17 -0
  2. package/LICENSE +21 -0
  3. package/Package.swift +28 -0
  4. package/README.md +472 -0
  5. package/android/build.gradle +61 -0
  6. package/android/src/main/AndroidManifest.xml +2 -0
  7. package/android/src/main/java/io/capawesome/capacitorjs/plugins/passkeys/Passkeys.java +173 -0
  8. package/android/src/main/java/io/capawesome/capacitorjs/plugins/passkeys/PasskeysPlugin.java +115 -0
  9. package/android/src/main/java/io/capawesome/capacitorjs/plugins/passkeys/classes/CustomException.java +20 -0
  10. package/android/src/main/java/io/capawesome/capacitorjs/plugins/passkeys/classes/CustomExceptions.java +12 -0
  11. package/android/src/main/java/io/capawesome/capacitorjs/plugins/passkeys/classes/options/CreatePasskeyOptions.java +38 -0
  12. package/android/src/main/java/io/capawesome/capacitorjs/plugins/passkeys/classes/options/GetPasskeyOptions.java +33 -0
  13. package/android/src/main/java/io/capawesome/capacitorjs/plugins/passkeys/classes/results/CreatePasskeyResult.java +22 -0
  14. package/android/src/main/java/io/capawesome/capacitorjs/plugins/passkeys/classes/results/GetPasskeyResult.java +22 -0
  15. package/android/src/main/java/io/capawesome/capacitorjs/plugins/passkeys/classes/results/IsAvailableResult.java +22 -0
  16. package/android/src/main/java/io/capawesome/capacitorjs/plugins/passkeys/interfaces/Callback.java +5 -0
  17. package/android/src/main/java/io/capawesome/capacitorjs/plugins/passkeys/interfaces/NonEmptyResultCallback.java +7 -0
  18. package/android/src/main/java/io/capawesome/capacitorjs/plugins/passkeys/interfaces/Result.java +7 -0
  19. package/android/src/main/res/.gitkeep +0 -0
  20. package/dist/docs.json +1091 -0
  21. package/dist/esm/definitions.d.ts +567 -0
  22. package/dist/esm/definitions.js +47 -0
  23. package/dist/esm/definitions.js.map +1 -0
  24. package/dist/esm/index.d.ts +4 -0
  25. package/dist/esm/index.js +7 -0
  26. package/dist/esm/index.js.map +1 -0
  27. package/dist/esm/web.d.ts +18 -0
  28. package/dist/esm/web.js +184 -0
  29. package/dist/esm/web.js.map +1 -0
  30. package/dist/plugin.cjs.js +244 -0
  31. package/dist/plugin.cjs.js.map +1 -0
  32. package/dist/plugin.js +247 -0
  33. package/dist/plugin.js.map +1 -0
  34. package/ios/Plugin/Classes/Options/CreatePasskeyOptions.swift +81 -0
  35. package/ios/Plugin/Classes/Options/GetPasskeyOptions.swift +54 -0
  36. package/ios/Plugin/Classes/Results/CreatePasskeyResult.swift +28 -0
  37. package/ios/Plugin/Classes/Results/GetPasskeyResult.swift +34 -0
  38. package/ios/Plugin/Classes/Results/IsAvailableResult.swift +16 -0
  39. package/ios/Plugin/Enums/CustomError.swift +65 -0
  40. package/ios/Plugin/Info.plist +24 -0
  41. package/ios/Plugin/Passkeys.swift +118 -0
  42. package/ios/Plugin/PasskeysHelper.swift +20 -0
  43. package/ios/Plugin/PasskeysPlugin.swift +86 -0
  44. package/ios/Plugin/Protocols/Result.swift +5 -0
  45. package/package.json +91 -0
@@ -0,0 +1,173 @@
1
+ package io.capawesome.capacitorjs.plugins.passkeys;
2
+
3
+ import android.os.Build;
4
+ import androidx.annotation.NonNull;
5
+ import androidx.credentials.CreateCredentialResponse;
6
+ import androidx.credentials.CreatePublicKeyCredentialRequest;
7
+ import androidx.credentials.CreatePublicKeyCredentialResponse;
8
+ import androidx.credentials.CredentialManager;
9
+ import androidx.credentials.CredentialManagerCallback;
10
+ import androidx.credentials.GetCredentialRequest;
11
+ import androidx.credentials.GetCredentialResponse;
12
+ import androidx.credentials.GetPublicKeyCredentialOption;
13
+ import androidx.credentials.PublicKeyCredential;
14
+ import androidx.credentials.exceptions.CreateCredentialCancellationException;
15
+ import androidx.credentials.exceptions.CreateCredentialException;
16
+ import androidx.credentials.exceptions.CreateCredentialProviderConfigurationException;
17
+ import androidx.credentials.exceptions.CreateCredentialUnsupportedException;
18
+ import androidx.credentials.exceptions.GetCredentialCancellationException;
19
+ import androidx.credentials.exceptions.GetCredentialException;
20
+ import androidx.credentials.exceptions.GetCredentialProviderConfigurationException;
21
+ import androidx.credentials.exceptions.GetCredentialUnsupportedException;
22
+ import androidx.credentials.exceptions.NoCredentialException;
23
+ import androidx.credentials.exceptions.domerrors.SecurityError;
24
+ import androidx.credentials.exceptions.publickeycredential.CreatePublicKeyCredentialDomException;
25
+ import androidx.credentials.exceptions.publickeycredential.GetPublicKeyCredentialDomException;
26
+ import io.capawesome.capacitorjs.plugins.passkeys.classes.CustomException;
27
+ import io.capawesome.capacitorjs.plugins.passkeys.classes.CustomExceptions;
28
+ import io.capawesome.capacitorjs.plugins.passkeys.classes.options.CreatePasskeyOptions;
29
+ import io.capawesome.capacitorjs.plugins.passkeys.classes.options.GetPasskeyOptions;
30
+ import io.capawesome.capacitorjs.plugins.passkeys.classes.results.CreatePasskeyResult;
31
+ import io.capawesome.capacitorjs.plugins.passkeys.classes.results.GetPasskeyResult;
32
+ import io.capawesome.capacitorjs.plugins.passkeys.classes.results.IsAvailableResult;
33
+ import io.capawesome.capacitorjs.plugins.passkeys.interfaces.NonEmptyResultCallback;
34
+
35
+ public class Passkeys {
36
+
37
+ @NonNull
38
+ private final PasskeysPlugin plugin;
39
+
40
+ public Passkeys(@NonNull PasskeysPlugin plugin) {
41
+ this.plugin = plugin;
42
+ }
43
+
44
+ public void createPasskey(@NonNull CreatePasskeyOptions options, @NonNull NonEmptyResultCallback<CreatePasskeyResult> callback) {
45
+ CreatePublicKeyCredentialRequest request = new CreatePublicKeyCredentialRequest(options.getRequestJson());
46
+ CredentialManager credentialManager = CredentialManager.create(plugin.getContext());
47
+ credentialManager.createCredentialAsync(
48
+ plugin.getActivity(),
49
+ request,
50
+ null,
51
+ Runnable::run,
52
+ new CredentialManagerCallback<CreateCredentialResponse, CreateCredentialException>() {
53
+ @Override
54
+ public void onResult(@NonNull CreateCredentialResponse response) {
55
+ handleCreateCredentialResponse(response, callback);
56
+ }
57
+
58
+ @Override
59
+ public void onError(@NonNull CreateCredentialException exception) {
60
+ callback.error(mapCreateCredentialException(exception));
61
+ }
62
+ }
63
+ );
64
+ }
65
+
66
+ public void getPasskey(@NonNull GetPasskeyOptions options, @NonNull NonEmptyResultCallback<GetPasskeyResult> callback) {
67
+ GetPublicKeyCredentialOption option = new GetPublicKeyCredentialOption(options.getRequestJson());
68
+ GetCredentialRequest request = new GetCredentialRequest.Builder().addCredentialOption(option).build();
69
+ CredentialManager credentialManager = CredentialManager.create(plugin.getContext());
70
+ credentialManager.getCredentialAsync(
71
+ plugin.getActivity(),
72
+ request,
73
+ null,
74
+ Runnable::run,
75
+ new CredentialManagerCallback<GetCredentialResponse, GetCredentialException>() {
76
+ @Override
77
+ public void onResult(@NonNull GetCredentialResponse response) {
78
+ handleGetCredentialResponse(response, callback);
79
+ }
80
+
81
+ @Override
82
+ public void onError(@NonNull GetCredentialException exception) {
83
+ callback.error(mapGetCredentialException(exception));
84
+ }
85
+ }
86
+ );
87
+ }
88
+
89
+ public void isAvailable(@NonNull NonEmptyResultCallback<IsAvailableResult> callback) {
90
+ boolean available = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P;
91
+ IsAvailableResult result = new IsAvailableResult(available);
92
+ callback.success(result);
93
+ }
94
+
95
+ private void handleCreateCredentialResponse(
96
+ @NonNull CreateCredentialResponse response,
97
+ @NonNull NonEmptyResultCallback<CreatePasskeyResult> callback
98
+ ) {
99
+ if (response instanceof CreatePublicKeyCredentialResponse) {
100
+ try {
101
+ String registrationResponseJson = ((CreatePublicKeyCredentialResponse) response).getRegistrationResponseJson();
102
+ CreatePasskeyResult result = new CreatePasskeyResult(registrationResponseJson);
103
+ callback.success(result);
104
+ } catch (Exception exception) {
105
+ callback.error(exception);
106
+ }
107
+ } else {
108
+ callback.error(CustomExceptions.CREATE_FAILED);
109
+ }
110
+ }
111
+
112
+ private void handleGetCredentialResponse(
113
+ @NonNull GetCredentialResponse response,
114
+ @NonNull NonEmptyResultCallback<GetPasskeyResult> callback
115
+ ) {
116
+ if (response.getCredential() instanceof PublicKeyCredential) {
117
+ try {
118
+ String authenticationResponseJson = ((PublicKeyCredential) response.getCredential()).getAuthenticationResponseJson();
119
+ GetPasskeyResult result = new GetPasskeyResult(authenticationResponseJson);
120
+ callback.success(result);
121
+ } catch (Exception exception) {
122
+ callback.error(exception);
123
+ }
124
+ } else {
125
+ callback.error(CustomExceptions.GET_FAILED);
126
+ }
127
+ }
128
+
129
+ private static Exception mapCreateCredentialException(@NonNull CreateCredentialException exception) {
130
+ if (exception instanceof CreateCredentialCancellationException) {
131
+ return CustomExceptions.OPERATION_CANCELED;
132
+ }
133
+ String message = exception.getMessage();
134
+ if (message == null) {
135
+ message = CustomExceptions.CREATE_FAILED.getMessage();
136
+ }
137
+ if (
138
+ exception instanceof CreatePublicKeyCredentialDomException &&
139
+ ((CreatePublicKeyCredentialDomException) exception).getDomError() instanceof SecurityError
140
+ ) {
141
+ return new CustomException("DOMAIN_NOT_ASSOCIATED", message);
142
+ }
143
+ if (
144
+ exception instanceof CreateCredentialProviderConfigurationException || exception instanceof CreateCredentialUnsupportedException
145
+ ) {
146
+ return new CustomException("NOT_SUPPORTED", message);
147
+ }
148
+ return new CustomException("CREATE_FAILED", message);
149
+ }
150
+
151
+ private static Exception mapGetCredentialException(@NonNull GetCredentialException exception) {
152
+ if (exception instanceof GetCredentialCancellationException) {
153
+ return CustomExceptions.OPERATION_CANCELED;
154
+ }
155
+ String message = exception.getMessage();
156
+ if (message == null) {
157
+ message = CustomExceptions.GET_FAILED.getMessage();
158
+ }
159
+ if (exception instanceof NoCredentialException) {
160
+ return new CustomException("NO_CREDENTIAL", message);
161
+ }
162
+ if (
163
+ exception instanceof GetPublicKeyCredentialDomException &&
164
+ ((GetPublicKeyCredentialDomException) exception).getDomError() instanceof SecurityError
165
+ ) {
166
+ return new CustomException("DOMAIN_NOT_ASSOCIATED", message);
167
+ }
168
+ if (exception instanceof GetCredentialProviderConfigurationException || exception instanceof GetCredentialUnsupportedException) {
169
+ return new CustomException("NOT_SUPPORTED", message);
170
+ }
171
+ return new CustomException("GET_FAILED", message);
172
+ }
173
+ }
@@ -0,0 +1,115 @@
1
+ package io.capawesome.capacitorjs.plugins.passkeys;
2
+
3
+ import androidx.annotation.NonNull;
4
+ import androidx.annotation.Nullable;
5
+ import com.getcapacitor.Logger;
6
+ import com.getcapacitor.Plugin;
7
+ import com.getcapacitor.PluginCall;
8
+ import com.getcapacitor.PluginMethod;
9
+ import com.getcapacitor.annotation.CapacitorPlugin;
10
+ import io.capawesome.capacitorjs.plugins.passkeys.classes.CustomException;
11
+ import io.capawesome.capacitorjs.plugins.passkeys.classes.options.CreatePasskeyOptions;
12
+ import io.capawesome.capacitorjs.plugins.passkeys.classes.options.GetPasskeyOptions;
13
+ import io.capawesome.capacitorjs.plugins.passkeys.classes.results.CreatePasskeyResult;
14
+ import io.capawesome.capacitorjs.plugins.passkeys.classes.results.GetPasskeyResult;
15
+ import io.capawesome.capacitorjs.plugins.passkeys.classes.results.IsAvailableResult;
16
+ import io.capawesome.capacitorjs.plugins.passkeys.interfaces.NonEmptyResultCallback;
17
+ import io.capawesome.capacitorjs.plugins.passkeys.interfaces.Result;
18
+
19
+ @CapacitorPlugin(name = "Passkeys")
20
+ public class PasskeysPlugin extends Plugin {
21
+
22
+ public static final String ERROR_UNKNOWN_ERROR = "An unknown error has occurred.";
23
+ public static final String TAG = "PasskeysPlugin";
24
+
25
+ private Passkeys implementation;
26
+
27
+ @Override
28
+ public void load() {
29
+ super.load();
30
+ this.implementation = new Passkeys(this);
31
+ }
32
+
33
+ @PluginMethod
34
+ public void createPasskey(PluginCall call) {
35
+ try {
36
+ CreatePasskeyOptions options = new CreatePasskeyOptions(call);
37
+ NonEmptyResultCallback<CreatePasskeyResult> callback = new NonEmptyResultCallback<>() {
38
+ @Override
39
+ public void success(@NonNull CreatePasskeyResult result) {
40
+ resolveCall(call, result);
41
+ }
42
+
43
+ @Override
44
+ public void error(Exception exception) {
45
+ rejectCall(call, exception);
46
+ }
47
+ };
48
+ implementation.createPasskey(options, callback);
49
+ } catch (Exception exception) {
50
+ rejectCall(call, exception);
51
+ }
52
+ }
53
+
54
+ @PluginMethod
55
+ public void getPasskey(PluginCall call) {
56
+ try {
57
+ GetPasskeyOptions options = new GetPasskeyOptions(call);
58
+ NonEmptyResultCallback<GetPasskeyResult> callback = new NonEmptyResultCallback<>() {
59
+ @Override
60
+ public void success(@NonNull GetPasskeyResult result) {
61
+ resolveCall(call, result);
62
+ }
63
+
64
+ @Override
65
+ public void error(Exception exception) {
66
+ rejectCall(call, exception);
67
+ }
68
+ };
69
+ implementation.getPasskey(options, callback);
70
+ } catch (Exception exception) {
71
+ rejectCall(call, exception);
72
+ }
73
+ }
74
+
75
+ @PluginMethod
76
+ public void isAvailable(PluginCall call) {
77
+ try {
78
+ NonEmptyResultCallback<IsAvailableResult> callback = new NonEmptyResultCallback<>() {
79
+ @Override
80
+ public void success(@NonNull IsAvailableResult result) {
81
+ resolveCall(call, result);
82
+ }
83
+
84
+ @Override
85
+ public void error(Exception exception) {
86
+ rejectCall(call, exception);
87
+ }
88
+ };
89
+ implementation.isAvailable(callback);
90
+ } catch (Exception exception) {
91
+ rejectCall(call, exception);
92
+ }
93
+ }
94
+
95
+ private void rejectCall(@NonNull PluginCall call, @NonNull Exception exception) {
96
+ String message = exception.getMessage();
97
+ if (message == null) {
98
+ message = ERROR_UNKNOWN_ERROR;
99
+ }
100
+ String code = null;
101
+ if (exception instanceof CustomException) {
102
+ code = ((CustomException) exception).getCode();
103
+ }
104
+ Logger.error(TAG, message, exception);
105
+ call.reject(message, code);
106
+ }
107
+
108
+ private void resolveCall(@NonNull PluginCall call, @Nullable Result result) {
109
+ if (result == null) {
110
+ call.resolve();
111
+ } else {
112
+ call.resolve(result.toJSObject());
113
+ }
114
+ }
115
+ }
@@ -0,0 +1,20 @@
1
+ package io.capawesome.capacitorjs.plugins.passkeys.classes;
2
+
3
+ import androidx.annotation.NonNull;
4
+ import androidx.annotation.Nullable;
5
+
6
+ public class CustomException extends Exception {
7
+
8
+ @Nullable
9
+ private final String code;
10
+
11
+ public CustomException(@Nullable String code, @NonNull String message) {
12
+ super(message);
13
+ this.code = code;
14
+ }
15
+
16
+ @Nullable
17
+ public String getCode() {
18
+ return code;
19
+ }
20
+ }
@@ -0,0 +1,12 @@
1
+ package io.capawesome.capacitorjs.plugins.passkeys.classes;
2
+
3
+ public class CustomExceptions {
4
+
5
+ public static final CustomException CHALLENGE_MISSING = new CustomException(null, "challenge must be provided.");
6
+ public static final CustomException CREATE_FAILED = new CustomException("CREATE_FAILED", "The passkey could not be created.");
7
+ public static final CustomException GET_FAILED = new CustomException("GET_FAILED", "The passkey could not be retrieved.");
8
+ public static final CustomException OPERATION_CANCELED = new CustomException("CANCELED", "The operation was canceled by the user.");
9
+ public static final CustomException RP_ID_MISSING = new CustomException(null, "rp.id must be provided.");
10
+ public static final CustomException RP_ID_TOP_LEVEL_MISSING = new CustomException(null, "rpId must be provided.");
11
+ public static final CustomException USER_ID_MISSING = new CustomException(null, "user.id must be provided.");
12
+ }
@@ -0,0 +1,38 @@
1
+ package io.capawesome.capacitorjs.plugins.passkeys.classes.options;
2
+
3
+ import androidx.annotation.NonNull;
4
+ import com.getcapacitor.JSObject;
5
+ import com.getcapacitor.PluginCall;
6
+ import io.capawesome.capacitorjs.plugins.passkeys.classes.CustomExceptions;
7
+
8
+ public class CreatePasskeyOptions {
9
+
10
+ @NonNull
11
+ private final String requestJson;
12
+
13
+ public CreatePasskeyOptions(@NonNull PluginCall call) throws Exception {
14
+ this.requestJson = CreatePasskeyOptions.getRequestJsonFromCall(call);
15
+ }
16
+
17
+ @NonNull
18
+ public String getRequestJson() {
19
+ return requestJson;
20
+ }
21
+
22
+ @NonNull
23
+ private static String getRequestJsonFromCall(@NonNull PluginCall call) throws Exception {
24
+ JSObject data = call.getData();
25
+ if (data.getString("challenge") == null) {
26
+ throw CustomExceptions.CHALLENGE_MISSING;
27
+ }
28
+ JSObject rp = data.getJSObject("rp");
29
+ if (rp == null || rp.getString("id") == null) {
30
+ throw CustomExceptions.RP_ID_MISSING;
31
+ }
32
+ JSObject user = data.getJSObject("user");
33
+ if (user == null || user.getString("id") == null) {
34
+ throw CustomExceptions.USER_ID_MISSING;
35
+ }
36
+ return data.toString();
37
+ }
38
+ }
@@ -0,0 +1,33 @@
1
+ package io.capawesome.capacitorjs.plugins.passkeys.classes.options;
2
+
3
+ import androidx.annotation.NonNull;
4
+ import com.getcapacitor.JSObject;
5
+ import com.getcapacitor.PluginCall;
6
+ import io.capawesome.capacitorjs.plugins.passkeys.classes.CustomExceptions;
7
+
8
+ public class GetPasskeyOptions {
9
+
10
+ @NonNull
11
+ private final String requestJson;
12
+
13
+ public GetPasskeyOptions(@NonNull PluginCall call) throws Exception {
14
+ this.requestJson = GetPasskeyOptions.getRequestJsonFromCall(call);
15
+ }
16
+
17
+ @NonNull
18
+ public String getRequestJson() {
19
+ return requestJson;
20
+ }
21
+
22
+ @NonNull
23
+ private static String getRequestJsonFromCall(@NonNull PluginCall call) throws Exception {
24
+ JSObject data = call.getData();
25
+ if (data.getString("challenge") == null) {
26
+ throw CustomExceptions.CHALLENGE_MISSING;
27
+ }
28
+ if (data.getString("rpId") == null) {
29
+ throw CustomExceptions.RP_ID_TOP_LEVEL_MISSING;
30
+ }
31
+ return data.toString();
32
+ }
33
+ }
@@ -0,0 +1,22 @@
1
+ package io.capawesome.capacitorjs.plugins.passkeys.classes.results;
2
+
3
+ import androidx.annotation.NonNull;
4
+ import com.getcapacitor.JSObject;
5
+ import io.capawesome.capacitorjs.plugins.passkeys.interfaces.Result;
6
+ import org.json.JSONException;
7
+
8
+ public class CreatePasskeyResult implements Result {
9
+
10
+ @NonNull
11
+ private final JSObject registrationResponse;
12
+
13
+ public CreatePasskeyResult(@NonNull String registrationResponseJson) throws JSONException {
14
+ this.registrationResponse = new JSObject(registrationResponseJson);
15
+ }
16
+
17
+ @Override
18
+ @NonNull
19
+ public JSObject toJSObject() {
20
+ return registrationResponse;
21
+ }
22
+ }
@@ -0,0 +1,22 @@
1
+ package io.capawesome.capacitorjs.plugins.passkeys.classes.results;
2
+
3
+ import androidx.annotation.NonNull;
4
+ import com.getcapacitor.JSObject;
5
+ import io.capawesome.capacitorjs.plugins.passkeys.interfaces.Result;
6
+ import org.json.JSONException;
7
+
8
+ public class GetPasskeyResult implements Result {
9
+
10
+ @NonNull
11
+ private final JSObject authenticationResponse;
12
+
13
+ public GetPasskeyResult(@NonNull String authenticationResponseJson) throws JSONException {
14
+ this.authenticationResponse = new JSObject(authenticationResponseJson);
15
+ }
16
+
17
+ @Override
18
+ @NonNull
19
+ public JSObject toJSObject() {
20
+ return authenticationResponse;
21
+ }
22
+ }
@@ -0,0 +1,22 @@
1
+ package io.capawesome.capacitorjs.plugins.passkeys.classes.results;
2
+
3
+ import androidx.annotation.NonNull;
4
+ import com.getcapacitor.JSObject;
5
+ import io.capawesome.capacitorjs.plugins.passkeys.interfaces.Result;
6
+
7
+ public class IsAvailableResult implements Result {
8
+
9
+ private final boolean available;
10
+
11
+ public IsAvailableResult(boolean available) {
12
+ this.available = available;
13
+ }
14
+
15
+ @Override
16
+ @NonNull
17
+ public JSObject toJSObject() {
18
+ JSObject result = new JSObject();
19
+ result.put("available", available);
20
+ return result;
21
+ }
22
+ }
@@ -0,0 +1,5 @@
1
+ package io.capawesome.capacitorjs.plugins.passkeys.interfaces;
2
+
3
+ public interface Callback {
4
+ void error(Exception exception);
5
+ }
@@ -0,0 +1,7 @@
1
+ package io.capawesome.capacitorjs.plugins.passkeys.interfaces;
2
+
3
+ import androidx.annotation.NonNull;
4
+
5
+ public interface NonEmptyResultCallback<T extends Result> extends Callback {
6
+ void success(@NonNull T result);
7
+ }
@@ -0,0 +1,7 @@
1
+ package io.capawesome.capacitorjs.plugins.passkeys.interfaces;
2
+
3
+ import com.getcapacitor.JSObject;
4
+
5
+ public interface Result {
6
+ JSObject toJSObject();
7
+ }
File without changes