Skip to content

Commit

Permalink
Support Composition API (#139)
Browse files Browse the repository at this point in the history
OKTA-645251 Support both Options API and Composition API
  • Loading branch information
denysoblohin-okta authored Oct 27, 2023
1 parent 846418d commit 5335310
Show file tree
Hide file tree
Showing 13 changed files with 227 additions and 139 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
# 5.7.0

### Features

- [#139](https://github.com/okta/okta-vue/pull/139)
- Plugin supports both Options API and Composition API
- `LoginCallback` component and test app migrated to Composition API

### Fixes

- [#139](https://github.com/okta/okta-vue/pull/139) Fixes `TypeError: 'set' on proxy: trap returned falsish for property 'authState'` when plugin is used in app with Composition API

# 5.6.0

### Fixes
Expand Down
39 changes: 34 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,9 @@ If a user does not have a valid session, then a new authorization flow will begi

### Show Login and Logout Buttons

In the relevant location in your application, you will want to provide `Login` and `Logout` buttons for the user. You can show/hide the correct button by using the injected reactive [authState][] property. For example:
In the relevant location in your application, you will want to provide `Login` and `Logout` buttons for the user. You can show/hide the correct button by using the injected reactive [authState][] property.

Example for Options API:
```typescript
// src/App.vue

Expand Down Expand Up @@ -188,8 +189,7 @@ export default defineComponent({
</script>
```

If you are using setup function or setup script, you can access the oktaAuth instance with `useAuth` composable.

If you are using Composition API, you can access the OktaAuth instance with `useAuth()` composable.
```typescript
// src/App.vue

Expand All @@ -205,7 +205,7 @@ If you are using setup function or setup script, you can access the oktaAuth ins
<script setup lang="ts">
import { useAuth } from '@okta/okta-vue';

const auth = useAuth();
const $auth = useAuth();

const login = async () => {
await auth.signInWithRedirect()
Expand All @@ -217,6 +217,16 @@ const logout = async () => {
</script>
```

If you have disabled Options API (use [`__VUE_OPTIONS_API__: false`](https://github.com/vuejs/core/blob/main/packages/vue/README.md#bundler-build-feature-flags)), you need to inject `okta.authState` and expose property named `authState` in setup in order to use `authState` in template:

```typescript
<script setup lang="ts">
import { ShallowRef, inject } from 'vue';
import { AuthState } from '@okta/okta-auth-js';
const authState = inject<ShallowRef<AuthState>>('okta.authState')
</script>
```

### Use the Access Token

When your users are authenticated, your Vue application has an access token that was issued by your Okta Authorization server. You can use this token to authenticate requests for resources on your server or API. As a hypothetical example, let's say you have an API that provides messages for a user. You could create a `MessageList` component that gets the access token and uses it to make an authenticated request to your server.
Expand Down Expand Up @@ -310,7 +320,26 @@ Note that `onAuthResume` has the same signature as `onAuthRequired`. If you do n

### `$auth`

This SDK works as a [Vue Plugin][]. It provides an instance of the [Okta Auth SDK][] to your components on the [globalProperties][]. You can access the [Okta Auth SDK][] instance by using `this.$auth` in your components.
This SDK works as a [Vue Plugin][]. It provides an instance of the [Okta Auth SDK][] to your components on the [globalProperties][]. For Options API you can access the [Okta Auth SDK][] instance by using `this.$auth` in your components. For Composition API you can access the OktaAuth instance with `useAuth()` composable.

```typescript
import { useAuth } from '@okta/okta-vue';
const $auth = useAuth();
```

### `authState`

This SDK provides reactive [authState][] property for your components. For Options API you can access the value by using `this.authState` in your components. For Composition API you can inject `okta.authState`:

```typescript
import { inject, ShallowRef } from 'vue';
import { AuthState } from '@okta/okta-auth-js';
const authState = inject<ShallowRef<AuthState>>('okta.authState');
// use authState.value
```

Note that if you have disabled Options API (with [`__VUE_OPTIONS_API__: false`](https://github.com/vuejs/core/blob/main/packages/vue/README.md#bundler-build-feature-flags)), you need to expose property `authState` in setup in order to use `authState` in template.


### `LoginCallback`

Expand Down
57 changes: 27 additions & 30 deletions src/components/LoginCallback.vue
Original file line number Diff line number Diff line change
Expand Up @@ -10,39 +10,36 @@
* See the License for the specific language governing permissions and limitations under the License.
*/

<script lang="ts">
import { h, ref, onBeforeMount, Slot } from 'vue'
import { useAuth } from '../okta-vue'
<script>
import { defineComponent, h } from 'vue'
export default defineComponent({
name: 'LoginCallback',
data() {
return {
error: null
};
},
async beforeMount () {
try {
await this.$auth.handleLoginRedirect();
} catch (e) {
const isInteractionRequiredError = this.$auth.isInteractionRequiredError || this.$auth.idx.isInteractionRequiredError;
if (isInteractionRequiredError(e)) {
const { onAuthResume, onAuthRequired } = this.$auth.options;
const callbackFn = onAuthResume || onAuthRequired;
if (callbackFn) {
callbackFn(this.$auth);
return;
export default {
setup(_props: {}, { slots }: { slots: { error?: Slot } }) {
const error = ref<string | null>(null);
const $auth = useAuth();
onBeforeMount(async () => {
try {
await $auth.handleLoginRedirect();
} catch (e) {
const isInteractionRequiredError = $auth.isInteractionRequiredError || $auth.idx.isInteractionRequiredError;
if (isInteractionRequiredError(e)) {
const { onAuthResume, onAuthRequired } = $auth.options;
const callbackFn = onAuthResume || onAuthRequired;
if (callbackFn) {
callbackFn($auth);
return;
}
}
error.value = e.toString();
}
this.error = e.toString();
}
},
render() {
if (this.$slots.error) {
return h('div', this.$slots.error({ error: this.error }));
});
return () => {
if (slots.error) {
return h('div', slots.error({ error: error.value }));
}
return error.value;
}
return this.error;
}
})
}
</script>
57 changes: 25 additions & 32 deletions src/okta-vue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@
* See the License for the specific language governing permissions and limitations under the License.
*/

import { App } from 'vue'
import { App, shallowRef, triggerRef, version } from 'vue'
import { Router, RouteLocationNormalized } from 'vue-router'
import { AuthSdkError, OktaAuth, AuthState, toRelativeUrl } from '@okta/okta-auth-js'
import { compare } from 'compare-versions';
import { OktaVueOptions, OnAuthRequiredFunction } from './types'
import { OktaVueOptions, OnAuthRequiredFunction, OktaAuthVue } from './types'

// constants are defined in webpack.config.js
declare const PACKAGE: {
Expand All @@ -26,9 +26,8 @@ declare const AUTH_JS: {
minSupportedVersion: string;
}

let _oktaAuth: OktaAuth
let _oktaAuth: OktaAuthVue
let _onAuthRequired: OnAuthRequiredFunction | undefined
let _router: Router
let originalUriTracker: string

const guardSecureRoute = async (authState: AuthState | null) => {
Expand Down Expand Up @@ -101,9 +100,10 @@ function install (app: App, {
if (!oktaAuth.options.restoreOriginalUri) {
oktaAuth.options.restoreOriginalUri = async (oktaAuth: OktaAuth, originalUri: string) => {
// If a router is available, provide a default implementation
if (_router) {
const $router: Router = app.config.globalProperties.$router;
if ($router) {
const path = toRelativeUrl(originalUri || '/', window.location.origin);
_router.replace({ path })
$router.replace({ path })
}
}
}
Expand All @@ -112,33 +112,26 @@ function install (app: App, {
// Also starts services
oktaAuth.start();

app.mixin({
data () {
return {
authState: oktaAuth.authStateManager.getAuthState()
}
},
beforeCreate () {
// assign router for the default restoreOriginalUri callback
_router = this.$router
},
created () {
// subscribe to the latest authState
this.authState = oktaAuth.authStateManager.getAuthState()
oktaAuth.authStateManager.subscribe(this.$_oktaVue_handleAuthStateUpdate)
},
beforeUnmount () {
oktaAuth.authStateManager.unsubscribe(this.$_oktaVue_handleAuthStateUpdate)
},
// private property naming convention follows
// https://vuejs.org/v2/style-guide/#Private-property-names-essential
methods: {
// eslint-disable-next-line @typescript-eslint/camelcase
async $_oktaVue_handleAuthStateUpdate (authState: AuthState) {
this.authState = Object.assign(this.authState || {}, authState)
// Subscribe to the latest authState
const authStateRef = shallowRef(oktaAuth.authStateManager.getAuthState())
const handleAuthStateUpdate = async function(authState: AuthState) {
authStateRef.value = authState
triggerRef(authStateRef)
}
oktaAuth.authStateManager.subscribe(handleAuthStateUpdate)

// Use mixin to support Options API
if (typeof __VUE_OPTIONS_API__ === 'undefined' || __VUE_OPTIONS_API__ === true) {
app.mixin({
computed: {
authState() {
return authStateRef.value
}
}
}
})
})
}
// Provide ref to authState to support Composition API
app.provide('okta.authState', authStateRef)

// add additional options to oktaAuth options
Object.assign(oktaAuth.options, {
Expand Down
15 changes: 13 additions & 2 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,21 @@ export interface OktaVueOptions {
onAuthResume?: OnAuthRequiredFunction;
}

export type OktaAuthVue = OktaAuth & {
isInteractionRequiredError?: (error: Error) => boolean;
options: {
onAuthRequired?: OnAuthRequiredFunction;
onAuthResume?: OnAuthRequiredFunction;
};
}

declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
$auth: OktaAuth;
$auth: OktaAuthVue;
authState: AuthState;
$_oktaVue_handleAuthStateUpdate: (authState: AuthState) => void;
}
}

declare global {
const __VUE_OPTIONS_API__: boolean;
}
35 changes: 25 additions & 10 deletions test/apps/test-harness/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<router-link to="/" tag="button" id='home-button'> Home </router-link>
<button v-if='authState && authState.isAuthenticated' v-on:click='logout' id='logout-button'> Logout </button>
<button v-else v-on:click='login' id='login-button'> Login </button>
<router-link v-if='!authState || !authState.isAuthenticated' to="/sessionToken" tag="button" id='session-login-button'> Session login </router-link>
<router-link id="protected-link" to="/protected" tag="button"> Protected </router-link>
<router-view v-slot="{ Component }">
<component :is="Component">
Expand All @@ -14,18 +15,32 @@
</div>
</template>


<script lang="ts">
import { defineComponent } from 'vue'
import { useAuth } from '@okta/okta-vue';
import { ShallowRef, inject } from 'vue';
import { AuthState } from '@okta/okta-auth-js';
export default {
setup() {
const authState = inject<ShallowRef<AuthState>>('okta.authState')
const $auth = useAuth()
const login = () => {
$auth.signInWithRedirect()
};
export default defineComponent({
name: 'app',
methods: {
login () {
this.$auth.signInWithRedirect()
},
async logout () {
await this.$auth.signOut()
const logout = async () => {
$auth.signOut();
};
return {
logout,
login,
// required if you've disabled Options API with `__VUE_OPTIONS_API__: false`
authState,
}
}
})
}
</script>

8 changes: 0 additions & 8 deletions test/apps/test-harness/src/components/Home.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,3 @@
<div class="home">
</div>
</template>

<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'Home'
})
</script>
Loading

0 comments on commit 5335310

Please sign in to comment.