diff --git a/src/addon/badges/badges.module.ts b/src/addon/badges/badges.module.ts
new file mode 100644
index 000000000..b4416dfa3
--- /dev/null
+++ b/src/addon/badges/badges.module.ts
@@ -0,0 +1,44 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { NgModule } from '@angular/core';
+import { AddonBadgesProvider } from './providers/badges';
+import { AddonBadgesUserHandler } from './providers/user-handler';
+import { AddonBadgesMyBadgesLinkHandler } from './providers/mybadges-link-handler';
+import { AddonBadgesBadgeLinkHandler } from './providers/badge-link-handler';
+import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate';
+import { CoreUserDelegate } from '@core/user/providers/user-delegate';
+
+@NgModule({
+    declarations: [
+    ],
+    imports: [
+    ],
+    providers: [
+        AddonBadgesProvider,
+        AddonBadgesUserHandler,
+        AddonBadgesMyBadgesLinkHandler,
+        AddonBadgesBadgeLinkHandler
+    ]
+})
+export class AddonBadgesModule {
+    constructor(userDelegate: CoreUserDelegate, userHandler: AddonBadgesUserHandler,
+        contentLinksDelegate: CoreContentLinksDelegate, myBadgesLinkHandler: AddonBadgesMyBadgesLinkHandler,
+        badgeLinkHandler: AddonBadgesBadgeLinkHandler) {
+
+        userDelegate.registerHandler(userHandler);
+        contentLinksDelegate.registerHandler(myBadgesLinkHandler);
+        contentLinksDelegate.registerHandler(badgeLinkHandler);
+    }
+}
diff --git a/src/addon/badges/lang/en.json b/src/addon/badges/lang/en.json
new file mode 100644
index 000000000..4443773c4
--- /dev/null
+++ b/src/addon/badges/lang/en.json
@@ -0,0 +1,13 @@
+{
+    "badgedetails": "Badge details",
+    "badges": "Badges",
+    "contact": "Contact",
+    "dateawarded": "Date issued",
+    "expired": "Expired",
+    "expirydate": "Expiry date",
+    "issuancedetails": "Badge expiry",
+    "issuerdetails": "Issuer details",
+    "issuername": "Issuer name",
+    "nobadges": "There are no badges available.",
+    "recipientdetails": "Recipient details"
+}
diff --git a/src/addon/badges/pages/issued-badge/issued-badge.html b/src/addon/badges/pages/issued-badge/issued-badge.html
new file mode 100644
index 000000000..e28898229
--- /dev/null
+++ b/src/addon/badges/pages/issued-badge/issued-badge.html
@@ -0,0 +1,87 @@
+<ion-header>
+    <ion-navbar>
+        <ion-title>{{badge.name}}</ion-title>
+    </ion-navbar>
+</ion-header>
+<ion-content>
+    <ion-refresher [enabled]="badgeLoaded" (ionRefresh)="refreshEvent($event)">
+        <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
+    </ion-refresher>
+    <core-loading [hideUntil]="badgeLoaded">
+
+        <ion-item-group>
+            <ion-item text-wrap class="item-avatar-center">
+                <img *ngIf="badge.badgeurl" class="avatar" [src]="badge.badgeurl" core-external-content [alt]="badge.name">
+                <ion-badge color="danger" *ngIf="badge.dateexpire && currentTime >= badge.dateexpire">
+                    {{ 'addon.badges.expired' | translate }}
+                </ion-badge>
+            </ion-item>
+        </ion-item-group>
+
+        <ion-item-group *ngIf="user.fullname">
+            <ion-item-divider color="light">
+                <h2>{{ 'addon.badges.recipientdetails' | translate}}</h2>
+            </ion-item-divider>
+            <ion-item text-wrap>
+                <h2>{{ 'core.name' | translate}}</h2>
+                <p>
+                    <core-format-text clean="true" [text]="user.fullname"></core-format-text>
+                </p>
+            </ion-item>
+        </ion-item-group>
+
+        <ion-item-group>
+            <ion-item-divider color="light">
+                <h2>{{ 'addon.badges.issuerdetails' | translate}}</h2>
+            </ion-item-divider>
+            <ion-item text-wrap *ngIf="badge.issuername">
+                <h2>{{ 'addon.badges.issuername' | translate}}</h2>
+                <p>
+                    <core-format-text clean="true" [text]="badge.issuername"></core-format-text>
+                </p>
+            </ion-item>
+            <ion-item text-wrap *ngIf="badge.issuercontact">
+                <h2>{{ 'addon.badges.contact' | translate}}</h2>
+                <p>
+                    <core-format-text clean="true" [text]="badge.issuercontact"></core-format-text>
+                </p>
+            </ion-item>
+        </ion-item-group>
+
+        <ion-item-group>
+            <ion-item-divider color="light">
+                <h2>{{ 'addon.badges.badgedetails' | translate}}</h2>
+            </ion-item-divider>
+            <ion-item text-wrap *ngIf="badge.name">
+                <h2>{{ 'core.name' | translate}}</h2>
+                <p>{{badge.name}}</p>
+            </ion-item>
+            <ion-item text-wrap *ngIf="badge.description">
+                <h2>{{ 'core.description' | translate}}</h2>
+                <p>
+                    <core-format-text clean="true" [text]="badge.description"></core-format-text>
+                </p>
+            </ion-item>
+            <ion-item text-wrap *ngIf="course.fullname">
+                <h2>{{ 'core.course' | translate}}</h2>
+                <p>
+                    <core-format-text clean="true" [text]="course.fullname"></core-format-text>
+                </p>
+            </ion-item>
+        </ion-item-group>
+
+        <ion-item-group>
+            <ion-item-divider color="light">
+                <h2>{{ 'addon.badges.issuancedetails' | translate}}</h2>
+            </ion-item-divider>
+            <ion-item text-wrap *ngIf="badge.dateissued">
+                <h2>{{ 'addon.badges.dateawarded' | translate}}</h2>
+                <p>{{badge.dateissued | coreToLocaleString }}</p>
+            </ion-item>
+            <ion-item text-wrap *ngIf="badge.dateexpire">
+                <h2>{{ 'addon.badges.expirydate' | translate}}</h2>
+                <p>{{badge.dateexpire | coreToLocaleString }}</p>
+            </ion-item>
+        </ion-item-group>
+    </core-loading>
+</ion-content>
diff --git a/src/addon/badges/pages/issued-badge/issued-badge.module.ts b/src/addon/badges/pages/issued-badge/issued-badge.module.ts
new file mode 100644
index 000000000..cdc621998
--- /dev/null
+++ b/src/addon/badges/pages/issued-badge/issued-badge.module.ts
@@ -0,0 +1,35 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { NgModule } from '@angular/core';
+import { IonicPageModule } from 'ionic-angular';
+import { TranslateModule } from '@ngx-translate/core';
+import { CoreComponentsModule } from '@components/components.module';
+import { CoreDirectivesModule } from '@directives/directives.module';
+import { CorePipesModule } from '@pipes/pipes.module';
+import { AddonBadgesIssuedBadgePage } from './issued-badge';
+
+@NgModule({
+    declarations: [
+        AddonBadgesIssuedBadgePage,
+    ],
+    imports: [
+        CoreComponentsModule,
+        CoreDirectivesModule,
+        CorePipesModule,
+        IonicPageModule.forChild(AddonBadgesIssuedBadgePage),
+        TranslateModule.forChild()
+    ],
+})
+export class AddonBadgesIssuedBadgePageModule {}
diff --git a/src/addon/badges/pages/issued-badge/issued-badge.ts b/src/addon/badges/pages/issued-badge/issued-badge.ts
new file mode 100644
index 000000000..7b8f6d5a1
--- /dev/null
+++ b/src/addon/badges/pages/issued-badge/issued-badge.ts
@@ -0,0 +1,114 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Component, ViewChild } from '@angular/core';
+import { IonicPage, Content, PopoverController, NavParams } from 'ionic-angular';
+import { TranslateService } from '@ngx-translate/core';
+import { AddonBadgesProvider } from '../../providers/badges';
+import { CoreTimeUtilsProvider } from '@providers/utils/time';
+import { CoreDomUtilsProvider } from '@providers/utils/dom';
+import { CoreSitesProvider } from '@providers/sites';
+import { CoreUserProvider } from '@core/user/providers/user';
+import { CoreCoursesProvider } from '@core/courses/providers/courses';
+
+/**
+ * Page that displays the list of calendar events.
+ */
+@IonicPage({ segment: 'addon-badges-issued-badge' })
+@Component({
+    selector: 'page-addon-badges-issued-badge',
+    templateUrl: 'issued-badge.html',
+})
+export class AddonBadgesIssuedBadgePage {
+    @ViewChild(Content) content: Content;
+
+    courseId: number;
+    userId: number;
+    badgeHash: string;
+    user: any = {};
+    course: any = {};
+    badge: any = {};
+
+    badgeLoaded = false;
+    currentTime = 0;
+
+    constructor(private translate: TranslateService, private badgesProvider: AddonBadgesProvider, navParams: NavParams,
+            private domUtils: CoreDomUtilsProvider, private timeUtils: CoreTimeUtilsProvider,
+            private sitesProvider: CoreSitesProvider, private userProvider: CoreUserProvider,
+            private coursesProvider: CoreCoursesProvider) {
+
+        this.courseId = navParams.get('courseId') || 0; // Use 0 for site badges.
+        this.userId = navParams.get('userId') || sitesProvider.getCurrentSite().getUserId();
+        this.badgeHash = navParams.get('badgeHash');
+    }
+
+    /**
+     * View loaded.
+     */
+    ionViewDidLoad(): void {
+
+        this.fetchIssuedBadge().finally(() => {
+            this.badgeLoaded = true;
+        });
+    }
+
+    /**
+     * Fetch the issued badge required for the view.
+     *
+     * @return {Promise<any>} Promise resolved when done.
+     */
+    fetchIssuedBadge(): Promise<any> {
+        const promises = [];
+
+        this.currentTime = this.timeUtils.timestamp();
+        let promise = this.userProvider.getProfile(this.userId, this.courseId, true).then((user) => {
+            this.user = user;
+        });
+        promises.push(promise);
+
+        promise = this.badgesProvider.getUserBadges(this.courseId, this.userId).then((badges) => {
+            badges.forEach((badge) => {
+                if (this.badgeHash == badge.uniquehash) {
+                    this.badge = badge;
+                    if (badge.courseid) {
+                        return this.coursesProvider.getUserCourse(badge.courseid, true).then((course) => {
+                            this.course = course;
+                        }).catch(() => {
+                            // Maybe an old deleted course.
+                            this.course = null;
+                        });
+                    }
+                }
+            });
+        }).catch((message) => {
+            this.domUtils.showErrorModalDefault(message, 'Error getting badge data.');
+        });
+        promises.push(promise);
+
+        return Promise.all(promises);
+    }
+
+    /**
+     * Refresh the badges.
+     *
+     * @param {any} refresher Refresher.
+     */
+    refreshBadges(refresher: any): void {
+        this.badgesProvider.invalidateUserBadges(this.courseId, this.userId).finally(() => {
+            this.fetchIssuedBadge().finally(() => {
+                refresher.complete();
+            });
+        });
+    }
+}
diff --git a/src/addon/badges/pages/user-badges/user-badges.html b/src/addon/badges/pages/user-badges/user-badges.html
new file mode 100644
index 000000000..9768f7901
--- /dev/null
+++ b/src/addon/badges/pages/user-badges/user-badges.html
@@ -0,0 +1,29 @@
+<ion-header>
+    <ion-navbar>
+        <ion-title>{{ 'addon.badges.badges' | translate }}</ion-title>
+    </ion-navbar>
+</ion-header>
+<core-split-view>
+    <ion-content>
+        <ion-refresher [enabled]="badgesLoaded" (ionRefresh)="refreshBadges($event)">
+            <ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
+        </ion-refresher>
+        <core-loading [hideUntil]="badgesLoaded">
+            <core-empty-box *ngIf="!badges || badges.length == 0" icon="trophy" [message]="'addon.badges.nobadges' | translate">
+            </core-empty-box>
+
+            <ion-list *ngIf="badges && badges.length" no-margin>
+                <a ion-item text-wrap *ngFor="let badge of badges" [title]="badge.name" (click)="loadIssuedBadge(badge.uniquehash)" [class.core-split-item-selected]="badge.uniquehash == badgeHash">
+                    <ion-avatar item-start>
+                        <img [src]="badge.badgeurl" [alt]="badge.name" item-start core-external-content>
+                    </ion-avatar>
+                    <h2><core-format-text [text]="badge.name"></core-format-text></h2>
+                    <p>{{ badge.dateissued | coreToLocaleString }}</p>
+                    <ion-badge item-end color="danger" *ngIf="badge.dateexpire && currentTime >= badge.dateexpire">
+                        {{ 'addon.badges.expired' | translate }}
+                    </ion-badge>
+                </a>
+            </ion-list>
+        </core-loading>
+    </ion-content>
+</core-split-view>
\ No newline at end of file
diff --git a/src/addon/badges/pages/user-badges/user-badges.module.ts b/src/addon/badges/pages/user-badges/user-badges.module.ts
new file mode 100644
index 000000000..2609cdf00
--- /dev/null
+++ b/src/addon/badges/pages/user-badges/user-badges.module.ts
@@ -0,0 +1,35 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { NgModule } from '@angular/core';
+import { IonicPageModule } from 'ionic-angular';
+import { TranslateModule } from '@ngx-translate/core';
+import { CoreComponentsModule } from '@components/components.module';
+import { CoreDirectivesModule } from '@directives/directives.module';
+import { CorePipesModule } from '@pipes/pipes.module';
+import { AddonBadgesUserBadgesPage } from './user-badges';
+
+@NgModule({
+    declarations: [
+        AddonBadgesUserBadgesPage,
+    ],
+    imports: [
+        CoreComponentsModule,
+        CoreDirectivesModule,
+        CorePipesModule,
+        IonicPageModule.forChild(AddonBadgesUserBadgesPage),
+        TranslateModule.forChild()
+    ],
+})
+export class AddonBadgesUserBadgesPageModule {}
diff --git a/src/addon/badges/pages/user-badges/user-badges.ts b/src/addon/badges/pages/user-badges/user-badges.ts
new file mode 100644
index 000000000..35029793c
--- /dev/null
+++ b/src/addon/badges/pages/user-badges/user-badges.ts
@@ -0,0 +1,104 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Component, ViewChild } from '@angular/core';
+import { IonicPage, Content, NavParams, NavController } from 'ionic-angular';
+import { TranslateService } from '@ngx-translate/core';
+import { AddonBadgesProvider } from '../../providers/badges';
+import { CoreTimeUtilsProvider } from '@providers/utils/time';
+import { CoreDomUtilsProvider } from '@providers/utils/dom';
+import { CoreSitesProvider } from '@providers/sites';
+import { CoreSplitViewComponent } from '@components/split-view/split-view';
+
+/**
+ * Page that displays the list of calendar events.
+ */
+@IonicPage({ segment: 'addon-badges-user-badges' })
+@Component({
+    selector: 'page-addon-badges-user-badges',
+    templateUrl: 'user-badges.html',
+})
+export class AddonBadgesUserBadgesPage {
+    @ViewChild(Content) content: Content;
+    @ViewChild(CoreSplitViewComponent) splitviewCtrl: CoreSplitViewComponent;
+
+    courseId: number;
+    userId: number;
+
+    badgesLoaded = false;
+    badges = [];
+    currentTime = 0;
+    badgeHash: string;
+
+    constructor(private translate: TranslateService, private badgesProvider: AddonBadgesProvider,
+            navParams: NavParams, private domUtils: CoreDomUtilsProvider, private timeUtils: CoreTimeUtilsProvider,
+            private sitesProvider: CoreSitesProvider) {
+
+        this.courseId = navParams.get('courseId') || 0; // Use 0 for site badges.
+        this.userId = navParams.get('userId') || sitesProvider.getCurrentSite().getUserId();
+    }
+
+    /**
+     * View loaded.
+     */
+    ionViewDidLoad(): void {
+
+        this.fetchBadges().finally(() => {
+            if (!this.badgeHash && this.splitviewCtrl.isOn() && this.badges.length > 0) {
+                // Take first and load it.
+                this.loadIssuedBadge(this.badges[0].uniquehash);
+            }
+            this.badgesLoaded = true;
+        });
+    }
+
+    /**
+     * Fetch all the badges required for the view.
+     *
+     * @return {Promise<any>} Promise resolved when done.
+     */
+    fetchBadges(): Promise<any> {
+        this.currentTime = this.timeUtils.timestamp();
+
+        return this.badgesProvider.getUserBadges(this.courseId, this.userId).then((badges) => {
+            this.badges = badges;
+        }).catch((message) => {
+            this.domUtils.showErrorModalDefault(message, 'Error getting badges data.');
+        });
+    }
+
+    /**
+     * Refresh the badges.
+     *
+     * @param {any} refresher Refresher.
+     */
+    refreshBadges(refresher: any): void {
+        this.badgesProvider.invalidateUserBadges(this.courseId, this.userId).finally(() => {
+            this.fetchBadges().finally(() => {
+                refresher.complete();
+            });
+        });
+    }
+
+    /**
+     * Navigate to a particular badge.
+     *
+     * @param {string} badgeHash Badge to load.
+     */
+    loadIssuedBadge(badgeHash: string): void {
+        this.badgeHash = badgeHash;
+        const params = {courseId: this.courseId, userId: this.userId, badgeHash: badgeHash};
+        this.splitviewCtrl.push('AddonBadgesIssuedBadgePage', params);
+    }
+}
diff --git a/src/addon/badges/providers/badge-link-handler.ts b/src/addon/badges/providers/badge-link-handler.ts
new file mode 100644
index 000000000..bcaf759e0
--- /dev/null
+++ b/src/addon/badges/providers/badge-link-handler.ts
@@ -0,0 +1,67 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable } from '@angular/core';
+import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler';
+import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate';
+import { CoreLoginHelperProvider } from '@core/login/providers/helper';
+import { AddonBadgesProvider } from './badges';
+
+/**
+ * Handler to treat links to user participants page.
+ */
+@Injectable()
+export class AddonBadgesBadgeLinkHandler extends CoreContentLinksHandlerBase {
+    name = 'AddonBadgesBadgeLinkHandler';
+    pattern = /\/badges\/badge\.php.*([\?\&]hash=)/;
+
+    constructor(private badgesProvider: AddonBadgesProvider, private loginHelper: CoreLoginHelperProvider) {
+        super();
+    }
+
+    /**
+     * Get the list of actions for a link (url).
+     *
+     * @param {string[]} siteIds List of sites the URL belongs to.
+     * @param {string} url The URL to treat.
+     * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
+     * @param {number} [courseId] Course ID related to the URL. Optional but recommended.
+     * @return {CoreContentLinksAction[]|Promise<CoreContentLinksAction[]>} List of (or promise resolved with list of) actions.
+     */
+    getActions(siteIds: string[], url: string, params: any, courseId?: number):
+            CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> {
+
+        return [{
+            action: (siteId, navCtrl?): void => {
+                // Always use redirect to make it the new history root (to avoid "loops" in history).
+                this.loginHelper.redirect('AddonBadgesIssuedBadgePage', {courseId: 0, badgeHash: params.hash}, siteId);
+            }
+        }];
+    }
+
+    /**
+     * Check if the handler is enabled for a certain site (site + user) and a URL.
+     * If not defined, defaults to true.
+     *
+     * @param {string} siteId The site ID.
+     * @param {string} url The URL to treat.
+     * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
+     * @param {number} [courseId] Course ID related to the URL. Optional but recommended.
+     * @return {boolean|Promise<boolean>} Whether the handler is enabled for the URL and site.
+     */
+    isEnabled(siteId: string, url: string, params: any, courseId?: number): boolean | Promise<boolean> {
+
+        return this.badgesProvider.isPluginEnabled(siteId);
+    }
+}
diff --git a/src/addon/badges/providers/badges.ts b/src/addon/badges/providers/badges.ts
new file mode 100644
index 000000000..2245ad1fb
--- /dev/null
+++ b/src/addon/badges/providers/badges.ts
@@ -0,0 +1,110 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable } from '@angular/core';
+import { CoreLoggerProvider } from '@providers/logger';
+import { CoreSitesProvider } from '@providers/sites';
+
+/**
+ * Service to handle badges.
+ */
+@Injectable()
+export class AddonBadgesProvider {
+    protected logger;
+    protected ROOT_CACHE_KEY = 'mmaBadges:';
+
+    constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider) {
+        this.logger = logger.getInstance('AddonBadgesProvider');
+    }
+
+    /**
+     * Returns whether or not the badge plugin is enabled for a certain site.
+     *
+     * This method is called quite often and thus should only perform a quick
+     * check, we should not be calling WS from here.
+     *
+     * @param {string} [siteId] Site ID. If not defined, current site.
+     * @return {Promise<boolean>} Promise resolved with true if enabled, false otherwise.
+     */
+    isPluginEnabled(siteId?: string): Promise<boolean> {
+
+        return this.sitesProvider.getSite(siteId).then((site) => {
+            if (!site.canUseAdvancedFeature('enablebadges')) {
+                return false;
+            } else if (!site.wsAvailable('core_course_get_user_navigation_options')) {
+                return false;
+            }
+
+            return true;
+        });
+    }
+
+    /**
+     * Get the cache key for the get badges call.
+     *
+     * @param {number} courseId ID of the course to get the badges from.
+     * @param {number} userId ID of the user to get the badges from.
+     * @return {string} Cache key.
+     */
+    protected getBadgesCacheKey(courseId: number, userId: number): string {
+        return this.ROOT_CACHE_KEY + 'badges:' + courseId + ':' + userId;
+    }
+
+    /**
+     * Get issued badges for a certain user in a course.
+     *
+     * @param {number} courseId ID of the course to get the badges from.
+     * @param {number} userId ID of the user to get the badges from.
+     * @param {string} [siteId] Site ID. If not defined, current site.
+     * @return {Promise<any>}Promise to be resolved when the badges are retrieved.
+     */
+    getUserBadges(courseId: number, userId: number, siteId?: string): Promise<any> {
+
+        this.logger.debug('Get badges for course ' + courseId);
+
+        return this.sitesProvider.getSite(siteId).then((site) => {
+
+            const data = {
+                    courseid : courseId,
+                    userid : userId
+                },
+                presets = {
+                    cacheKey: this.getBadgesCacheKey(courseId, userId)
+                };
+
+            return site.read('core_badges_get_user_badges', data, presets).then((response) => {
+                if (response && response.badges) {
+                    return response.badges;
+                } else {
+                    return Promise.reject(null);
+                }
+            });
+        });
+    }
+
+    /**
+     * Invalidate get badges WS call.
+     *
+     * @param {number} courseId Course ID.
+     * @param {number} userId ID of the user to get the badges from.
+     * @param {string} [siteId] Site ID. If not defined, current site.
+     * @return {Promise<any>} Promise resolved when data is invalidated.
+     */
+    invalidateUserBadges(courseId: number, userId: number, siteId?: string): Promise<any> {
+
+        return this.sitesProvider.getSite(siteId).then((site) => {
+            return site.invalidateWsCacheForKey(this.getBadgesCacheKey(courseId, userId));
+        });
+    }
+}
diff --git a/src/addon/badges/providers/mybadges-link-handler.ts b/src/addon/badges/providers/mybadges-link-handler.ts
new file mode 100644
index 000000000..59a3025da
--- /dev/null
+++ b/src/addon/badges/providers/mybadges-link-handler.ts
@@ -0,0 +1,68 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable } from '@angular/core';
+import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler';
+import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate';
+import { CoreLoginHelperProvider } from '@core/login/providers/helper';
+import { AddonBadgesProvider } from './badges';
+
+/**
+ * Handler to treat links to user participants page.
+ */
+@Injectable()
+export class AddonBadgesMyBadgesLinkHandler extends CoreContentLinksHandlerBase {
+    name = 'AddonBadgesMyBadgesLinkHandler';
+    featureName = '$mmUserDelegate_mmaBadges';
+    pattern = /\/badges\/mybadges\.php/;
+
+    constructor(private badgesProvider: AddonBadgesProvider, private loginHelper: CoreLoginHelperProvider) {
+        super();
+    }
+
+    /**
+     * Get the list of actions for a link (url).
+     *
+     * @param {string[]} siteIds List of sites the URL belongs to.
+     * @param {string} url The URL to treat.
+     * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
+     * @param {number} [courseId] Course ID related to the URL. Optional but recommended.
+     * @return {CoreContentLinksAction[]|Promise<CoreContentLinksAction[]>} List of (or promise resolved with list of) actions.
+     */
+    getActions(siteIds: string[], url: string, params: any, courseId?: number):
+            CoreContentLinksAction[] | Promise<CoreContentLinksAction[]> {
+
+        return [{
+            action: (siteId, navCtrl?): void => {
+                // Always use redirect to make it the new history root (to avoid "loops" in history).
+                this.loginHelper.redirect('AddonBadgesUserBadgesPage', {}, siteId);
+            }
+        }];
+    }
+
+    /**
+     * Check if the handler is enabled for a certain site (site + user) and a URL.
+     * If not defined, defaults to true.
+     *
+     * @param {string} siteId The site ID.
+     * @param {string} url The URL to treat.
+     * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
+     * @param {number} [courseId] Course ID related to the URL. Optional but recommended.
+     * @return {boolean|Promise<boolean>} Whether the handler is enabled for the URL and site.
+     */
+    isEnabled(siteId: string, url: string, params: any, courseId?: number): boolean | Promise<boolean> {
+
+        return this.badgesProvider.isPluginEnabled(siteId);
+    }
+}
diff --git a/src/addon/badges/providers/user-handler.ts b/src/addon/badges/providers/user-handler.ts
new file mode 100644
index 000000000..8e8ea9340
--- /dev/null
+++ b/src/addon/badges/providers/user-handler.ts
@@ -0,0 +1,75 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable } from '@angular/core';
+import { CoreUserDelegate, CoreUserProfileHandler, CoreUserProfileHandlerData } from '@core/user/providers/user-delegate';
+import { AddonBadgesProvider } from './badges';
+
+/**
+ * Profile badges handler.
+ */
+@Injectable()
+export class AddonBadgesUserHandler implements CoreUserProfileHandler {
+    name = 'mmaBadges';
+    priority = 50;
+    type = CoreUserDelegate.TYPE_NEW_PAGE;
+
+    constructor(protected badgesProvider: AddonBadgesProvider) { }
+
+    /**
+     * Check if handler is enabled.
+     *
+     * @return {Promise<boolean>} Always enabled.
+     */
+    isEnabled(): Promise<boolean> {
+        return this.badgesProvider.isPluginEnabled();
+    }
+
+    /**
+     * Check if handler is enabled for this user in this context.
+     *
+     * @param {any} user     User to check.
+     * @param {number} courseId Course ID.
+     * @param  {any} [navOptions] Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions.
+     * @param  {any} [admOptions] Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions.
+     * @return  {boolean}   True if enabled, false otherwise.
+     */
+    isEnabledForUser(user: any, courseId: number, navOptions?: any, admOptions?: any): boolean {
+
+        if (navOptions && typeof navOptions.badges != 'undefined') {
+            return navOptions.badges;
+        }
+
+        // If we reach here, it means we are opening the user site profile.
+        return true;
+    }
+
+    /**
+     * Returns the data needed to render the handler.
+     *
+     * @return {CoreUserProfileHandlerData} Data needed to render the handler.
+     */
+    getDisplayData(user: any, courseId: number): CoreUserProfileHandlerData {
+        return {
+            icon: 'trophy',
+            title: 'addon.badges.badges',
+            class: '',
+            action: (event, navCtrl, user, courseId): void => {
+                event.preventDefault();
+                event.stopPropagation();
+                navCtrl.push('AddonBadgesUserBadgesPage', {courseId: courseId, userId: user.id });
+            }
+        };
+    }
+}
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index 1ae090cd9..eaf546c1a 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -69,8 +69,9 @@ import { CoreSitePluginsModule } from '@core/siteplugins/siteplugins.module';
 import { CoreCompileModule } from '@core/compile/compile.module';
 
 // Addon modules.
+import { AddonBadgesModule } from '@addon/badges/badges.module';
 import { AddonCalendarModule } from '@addon/calendar/calendar.module';
-import { AddonCompetencyModule } from '../addon/competency/competency.module';
+import { AddonCompetencyModule } from '@addon/competency/competency.module';
 import { AddonUserProfileFieldModule } from '@addon/userprofilefield/userprofilefield.module';
 import { AddonFilesModule } from '@addon/files/files.module';
 import { AddonModBookModule } from '@addon/mod/book/book.module';
@@ -150,6 +151,7 @@ export const CORE_PROVIDERS: any[] = [
         CoreSettingsModule,
         CoreSitePluginsModule,
         CoreCompileModule,
+        AddonBadgesModule,
         AddonCalendarModule,
         AddonCompetencyModule,
         AddonUserProfileFieldModule,
diff --git a/src/core/user/providers/user-handler.ts b/src/core/user/providers/user-handler.ts
index 5ddb638b2..7414dd287 100644
--- a/src/core/user/providers/user-handler.ts
+++ b/src/core/user/providers/user-handler.ts
@@ -41,8 +41,8 @@ export class CoreUserProfileMailHandler implements CoreUserProfileHandler {
      *
      * @param {any} user     User to check.
      * @param {number} courseId Course ID.
-     * @param  {any} [navOptions] Course navigation options for current user. See $mmCourses#getUserNavigationOptions.
-     * @param  {any} [admOptions] Course admin options for current user. See $mmCourses#getUserAdministrationOptions.
+     * @param  {any} [navOptions] Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions.
+     * @param  {any} [admOptions] Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions.
      * @return  {boolean|Promise<boolean>}   Promise resolved with true if enabled, resolved with false otherwise.
      */
     isEnabledForUser(user: any, courseId: number, navOptions?: any, admOptions?: any): boolean | Promise<boolean> {