@iternio/react-native-auto-play 0.7.0 → 0.8.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
@@ -682,7 +682,7 @@ Below is a concise overview of the most important props per template. Optional p
682
682
 
683
683
  | Prop | Type | Required | Notes |
684
684
  | --- | --- | --- | --- |
685
- | `signInMethod` | `SignInMethod` | ✅ | The sign-in method configuration. Can be QRSignIn, PinSignIn, InputSignIn. |
685
+ | `signInMethod` | `SignInMethod` | ✅ | The sign-in method configuration. Can be QRSignIn, PinSignIn, InputSignIn, GoogleSignIn. |
686
686
  | `title` | `string` | ❌ | Header title. |
687
687
  | `additionalText` | `string` | ❌ | Additional descriptive text. |
688
688
  | `instructions` | `string` | ❌ | Sign-in Instructions text. |
@@ -696,6 +696,19 @@ Below is a concise overview of the most important props per template. Optional p
696
696
  | QR | `QrSignIn` | QR Code sign in |
697
697
  | PIN | `PinSignIn` | PIN Code sign in (1–12 characters) |
698
698
  | Input | `InputSignIn` | Text sign in, for example mail/username and password |
699
+ | Google | `GoogleSignIn` | Sign in with Google. Requires Google Play services 23.08 or newer — check `SignInTemplate.isGoogleSignInAvailable()` first. |
700
+
701
+ `GoogleSignIn` takes a `serverClientId` (your **Web** OAuth client ID), a `signInButtonText` and a `callback(error?, account?)`. On success the returned `GoogleSignInAccount` always carries both `idToken` and `serverAuthCode` (offline access, `openid` `profile` `email` scopes) for your backend. If the user cancels, or either the account picker or the authorization step fails, `callback` is called with an `error` and no account.
702
+
703
+ **Sign in with Google needs a recent Play services.** Constructing a `SignInTemplate` with `GoogleSignIn` throws `playServicesUnavailable` (and `updateTemplate` rejects with it) when Google Play services is missing or older than 23.08 (e.g. Android Automotive builds without GMS). Pick a different method up front instead:
704
+
705
+ ```ts
706
+ const signInMethod: SignInMethod = SignInTemplate.isGoogleSignInAvailable()
707
+ ? { method: SignInMethods.GOOGLE, serverClientId, signInButtonText: 'Sign in with Google', callback }
708
+ : { method: SignInMethods.QR, url: loginUrl };
709
+ ```
710
+
711
+ `isGoogleSignInAvailable()` always returns `false` on iOS.
699
712
 
700
713
  For InputSignIn the keyboard and input fields can be configured with following properties:
701
714
 
@@ -165,7 +165,9 @@ dependencies {
165
165
  implementation "androidx.car.app:app-projected:1.7.0"
166
166
  }
167
167
 
168
- implementation 'com.google.android.gms:play-services-base:18.10.0'
169
- implementation 'com.google.android.gms:play-services-auth:21.5.1'
168
+ implementation 'com.google.android.gms:play-services-auth:22.0.0'
169
+ implementation 'androidx.credentials:credentials:1.6.0'
170
+ implementation 'androidx.credentials:credentials-play-services-auth:1.6.0'
171
+ implementation 'com.google.android.libraries.identity.googleid:googleid:1.1.1'
170
172
  }
171
173
 
@@ -8,3 +8,7 @@ class TemplateNotFoundException(private val templateId: String) :
8
8
  class VoiceInputCancelledException : Exception("voiceInputCancelled") {
9
9
  override fun toString(): String = "voiceInputCancelled"
10
10
  }
11
+
12
+ class PlayServicesUnavailableException : Exception("playServicesUnavailable") {
13
+ override fun toString(): String = "playServicesUnavailable"
14
+ }
@@ -1,5 +1,8 @@
1
1
  package com.margelo.nitro.swe.iternio.reactnativeautoplay
2
2
 
3
+ import com.google.android.gms.common.ConnectionResult
4
+ import com.google.android.gms.common.GoogleApiAvailability
5
+ import com.margelo.nitro.NitroModules
3
6
  import com.margelo.nitro.core.Promise
4
7
  import com.margelo.nitro.swe.iternio.reactnativeautoplay.template.AndroidAutoTemplate
5
8
  import com.margelo.nitro.swe.iternio.reactnativeautoplay.template.SignInTemplate
@@ -9,14 +12,34 @@ class HybridSignInTemplate : HybridSignInTemplateSpec() {
9
12
  val context = AndroidAutoSession.getRootContext()
10
13
  ?: throw IllegalArgumentException("createSignInTemplate failed, no carContext found")
11
14
 
15
+ assertSignInMethodAvailable(config)
16
+
12
17
  val template = SignInTemplate(context, config)
13
18
  AndroidAutoTemplate.setTemplate(config.id, template)
14
19
  }
15
20
 
16
21
  override fun updateTemplate(templateId: String, config: SignInTemplateConfig): Promise<Unit> {
17
22
  return Promise.async {
23
+ assertSignInMethodAvailable(config)
18
24
  val template = AndroidAutoTemplate.getTemplate<SignInTemplate>(templateId)
19
25
  template.updateTemplate(config)
20
26
  }
21
27
  }
28
+
29
+ override fun isGoogleSignInAvailable(): Boolean {
30
+ val context = NitroModules.applicationContext ?: return false
31
+
32
+ // on older versions sign in with google is no longer supported
33
+ val googleServiceMinVersion = 230815045
34
+ val available = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(
35
+ context, googleServiceMinVersion
36
+ )
37
+ return ConnectionResult.SUCCESS == available
38
+ }
39
+
40
+ private fun assertSignInMethodAvailable(config: SignInTemplateConfig) {
41
+ if (config.signInMethod?.asFourthOrNull() != null && !isGoogleSignInAvailable()) {
42
+ throw PlayServicesUnavailableException()
43
+ }
44
+ }
22
45
  }
@@ -4,42 +4,90 @@ import android.os.Binder
4
4
  import android.os.Bundle
5
5
  import android.os.IBinder
6
6
  import androidx.activity.ComponentActivity
7
- import androidx.activity.result.ActivityResult
7
+ import androidx.activity.result.IntentSenderRequest
8
8
  import androidx.activity.result.contract.ActivityResultContracts
9
- import com.google.android.gms.auth.api.signin.GoogleSignIn
10
- import com.google.android.gms.auth.api.signin.GoogleSignInAccount
11
- import com.google.android.gms.auth.api.signin.GoogleSignInOptions
9
+ import androidx.credentials.CredentialManager
10
+ import androidx.credentials.GetCredentialRequest
11
+ import androidx.lifecycle.lifecycleScope
12
+ import com.google.android.gms.auth.api.identity.AuthorizationRequest
13
+ import com.google.android.gms.auth.api.identity.Identity
14
+ import com.google.android.gms.common.Scopes
15
+ import com.google.android.gms.common.api.Scope
16
+ import com.google.android.libraries.identity.googleid.GetSignInWithGoogleOption
17
+ import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential
18
+ import kotlinx.coroutines.launch
12
19
  import java.security.InvalidParameterException
13
- import javax.annotation.Nullable
14
20
 
15
21
  class SignInWithGoogleActivity : ComponentActivity() {
16
- override fun onCreate(savedInstanceState: Bundle?) {
17
- super.onCreate(savedInstanceState)
18
22
 
19
- val signInCompleteCallback = intent.extras?.getBinder(BINDER_KEY) as OnSignInComplete?
23
+ private var callback: OnSignInComplete? = null
24
+ private var idCredential: GoogleIdTokenCredential? = null
25
+
26
+ private val authLauncher = registerForActivityResult(
27
+ ActivityResultContracts.StartIntentSenderForResult()
28
+ ) { result ->
29
+ val auth = runCatching {
30
+ Identity.getAuthorizationClient(this)
31
+ .getAuthorizationResultFromIntent(result.data)
32
+ }.getOrNull()
33
+ finishWith(auth?.serverAuthCode)
34
+ }
20
35
 
36
+ override fun onCreate(savedInstanceState: Bundle?) {
37
+ super.onCreate(savedInstanceState)
38
+ callback = intent.extras?.getBinder(BINDER_KEY) as OnSignInComplete?
21
39
  val serverClientId = intent.extras?.getString("serverClientId")
22
40
  ?: throw InvalidParameterException("missing serverClientId parameter")
23
41
 
24
- val activityResultLauncher = registerForActivityResult(
25
- ActivityResultContracts.StartActivityForResult()
26
- ) { result: ActivityResult ->
27
- val account = GoogleSignIn.getSignedInAccountFromIntent(
28
- result.data
29
- ).result
30
- signInCompleteCallback?.onSignInComplete(account)
31
- finish()
32
- }
42
+ lifecycleScope.launch {
43
+ // 1. Authentication: ID token
44
+ val request = GetCredentialRequest.Builder()
45
+ .addCredentialOption(GetSignInWithGoogleOption.Builder(serverClientId).build())
46
+ .build()
47
+ try {
48
+ val result = CredentialManager.create(this@SignInWithGoogleActivity)
49
+ .getCredential(this@SignInWithGoogleActivity, request)
50
+ idCredential = GoogleIdTokenCredential.createFrom(result.credential.data)
51
+ } catch (_: Exception) {
52
+ callback?.onSignInComplete(null, null)
53
+ finish()
54
+ return@launch
55
+ }
33
56
 
34
- val googleSignInClient = GoogleSignIn.getClient(
35
- this,
36
- GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
37
- .requestServerAuthCode(serverClientId)
38
- .requestEmail()
39
- .requestIdToken(serverClientId)
57
+ // 2. Authorization: server auth code
58
+ val authRequest = AuthorizationRequest.builder()
59
+ // matches the legacy DEFAULT_SIGN_IN + requestEmail() scopes, so the exchanged
60
+ // server auth code grants the same access as before the Credential Manager migration
61
+ .setRequestedScopes(
62
+ listOf(Scope(Scopes.OPEN_ID), Scope(Scopes.PROFILE), Scope(Scopes.EMAIL))
63
+ )
64
+ .requestOfflineAccess(serverClientId)
40
65
  .build()
41
- )
42
- activityResultLauncher.launch(googleSignInClient.signInIntent)
66
+ Identity.getAuthorizationClient(this@SignInWithGoogleActivity)
67
+ .authorize(authRequest)
68
+ .addOnSuccessListener { res ->
69
+ if (res.hasResolution()) {
70
+ authLauncher.launch(
71
+ IntentSenderRequest.Builder(res.pendingIntent!!.intentSender).build()
72
+ )
73
+ } else {
74
+ finishWith(res.serverAuthCode)
75
+ }
76
+ }
77
+ .addOnFailureListener { finishWith(null) }
78
+ }
79
+ }
80
+
81
+ private fun finishWith(serverAuthCode: String?) {
82
+ // a cancelled or failed authorization step yields no server auth code; report it as a
83
+ // failed sign in so callers get either an error or a complete account, as with the
84
+ // legacy GoogleSignIn flow, never an account without a server auth code
85
+ if (serverAuthCode == null) {
86
+ callback?.onSignInComplete(null, null)
87
+ } else {
88
+ callback?.onSignInComplete(idCredential, serverAuthCode)
89
+ }
90
+ finish()
43
91
  }
44
92
 
45
93
  /**
@@ -49,9 +97,13 @@ class SignInWithGoogleActivity : ComponentActivity() {
49
97
  /**
50
98
  * Notifies that sign in flow completed.
51
99
  *
52
- * @param account the account signed in or `null` if there were issues signing in.
100
+ * @param credential the account signed in or `null` if there were issues signing in.
101
+ * @param serverAuthCode the server auth code, always non-null when [credential] is non-null.
53
102
  */
54
- abstract fun onSignInComplete(@Nullable account: GoogleSignInAccount?)
103
+ abstract fun onSignInComplete(
104
+ credential: GoogleIdTokenCredential?,
105
+ serverAuthCode: String?
106
+ )
55
107
  }
56
108
 
57
109
  companion object {
@@ -1,8 +1,8 @@
1
1
  package com.margelo.nitro.swe.iternio.reactnativeautoplay.template
2
2
 
3
3
  import android.content.Intent
4
- import android.net.Uri
5
4
  import android.os.Bundle
5
+ import android.util.Base64
6
6
  import androidx.car.app.CarContext
7
7
  import androidx.car.app.model.Action
8
8
  import androidx.car.app.model.ActionStrip
@@ -18,7 +18,9 @@ import androidx.car.app.model.signin.ProviderSignInMethod
18
18
  import androidx.car.app.model.signin.QRCodeSignInMethod
19
19
  import androidx.car.app.model.signin.SignInTemplate
20
20
  import androidx.core.graphics.drawable.IconCompat
21
- import com.google.android.gms.auth.api.signin.GoogleSignInAccount
21
+ import androidx.core.net.toUri
22
+ import com.google.android.libraries.identity.googleid.GoogleIdTokenCredential
23
+ import com.margelo.nitro.NitroModules
22
24
  import com.margelo.nitro.swe.iternio.reactnativeautoplay.KeyboardType
23
25
  import com.margelo.nitro.swe.iternio.reactnativeautoplay.NitroAction
24
26
  import com.margelo.nitro.swe.iternio.reactnativeautoplay.NitroActionType
@@ -26,8 +28,8 @@ import com.margelo.nitro.swe.iternio.reactnativeautoplay.R
26
28
  import com.margelo.nitro.swe.iternio.reactnativeautoplay.SignInTemplateConfig
27
29
  import com.margelo.nitro.swe.iternio.reactnativeautoplay.SignInWithGoogleActivity
28
30
  import com.margelo.nitro.swe.iternio.reactnativeautoplay.TextInputType
31
+ import org.json.JSONObject
29
32
  import java.security.InvalidParameterException
30
- import androidx.core.net.toUri
31
33
 
32
34
  class SignInTemplate(
33
35
  context: CarContext, config: SignInTemplateConfig
@@ -115,32 +117,35 @@ class SignInTemplate(
115
117
  ).build()
116
118
  )
117
119
  setOnClickListener(ParkedOnlyOnClickListener.create {
118
- val extras = Bundle(1)
120
+ val extras = Bundle(2)
119
121
  extras.putBinder(
120
122
  SignInWithGoogleActivity.BINDER_KEY,
121
123
  object : SignInWithGoogleActivity.OnSignInComplete() {
122
- override fun onSignInComplete(account: GoogleSignInAccount?) {
123
- if (account == null) {
124
+ override fun onSignInComplete(
125
+ credential: GoogleIdTokenCredential?,
126
+ serverAuthCode: String?
127
+ ) {
128
+ if (credential == null) {
124
129
  googleSignIn.callback("Error signing in", null)
125
- } else {
126
- googleSignIn.callback(
127
- null,
128
- com.margelo.nitro.swe.iternio.reactnativeautoplay.GoogleSignInAccount(
129
- serverAuthCode = account.serverAuthCode,
130
- email = account.email,
131
- id = account.id,
132
- displayName = account.displayName,
133
- photoUrl = account.photoUrl?.toString(),
134
- idToken = account.idToken,
135
- givenName = account.givenName,
136
- familyName = account.familyName
137
- )
138
- )
130
+ return
139
131
  }
132
+ googleSignIn.callback(
133
+ null,
134
+ com.margelo.nitro.swe.iternio.reactnativeautoplay.GoogleSignInAccount(
135
+ serverAuthCode = serverAuthCode,
136
+ email = credential.id,
137
+ id = subFromIdToken(credential.idToken),
138
+ displayName = credential.displayName,
139
+ photoUrl = credential.profilePictureUri?.toString(),
140
+ idToken = credential.idToken,
141
+ givenName = credential.givenName,
142
+ familyName = credential.familyName
143
+ )
144
+ )
140
145
  }
141
146
  })
142
147
  extras.putString("serverClientId", googleSignIn.serverClientId)
143
- context.startActivity(
148
+ NitroModules.applicationContext?.startActivity(
144
149
  Intent().setClass(context, SignInWithGoogleActivity::class.java)
145
150
  .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK).putExtras(extras)
146
151
  )
@@ -211,4 +216,13 @@ class SignInTemplate(
211
216
  config.onPopped?.let { it() }
212
217
  templates.remove(templateId)
213
218
  }
219
+
220
+ private fun subFromIdToken(idToken: String): String? = runCatching {
221
+ val payload = idToken.split(".")[1]
222
+ val json = String(
223
+ Base64.decode(payload, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP),
224
+ Charsets.UTF_8
225
+ )
226
+ JSONObject(json).getString("sub")
227
+ }.getOrNull()
214
228
  }
@@ -8,5 +8,6 @@ export interface SignInTemplate extends HybridObject<{
8
8
  }> {
9
9
  createSignInTemplate(config: SignInTemplateConfig): void;
10
10
  updateTemplate(templateId: string, config: SignInTemplateConfig): Promise<void>;
11
+ isGoogleSignInAvailable(): boolean;
11
12
  }
12
13
  export {};
@@ -46,4 +46,9 @@ export declare class SignInTemplate extends Template<SignInTemplateConfig, SignI
46
46
  * @returns A promise that resolves when the template is updated.
47
47
  */
48
48
  updateTemplate(updatedConfig: SignInTemplateUpdateConfig): Promise<void> | undefined;
49
+ /**
50
+ * Whether "Sign in with Google" is usable, i.e. Google Play services 23.08+ is installed.
51
+ * Check this before creating a `SignInTemplate` with `GoogleSignIn` — creation throws otherwise.
52
+ */
53
+ static isGoogleSignInAvailable(): boolean;
49
54
  }
@@ -48,4 +48,11 @@ export class SignInTemplate extends Template {
48
48
  }
49
49
  return HybridSignInTemplate?.updateTemplate(this.id, nitroConfig);
50
50
  }
51
+ /**
52
+ * Whether "Sign in with Google" is usable, i.e. Google Play services 23.08+ is installed.
53
+ * Check this before creating a `SignInTemplate` with `GoogleSignIn` — creation throws otherwise.
54
+ */
55
+ static isGoogleSignInAvailable() {
56
+ return HybridSignInTemplate?.isGoogleSignInAvailable() ?? false;
57
+ }
51
58
  }
@@ -51,5 +51,12 @@ class SignInTemplate extends Template_1.Template {
51
51
  }
52
52
  return exports.HybridSignInTemplate?.updateTemplate(this.id, nitroConfig);
53
53
  }
54
+ /**
55
+ * Whether "Sign in with Google" is usable, i.e. Google Play services 23.08+ is installed.
56
+ * Check this before creating a `SignInTemplate` with `GoogleSignIn` — creation throws otherwise.
57
+ */
58
+ static isGoogleSignInAvailable() {
59
+ return exports.HybridSignInTemplate?.isGoogleSignInAvailable() ?? false;
60
+ }
54
61
  }
55
62
  exports.SignInTemplate = SignInTemplate;
@@ -144,5 +144,10 @@ namespace margelo::nitro::swe::iternio::reactnativeautoplay {
144
144
  return __promise;
145
145
  }();
146
146
  }
147
+ bool JHybridSignInTemplateSpec::isGoogleSignInAvailable() {
148
+ static const auto method = _javaPart->javaClassStatic()->getMethod<jboolean()>("isGoogleSignInAvailable");
149
+ auto __result = method(_javaPart);
150
+ return static_cast<bool>(__result);
151
+ }
147
152
 
148
153
  } // namespace margelo::nitro::swe::iternio::reactnativeautoplay
@@ -56,6 +56,7 @@ namespace margelo::nitro::swe::iternio::reactnativeautoplay {
56
56
  // Methods
57
57
  void createSignInTemplate(const SignInTemplateConfig& config) override;
58
58
  std::shared_ptr<Promise<void>> updateTemplate(const std::string& templateId, const SignInTemplateConfig& config) override;
59
+ bool isGoogleSignInAvailable() override;
59
60
 
60
61
  private:
61
62
  jni::global_ref<JHybridSignInTemplateSpec::JavaPart> _javaPart;
@@ -37,6 +37,10 @@ abstract class HybridSignInTemplateSpec: HybridObject() {
37
37
  @DoNotStrip
38
38
  @Keep
39
39
  abstract fun updateTemplate(templateId: String, config: SignInTemplateConfig): Promise<Unit>
40
+
41
+ @DoNotStrip
42
+ @Keep
43
+ abstract fun isGoogleSignInAvailable(): Boolean
40
44
 
41
45
  // Default implementation of `HybridObject.toString()`
42
46
  override fun toString(): String {
@@ -16,6 +16,7 @@ namespace margelo::nitro::swe::iternio::reactnativeautoplay {
16
16
  registerHybrids(this, [](Prototype& prototype) {
17
17
  prototype.registerHybridMethod("createSignInTemplate", &HybridSignInTemplateSpec::createSignInTemplate);
18
18
  prototype.registerHybridMethod("updateTemplate", &HybridSignInTemplateSpec::updateTemplate);
19
+ prototype.registerHybridMethod("isGoogleSignInAvailable", &HybridSignInTemplateSpec::isGoogleSignInAvailable);
19
20
  });
20
21
  }
21
22
 
@@ -53,6 +53,7 @@ namespace margelo::nitro::swe::iternio::reactnativeautoplay {
53
53
  // Methods
54
54
  virtual void createSignInTemplate(const SignInTemplateConfig& config) = 0;
55
55
  virtual std::shared_ptr<Promise<void>> updateTemplate(const std::string& templateId, const SignInTemplateConfig& config) = 0;
56
+ virtual bool isGoogleSignInAvailable() = 0;
56
57
 
57
58
  protected:
58
59
  // Hybrid Setup
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iternio/react-native-auto-play",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Android Auto and Apple CarPlay for react-native",
5
5
  "main": "lib/index",
6
6
  "module": "lib/index",
@@ -7,4 +7,5 @@ interface SignInTemplateConfig extends NitroTemplateConfig, NitroSignInTemplateC
7
7
  export interface SignInTemplate extends HybridObject<{ android: 'kotlin' }> {
8
8
  createSignInTemplate(config: SignInTemplateConfig): void;
9
9
  updateTemplate(templateId: string, config: SignInTemplateConfig): Promise<void>;
10
+ isGoogleSignInAvailable(): boolean;
10
11
  }
@@ -114,4 +114,12 @@ export class SignInTemplate extends Template<
114
114
 
115
115
  return HybridSignInTemplate?.updateTemplate(this.id, nitroConfig);
116
116
  }
117
+
118
+ /**
119
+ * Whether "Sign in with Google" is usable, i.e. Google Play services 23.08+ is installed.
120
+ * Check this before creating a `SignInTemplate` with `GoogleSignIn` — creation throws otherwise.
121
+ */
122
+ static isGoogleSignInAvailable(): boolean {
123
+ return HybridSignInTemplate?.isGoogleSignInAvailable() ?? false;
124
+ }
117
125
  }