@capgo/capacitor-autofill-save-password 7.0.3 → 7.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -53,11 +53,36 @@ login(username: string, password: string) {
53
53
  }
54
54
  ```
55
55
 
56
+
57
+ ### Android
58
+
59
+ Add `apply plugin: 'com.google.gms.google-services'` beneath `apply plugin: 'com.android.application'` in `android/app/build.gradle`
60
+
61
+ this will allow the plugin to import the proper lib.
62
+
63
+ Then you need to make sure you did set properly your domain and did add google-services.json.
64
+
65
+ guide here https://developer.android.com/identity/sign-in/credential-manager
66
+
67
+ You need to have the file at this path `android/google-services.json` set, if you dont use firebase add empty json
68
+
69
+ then add your domain in `example-app/android/app/src/main/res/values/strings.xml`
70
+
71
+ with
72
+ ```xml
73
+ <string name="asset_statements" translatable="false">
74
+ [{
75
+ \"include\": \"https://YOURDOMAIN/.well-known/assetlinks.json\"
76
+ }]
77
+ </string>
78
+ ```
79
+
56
80
  ## API
57
81
 
58
82
  <docgen-index>
59
83
 
60
84
  * [`promptDialog(...)`](#promptdialog)
85
+ * [`readPassword()`](#readpassword)
61
86
  * [Interfaces](#interfaces)
62
87
 
63
88
  </docgen-index>
@@ -80,14 +105,36 @@ Save a password to the keychain.
80
105
  --------------------
81
106
 
82
107
 
108
+ ### readPassword()
109
+
110
+ ```typescript
111
+ readPassword() => Promise<ReadPasswordResult>
112
+ ```
113
+
114
+ Read a password from the keychain. Requires the developer to setup associated domain for the app for iOS.
115
+
116
+ **Returns:** <code>Promise&lt;<a href="#readpasswordresult">ReadPasswordResult</a>&gt;</code>
117
+
118
+ --------------------
119
+
120
+
83
121
  ### Interfaces
84
122
 
85
123
 
86
124
  #### Options
87
125
 
88
- | Prop | Type | Description |
89
- | -------------- | ------------------- | --------------------- |
90
- | **`username`** | <code>string</code> | The username to save. |
91
- | **`password`** | <code>string</code> | The password to save. |
126
+ | Prop | Type | Description |
127
+ | -------------- | ------------------- | -------------------------------------------------------------------------- |
128
+ | **`username`** | <code>string</code> | The username to save. |
129
+ | **`password`** | <code>string</code> | The password to save. |
130
+ | **`url`** | <code>string</code> | The url to save the password for. (For example: "web.capgo.app") iOS only. |
131
+
132
+
133
+ #### ReadPasswordResult
134
+
135
+ | Prop | Type | Description |
136
+ | -------------- | ------------------- | ----------------------------- |
137
+ | **`username`** | <code>string</code> | The username of the password. |
138
+ | **`password`** | <code>string</code> | The password of the password. |
92
139
 
93
140
  </docgen-api>
@@ -47,14 +47,12 @@ repositories {
47
47
  mavenCentral()
48
48
  }
49
49
 
50
-
51
50
  dependencies {
52
51
  implementation fileTree(dir: 'libs', include: ['*.jar'])
53
52
  implementation project(':capacitor-android')
54
53
  implementation "androidx.appcompat:appcompat:$androidxAppCompatVersion"
55
- // implementation(libs.androidx.credentials)
56
- // implementation(libs.androidx.credentials.play.services.auth)
57
- // implementation 'androidx.credentials:credentials:1.5.0'
54
+ implementation "androidx.credentials:credentials:1.5.0"
55
+ implementation "androidx.credentials:credentials-play-services-auth:1.5.0"
58
56
  testImplementation "junit:junit:$junitVersion"
59
57
  androidTestImplementation "androidx.test.ext:junit:$androidxJunitVersion"
60
58
  androidTestImplementation "androidx.test.espresso:espresso-core:$androidxEspressoCoreVersion"
@@ -1,78 +1,167 @@
1
1
  package ee.forgr.autofill_password;
2
2
 
3
+ import android.app.Activity;
4
+ import android.os.Build;
5
+ import android.util.Log;
6
+ import androidx.core.content.ContextCompat;
7
+ import androidx.credentials.CreateCredentialResponse;
8
+ import androidx.credentials.CreatePasswordRequest;
9
+ import androidx.credentials.CredentialManager;
10
+ import androidx.credentials.CredentialManagerCallback;
11
+ import androidx.credentials.GetCredentialRequest;
12
+ import androidx.credentials.GetCredentialResponse;
13
+ import androidx.credentials.GetPasswordOption;
14
+ import androidx.credentials.PasswordCredential;
15
+ import androidx.credentials.PendingGetCredentialRequest;
16
+ import androidx.credentials.exceptions.CreateCredentialException;
17
+ import androidx.credentials.exceptions.GetCredentialException;
3
18
  import com.getcapacitor.JSObject;
4
19
  import com.getcapacitor.Plugin;
5
20
  import com.getcapacitor.PluginCall;
6
21
  import com.getcapacitor.PluginMethod;
7
22
  import com.getcapacitor.annotation.CapacitorPlugin;
8
- import android.util.Log;
9
- import android.os.CancellationSignal;
10
- //import androidx.credentials.CredentialManager;
11
- //import androidx.credentials.CredentialManagerCallback;
12
- //import androidx.credentials.CreatePasswordRequest;
13
- //import androidx.credentials.CreateCredentialResponse;
14
- //import androidx.credentials.exceptions.CreateCredentialException;
15
- import java.util.concurrent.Executor;
23
+ import java.util.Arrays;
24
+ import java.util.HashMap;
25
+ import java.util.List;
26
+ import java.util.Map;
16
27
 
17
28
  @CapacitorPlugin(name = "SavePassword")
18
29
  public class SavePasswordPlugin extends Plugin {
19
30
 
20
- private static final String TAG = "SavePasswordPlugin";
31
+ private static final String TAG = "CredentialManager";
32
+ private CredentialManager credentialManager;
33
+ private Map<String, PendingGetCredentialRequest> pendingRequestsByElementId = new HashMap<>();
34
+
35
+ @Override
36
+ public void load() {
37
+ super.load();
38
+ try {
39
+ credentialManager = CredentialManager.create(getContext());
40
+ } catch (Exception e) {
41
+ Log.e(TAG, "Error initializing CredentialManager", e);
42
+ }
43
+ }
21
44
 
22
45
  @PluginMethod
23
- public void promptDialog(PluginCall call) {
46
+ public void promptDialog(final PluginCall call) {
47
+ if (!isCredentialManagerAvailable(call)) {
48
+ return;
49
+ }
50
+
24
51
  String username = call.getString("username");
25
52
  String password = call.getString("password");
26
53
 
27
54
  if (username == null || username.isEmpty()) {
28
- call.reject("Username cannot be empty.");
29
- Log.w(TAG, "Username was null or empty.");
55
+ call.reject("Username is required");
30
56
  return;
31
57
  }
32
58
 
33
- if (password == null) {
34
- call.reject("Password cannot be null. Provide an empty string if the password is meant to be empty.");
35
- Log.w(TAG, "Password was null.");
59
+ if (password == null || password.isEmpty()) {
60
+ call.reject("Password is required");
36
61
  return;
37
62
  }
38
63
 
39
- if (getActivity() == null) {
40
- call.reject("Activity is not available to show dialog.");
41
- Log.e(TAG, "Activity was null, cannot access CredentialManager.");
64
+ try {
65
+ // Build request directly with username & password (API 1.5.0 signature)
66
+ CreatePasswordRequest request = new CreatePasswordRequest(username, password);
67
+
68
+ // Execute on main thread
69
+ bridge.executeOnMainThread(() -> {
70
+ Activity activity = getActivity();
71
+ if (activity == null) {
72
+ call.reject("Activity not available");
73
+ return;
74
+ }
75
+
76
+ try {
77
+ credentialManager.createCredentialAsync(
78
+ activity,
79
+ request,
80
+ null,
81
+ ContextCompat.getMainExecutor(getContext()),
82
+ new CredentialManagerCallback<CreateCredentialResponse, CreateCredentialException>() {
83
+ @Override
84
+ public void onResult(CreateCredentialResponse response) {
85
+ call.resolve();
86
+ }
87
+
88
+ @Override
89
+ public void onError(CreateCredentialException e) {
90
+ call.reject("Error saving credential: " + e.getMessage(), e);
91
+ }
92
+ }
93
+ );
94
+ } catch (Exception e) {
95
+ call.reject("Error saving credential", e);
96
+ }
97
+ });
98
+ } catch (Exception e) {
99
+ call.reject("Error building save credential request", e);
100
+ }
101
+ }
102
+
103
+ @PluginMethod
104
+ public void readPassword(final PluginCall call) {
105
+ if (!isCredentialManagerAvailable(call)) {
42
106
  return;
43
107
  }
44
108
 
45
- // // Build the CreatePasswordRequest
46
- // CreatePasswordRequest createPasswordRequest = new CreatePasswordRequest(username, password);
47
- //
48
- // // Get the CredentialManager instance
49
- // CredentialManager credentialManager = CredentialManager.create(getActivity());
50
- //
51
- // // Set up executor and cancellation signal
52
- // Executor executor = getActivity().getMainExecutor();
53
- // CancellationSignal cancellationSignal = new CancellationSignal();
54
- //
55
- // credentialManager.createCredentialAsync(
56
- // getActivity(),
57
- // createPasswordRequest,
58
- // cancellationSignal,
59
- // executor,
60
- // new CredentialManagerCallback<CreateCredentialResponse, CreateCredentialException>() {
61
- // @Override
62
- // public void onResult(CreateCredentialResponse result) {
63
- JSObject response = new JSObject();
64
- response.put("prompted", true);
65
- Log.d(TAG, "Password save prompt completed successfully.");
66
- call.resolve(response);
67
- // }
68
- //
69
- // @Override
70
- // public void onError(CreateCredentialException e) {
71
- // String errorMessage = "Failed to save password credential: " + e.getMessage();
72
- // Log.e(TAG, errorMessage, e);
73
- // call.reject(errorMessage, e);
74
- // }
75
- // }
76
- // );
109
+ try {
110
+ GetCredentialRequest request = new GetCredentialRequest(List.of(new GetPasswordOption()));
111
+
112
+ bridge.executeOnMainThread(() -> {
113
+ Activity activity = getActivity();
114
+ if (activity == null) {
115
+ call.reject("Activity not available");
116
+ return;
117
+ }
118
+
119
+ try {
120
+ credentialManager.getCredentialAsync(
121
+ activity,
122
+ request,
123
+ null,
124
+ ContextCompat.getMainExecutor(getContext()),
125
+ new CredentialManagerCallback<GetCredentialResponse, GetCredentialException>() {
126
+ @Override
127
+ public void onResult(GetCredentialResponse response) {
128
+ if (response.getCredential() instanceof PasswordCredential) {
129
+ PasswordCredential credential = (PasswordCredential) response.getCredential();
130
+ JSObject result = new JSObject();
131
+ result.put("username", credential.getId());
132
+ result.put("password", credential.getPassword());
133
+ call.resolve(result);
134
+ } else {
135
+ call.reject("No password credential found");
136
+ }
137
+ }
138
+
139
+ @Override
140
+ public void onError(GetCredentialException e) {
141
+ call.reject("Error retrieving credential: " + e.getMessage(), e);
142
+ }
143
+ }
144
+ );
145
+ } catch (Exception e) {
146
+ call.reject("Error retrieving credential", e);
147
+ }
148
+ });
149
+ } catch (Exception e) {
150
+ call.reject("Error building get credential request", e);
151
+ }
152
+ }
153
+
154
+ private boolean isCredentialManagerAvailable(PluginCall call) {
155
+ if (credentialManager == null) {
156
+ call.reject("Credential Manager not available on this device");
157
+ return false;
158
+ }
159
+
160
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
161
+ call.reject("Credential Manager requires Android API level 28 or higher");
162
+ return false;
163
+ }
164
+
165
+ return true;
77
166
  }
78
167
  }
package/dist/docs.json CHANGED
@@ -44,6 +44,23 @@
44
44
  "Options"
45
45
  ],
46
46
  "slug": "promptdialog"
47
+ },
48
+ {
49
+ "name": "readPassword",
50
+ "signature": "() => Promise<ReadPasswordResult>",
51
+ "parameters": [],
52
+ "returns": "Promise<ReadPasswordResult>",
53
+ "tags": [
54
+ {
55
+ "name": "returns",
56
+ "text": "The retrieved password credentials"
57
+ }
58
+ ],
59
+ "docs": "Read a password from the keychain. Requires the developer to setup associated domain for the app for iOS.",
60
+ "complexTypes": [
61
+ "ReadPasswordResult"
62
+ ],
63
+ "slug": "readpassword"
47
64
  }
48
65
  ],
49
66
  "properties": []
@@ -78,6 +95,36 @@
78
95
  "docs": "The password to save.",
79
96
  "complexTypes": [],
80
97
  "type": "string"
98
+ },
99
+ {
100
+ "name": "url",
101
+ "tags": [],
102
+ "docs": "The url to save the password for. (For example: \"web.capgo.app\")\niOS only.",
103
+ "complexTypes": [],
104
+ "type": "string | undefined"
105
+ }
106
+ ]
107
+ },
108
+ {
109
+ "name": "ReadPasswordResult",
110
+ "slug": "readpasswordresult",
111
+ "docs": "",
112
+ "tags": [],
113
+ "methods": [],
114
+ "properties": [
115
+ {
116
+ "name": "username",
117
+ "tags": [],
118
+ "docs": "The username of the password.",
119
+ "complexTypes": [],
120
+ "type": "string"
121
+ },
122
+ {
123
+ "name": "password",
124
+ "tags": [],
125
+ "docs": "The password of the password.",
126
+ "complexTypes": [],
127
+ "type": "string"
81
128
  }
82
129
  ]
83
130
  }
@@ -11,6 +11,21 @@ export interface Options {
11
11
  * The password to save.
12
12
  */
13
13
  password: string;
14
+ /**
15
+ * The url to save the password for. (For example: "web.capgo.app")
16
+ * iOS only.
17
+ */
18
+ url?: string;
19
+ }
20
+ export interface ReadPasswordResult {
21
+ /**
22
+ * The username of the password.
23
+ */
24
+ username: string;
25
+ /**
26
+ * The password of the password.
27
+ */
28
+ password: string;
14
29
  }
15
30
  /**
16
31
  * @interface SavePasswordPlugin
@@ -28,4 +43,9 @@ export interface SavePasswordPlugin {
28
43
  * });
29
44
  */
30
45
  promptDialog(options: Options): Promise<void>;
46
+ /**
47
+ * Read a password from the keychain. Requires the developer to setup associated domain for the app for iOS.
48
+ * @returns {Promise<ReadPasswordResult>} The retrieved password credentials
49
+ */
50
+ readPassword(): Promise<ReadPasswordResult>;
31
51
  }
@@ -1 +1 @@
1
- {"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * @interface Options\n * @description The options for the prompt.\n */\nexport interface Options {\n /**\n * The username to save.\n */\n username: string;\n /**\n * The password to save.\n */\n password: string;\n}\n\n/**\n * @interface SavePasswordPlugin\n * @description Capacitor plugin for saving passwords to the keychain.\n */\nexport interface SavePasswordPlugin {\n /**\n * Save a password to the keychain.\n * @param {Options} options - The options for the password.\n * @returns {Promise<void>} Success status\n * @example\n * await SavePassword.promptDialog({\n * username: 'your-username',\n * password: 'your-password'\n * });\n */\n promptDialog(options: Options): Promise<void>;\n}\n"]}
1
+ {"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * @interface Options\n * @description The options for the prompt.\n */\nexport interface Options {\n /**\n * The username to save.\n */\n username: string;\n /**\n * The password to save.\n */\n password: string;\n /**\n * The url to save the password for. (For example: \"web.capgo.app\")\n * iOS only.\n */\n url?: string;\n}\n\nexport interface ReadPasswordResult {\n /**\n * The username of the password.\n */\n username: string;\n /**\n * The password of the password.\n */\n password: string;\n}\n\n/**\n * @interface SavePasswordPlugin\n * @description Capacitor plugin for saving passwords to the keychain.\n */\nexport interface SavePasswordPlugin {\n /**\n * Save a password to the keychain.\n * @param {Options} options - The options for the password.\n * @returns {Promise<void>} Success status\n * @example\n * await SavePassword.promptDialog({\n * username: 'your-username',\n * password: 'your-password'\n * });\n */\n promptDialog(options: Options): Promise<void>;\n\n /**\n * Read a password from the keychain. Requires the developer to setup associated domain for the app for iOS.\n * @returns {Promise<ReadPasswordResult>} The retrieved password credentials\n */\n readPassword(): Promise<ReadPasswordResult>;\n}\n"]}
package/dist/esm/web.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { WebPlugin } from '@capacitor/core';
2
- import type { Options, SavePasswordPlugin } from './definitions';
2
+ import type { Options, ReadPasswordResult, SavePasswordPlugin } from './definitions';
3
3
  export declare class SavePasswordWeb extends WebPlugin implements SavePasswordPlugin {
4
+ readPassword(): Promise<ReadPasswordResult>;
4
5
  promptDialog(options: Options): Promise<void>;
5
6
  }
package/dist/esm/web.js CHANGED
@@ -1,7 +1,10 @@
1
1
  import { WebPlugin } from '@capacitor/core';
2
2
  export class SavePasswordWeb extends WebPlugin {
3
+ readPassword() {
4
+ throw new Error('Method not implemented.');
5
+ }
3
6
  async promptDialog(options) {
4
- throw new Error("Not implemented on web" + JSON.stringify(options));
7
+ throw new Error('Not implemented on web' + JSON.stringify(options));
5
8
  }
6
9
  }
7
10
  //# sourceMappingURL=web.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"web.js","sourceRoot":"","sources":["../../src/web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAI5C,MAAM,OAAO,eAAgB,SAAQ,SAAS;IAC5C,KAAK,CAAC,YAAY,CAAC,OAAgB;QACjC,MAAM,IAAI,KAAK,CAAC,wBAAwB,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC;IACtE,CAAC;CACF","sourcesContent":["import { WebPlugin } from '@capacitor/core';\n\nimport type { Options, SavePasswordPlugin } from './definitions';\n\nexport class SavePasswordWeb extends WebPlugin implements SavePasswordPlugin {\n async promptDialog(options: Options): Promise<void> {\n throw new Error(\"Not implemented on web\" + JSON.stringify(options));\n }\n}\n"]}
1
+ {"version":3,"file":"web.js","sourceRoot":"","sources":["../../src/web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAI5C,MAAM,OAAO,eAAgB,SAAQ,SAAS;IAC5C,YAAY;QACV,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;IAC7C,CAAC;IACD,KAAK,CAAC,YAAY,CAAC,OAAgB;QACjC,MAAM,IAAI,KAAK,CAAC,wBAAwB,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC;IACtE,CAAC;CACF","sourcesContent":["import { WebPlugin } from '@capacitor/core';\n\nimport type { Options, ReadPasswordResult, SavePasswordPlugin } from './definitions';\n\nexport class SavePasswordWeb extends WebPlugin implements SavePasswordPlugin {\n readPassword(): Promise<ReadPasswordResult> {\n throw new Error('Method not implemented.');\n }\n async promptDialog(options: Options): Promise<void> {\n throw new Error('Not implemented on web' + JSON.stringify(options));\n }\n}\n"]}
@@ -7,8 +7,11 @@ const SavePassword = core.registerPlugin('SavePassword', {
7
7
  });
8
8
 
9
9
  class SavePasswordWeb extends core.WebPlugin {
10
+ readPassword() {
11
+ throw new Error('Method not implemented.');
12
+ }
10
13
  async promptDialog(options) {
11
- throw new Error("Not implemented on web" + JSON.stringify(options));
14
+ throw new Error('Not implemented on web' + JSON.stringify(options));
12
15
  }
13
16
  }
14
17
 
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.cjs.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\nconst SavePassword = registerPlugin('SavePassword', {\n web: () => import('./web').then((m) => new m.SavePasswordWeb()),\n});\nexport * from './definitions';\nexport { SavePassword };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\nexport class SavePasswordWeb extends WebPlugin {\n async promptDialog(options) {\n throw new Error(\"Not implemented on web\" + JSON.stringify(options));\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":";;;;AACK,MAAC,YAAY,GAAGA,mBAAc,CAAC,cAAc,EAAE;AACpD,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,eAAe,EAAE,CAAC;AACnE,CAAC;;ACFM,MAAM,eAAe,SAASC,cAAS,CAAC;AAC/C,IAAI,MAAM,YAAY,CAAC,OAAO,EAAE;AAChC,QAAQ,MAAM,IAAI,KAAK,CAAC,wBAAwB,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;AAC3E;AACA;;;;;;;;;"}
1
+ {"version":3,"file":"plugin.cjs.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\nconst SavePassword = registerPlugin('SavePassword', {\n web: () => import('./web').then((m) => new m.SavePasswordWeb()),\n});\nexport * from './definitions';\nexport { SavePassword };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\nexport class SavePasswordWeb extends WebPlugin {\n readPassword() {\n throw new Error('Method not implemented.');\n }\n async promptDialog(options) {\n throw new Error('Not implemented on web' + JSON.stringify(options));\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":";;;;AACK,MAAC,YAAY,GAAGA,mBAAc,CAAC,cAAc,EAAE;AACpD,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,eAAe,EAAE,CAAC;AACnE,CAAC;;ACFM,MAAM,eAAe,SAASC,cAAS,CAAC;AAC/C,IAAI,YAAY,GAAG;AACnB,QAAQ,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC;AAClD;AACA,IAAI,MAAM,YAAY,CAAC,OAAO,EAAE;AAChC,QAAQ,MAAM,IAAI,KAAK,CAAC,wBAAwB,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;AAC3E;AACA;;;;;;;;;"}
package/dist/plugin.js CHANGED
@@ -6,8 +6,11 @@ var capacitorSavePassword = (function (exports, core) {
6
6
  });
7
7
 
8
8
  class SavePasswordWeb extends core.WebPlugin {
9
+ readPassword() {
10
+ throw new Error('Method not implemented.');
11
+ }
9
12
  async promptDialog(options) {
10
- throw new Error("Not implemented on web" + JSON.stringify(options));
13
+ throw new Error('Not implemented on web' + JSON.stringify(options));
11
14
  }
12
15
  }
13
16
 
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\nconst SavePassword = registerPlugin('SavePassword', {\n web: () => import('./web').then((m) => new m.SavePasswordWeb()),\n});\nexport * from './definitions';\nexport { SavePassword };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\nexport class SavePasswordWeb extends WebPlugin {\n async promptDialog(options) {\n throw new Error(\"Not implemented on web\" + JSON.stringify(options));\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":";;;AACK,UAAC,YAAY,GAAGA,mBAAc,CAAC,cAAc,EAAE;IACpD,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,eAAe,EAAE,CAAC;IACnE,CAAC;;ICFM,MAAM,eAAe,SAASC,cAAS,CAAC;IAC/C,IAAI,MAAM,YAAY,CAAC,OAAO,EAAE;IAChC,QAAQ,MAAM,IAAI,KAAK,CAAC,wBAAwB,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;IAC3E;IACA;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"plugin.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\nconst SavePassword = registerPlugin('SavePassword', {\n web: () => import('./web').then((m) => new m.SavePasswordWeb()),\n});\nexport * from './definitions';\nexport { SavePassword };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\nexport class SavePasswordWeb extends WebPlugin {\n readPassword() {\n throw new Error('Method not implemented.');\n }\n async promptDialog(options) {\n throw new Error('Not implemented on web' + JSON.stringify(options));\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":";;;AACK,UAAC,YAAY,GAAGA,mBAAc,CAAC,cAAc,EAAE;IACpD,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,eAAe,EAAE,CAAC;IACnE,CAAC;;ICFM,MAAM,eAAe,SAASC,cAAS,CAAC;IAC/C,IAAI,YAAY,GAAG;IACnB,QAAQ,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC;IAClD;IACA,IAAI,MAAM,YAAY,CAAC,OAAO,EAAE;IAChC,QAAQ,MAAM,IAAI,KAAK,CAAC,wBAAwB,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;IAC3E;IACA;;;;;;;;;;;;;;;"}
@@ -1,62 +1,100 @@
1
1
  import Foundation
2
2
  import Capacitor
3
+ import Security
4
+ import AuthenticationServices
3
5
 
4
6
  /**
5
7
  * Please read the Capacitor iOS Plugin Development Guide
6
8
  * here: https://capacitorjs.com/docs/plugins/ios
7
9
  */
8
10
  @objc(SavePasswordPlugin)
9
- public class SavePasswordPlugin: CAPPlugin, CAPBridgedPlugin {
11
+ public class SavePasswordPlugin: CAPPlugin, CAPBridgedPlugin, ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding {
10
12
  public let identifier = "SavePasswordPlugin"
11
13
 
12
14
  public let jsName = "SavePassword"
13
15
  public let pluginMethods: [CAPPluginMethod] = [
14
- CAPPluginMethod(name: "promptDialog", returnType: CAPPluginReturnPromise)
16
+ CAPPluginMethod(name: "promptDialog", returnType: CAPPluginReturnPromise),
17
+ CAPPluginMethod(name: "readPassword", returnType: CAPPluginReturnPromise)
15
18
  ]
16
19
 
17
20
  @objc func promptDialog(_ call: CAPPluginCall) {
18
- DispatchQueue.main.async {
19
- let loginScreen = LoginScreenViewController()
20
- loginScreen.usernameTextField.text = call.getString("username") ?? ""
21
- loginScreen.passwordTextField.text = call.getString("password") ?? ""
22
- self.bridge?.webView?.addSubview(loginScreen.view)
21
+ guard let username = call.getString("username"),
22
+ let password = call.getString("password") else {
23
+ call.reject("Username and password are required")
24
+ return
25
+ }
26
+ guard let url = call.getString("url") else {
27
+ call.reject("URL is required for iOS shared web credentials")
28
+ return
29
+ }
30
+ let fqdn = url as CFString
31
+ let user = username as CFString
32
+ let pass = password as CFString
33
+ SecAddSharedWebCredential(fqdn, user, pass) { error in
34
+ DispatchQueue.main.async {
35
+ if let error = error {
36
+ let cfError = error as CFError
37
+ let description = CFErrorCopyDescription(cfError) as String? ?? "Unknown error"
38
+ call.reject("Failed to save credential", description)
39
+ } else {
40
+ call.resolve()
41
+ }
42
+ }
43
+ }
44
+ }
23
45
 
24
- // Defer removal so the system registers the fields before they disappear
46
+ @objc func readPassword(_ call: CAPPluginCall) {
47
+ if #available(iOS 12.0, *) {
25
48
  DispatchQueue.main.async {
26
- loginScreen.view.removeFromSuperview()
27
- // Clear fields *after* removal as required by Autofill heuristics
28
- loginScreen.usernameTextField.text = ""
29
- loginScreen.passwordTextField.text = ""
30
- call.resolve()
49
+ let passwordRequest = ASAuthorizationPasswordProvider().createRequest()
50
+ let authController = ASAuthorizationController(authorizationRequests: [passwordRequest])
51
+ self.currentReadCall = call
52
+ authController.delegate = self
53
+ authController.presentationContextProvider = self
54
+ authController.performRequests()
31
55
  }
56
+ } else {
57
+ call.reject("Password autofill not available on this iOS version")
32
58
  }
33
59
  }
34
- }
35
60
 
36
- class LoginScreenViewController: UIViewController {
37
- let usernameTextField: UITextField = {
38
- let textField = UITextField()
39
- textField.frame.size.width = 1
40
- textField.frame.size.height = 1
41
- textField.textContentType = .username
42
- return textField
43
- }()
44
-
45
- let passwordTextField: UITextField = {
46
- let textField = UITextField()
47
- textField.frame.size.width = 1
48
- textField.frame.size.height = 1
49
- textField.textContentType = .newPassword
50
- // Fix for ios 18.3 : from https://stackoverflow.com/questions/76773166/password-autofill-wkwebview-doesnt-present-save-password-alert#comment140186929_76773167
51
- return textField
52
- }()
53
-
54
- override func viewDidLoad() {
55
- super.viewDidLoad()
56
- view.frame = CGRect(x: 0, y: 0, width: 0, height: 0)
57
- view.addSubview(usernameTextField)
58
- view.addSubview(passwordTextField)
59
- // Make password the first responder so the strong-password prompt or save-password alert triggers reliably
60
- passwordTextField.becomeFirstResponder()
61
+ private var currentReadCall: CAPPluginCall?
62
+ private var currentCall: CAPPluginCall?
63
+
64
+ public func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
65
+ if let passwordCredential = authorization.credential as? ASPasswordCredential {
66
+ if let call = currentReadCall {
67
+ call.resolve([
68
+ "username": passwordCredential.user,
69
+ "password": passwordCredential.password
70
+ ])
71
+ currentReadCall = nil
72
+ return
73
+ }
74
+ currentCall?.resolve([
75
+ "username": passwordCredential.user,
76
+ "password": passwordCredential.password
77
+ ])
78
+ } else {
79
+ currentReadCall?.resolve()
80
+ currentReadCall = nil
81
+ currentCall?.resolve()
82
+ currentCall = nil
83
+ }
84
+ }
85
+
86
+ public func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
87
+ if let call = currentReadCall {
88
+ call.reject("Autofill failed", error.localizedDescription)
89
+ currentReadCall = nil
90
+ return
91
+ }
92
+ currentCall?.reject("Autofill failed", error.localizedDescription)
93
+ currentCall = nil
94
+ }
95
+
96
+ // MARK: - ASAuthorizationControllerPresentationContextProviding
97
+ public func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
98
+ return self.bridge?.viewController?.view.window ?? ASPresentationAnchor()
61
99
  }
62
100
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capgo/capacitor-autofill-save-password",
3
- "version": "7.0.3",
3
+ "version": "7.1.0",
4
4
  "description": "Prompt to display dialog for saving password to keychain from webview app",
5
5
  "main": "dist/plugin.cjs.js",
6
6
  "module": "dist/esm/index.js",