├── .eslintrc ├── .gitignore ├── README.md ├── package-lock.json ├── package.json └── src ├── errors └── TwitchExtensionHelperNotFound.js ├── index.js ├── modules ├── actions │ ├── actions.js │ ├── index.js │ └── mutations.js ├── bits │ ├── actions.js │ ├── index.js │ └── mutations.js ├── channelInfo │ ├── actions.js │ ├── index.js │ └── mutations.js ├── clientQueryParameters │ ├── actions.js │ ├── index.js │ └── mutations.js ├── configurationService │ ├── actions.js │ ├── index.js │ └── mutations.js ├── context │ ├── actions.js │ ├── index.js │ └── mutations.js ├── extension │ ├── actions.js │ ├── index.js │ └── mutations.js ├── features │ ├── actions.js │ ├── index.js │ └── mutations.js ├── highlight │ ├── actions.js │ ├── index.js │ └── mutations.js ├── index.js ├── position │ ├── actions.js │ ├── index.js │ └── mutations.js ├── pubsub │ ├── actions.js │ └── index.js └── userInfo │ ├── actions.js │ ├── index.js │ └── mutations.js ├── plugin └── index.js └── script └── init.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "ecmaFeatures": { 3 | "globalReturn": true, 4 | "jsx": true, 5 | "modules": true 6 | }, 7 | "env": { 8 | "browser": false, 9 | "es6": true, 10 | "node": true 11 | }, 12 | "globals": { 13 | "document": false, 14 | "escape": false, 15 | "navigator": false, 16 | "unescape": false, 17 | "window": false, 18 | "describe": true, 19 | "before": true, 20 | "it": true, 21 | "expect": true, 22 | "sinon": true 23 | }, 24 | "parser": "babel-eslint", 25 | "plugins": [], 26 | "rules": { 27 | // ... lots of lots of rules here 28 | } 29 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | .nyc_output 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 28 | node_modules 29 | 30 | # Remove some common IDE working directories 31 | .idea 32 | .vscode 33 | 34 | .DS_Store 35 | 36 | lib/* 37 | dist -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Twitch-Ext-Vuex 2 | 3 | An abstraction of the Twitch Extension helper for VueX 4 | 5 | ## Description 6 | 7 | This lib allow you to abstract the Twitch extension helper for your twitch extension project using Vue / Vuex. 8 | 9 | You can easily have a reactive Twitch extension without worrying about all the set up. 10 | 11 | 12 | ## Dependencies 13 | 14 | - Vue 15 | - Vuex 16 | 17 | ## How to use 18 | 19 | ### Use the Vue plugin 20 | By default the store will be accessible on the `twitch` module. 21 | 22 | You need to create the store before using it in the plugin. 23 | 24 | ``` 25 | import Vue from "vue"; 26 | import App from "./App"; 27 | import Vuex from "vuex"; 28 | import { ExtensionPlugin } from "twitchext-vuex"; 29 | 30 | Vue.use(Vuex); 31 | 32 | const store = new Vuex.Store(); 33 | 34 | Vue.use(ExtensionPlugin, { store }); 35 | 36 | new Vue({ 37 | el: "#app", 38 | store, 39 | render: h => h(App) 40 | }); 41 | ``` 42 | 43 | You will be able to to access the data as computed values 44 | ``` 45 | computed(){ 46 | ... 47 | opaqueId(){ 48 | return this.$twitchExtension.viewer.id 49 | } 50 | } 51 | ``` 52 | 53 | 54 | ## Store description 55 | 56 | ### General data 57 | 58 | The data structure is based on the - [Twitch Extension Helper](https://dev.twitch.tv/docs/extensions/reference/#javascript-helper) 59 | 60 | You can access the data using the same structure 61 | ``` 62 | ... 63 | computed:{ 64 | ... 65 | opaqueId(){ 66 | return this.$twitchExtension.viewer.id 67 | } 68 | } 69 | ``` 70 | 71 | ### Custom data 72 | 73 | #### bits 74 | `bits.getBitsAmount(sku:string):number` return the bits amount of given sku. Return 0 if the sku doesn't exist. 75 | 76 | `bits.hasOngoingBitTransaction:boolean` return if the user has a bits transaction going on. 77 | 78 | #### Channel 79 | `channel.initialized:boolean` return if the channel information have been set. 80 | 81 | `id:string` return the channel id of the stream. 82 | 83 | #### Configuration Service 84 | `configuration.initialized:boolean` return if the configuration service has been set. 85 | 86 | #### Position 87 | `position.initialized:boolean` return if the position information have been set. 88 | 89 | #### Viewer 90 | `viewer.initialized:boolean` return if the viewer information have been set. 91 | 92 | #### Context 93 | 94 | For the default data structure see the [OnContext method](https://dev.twitch.tv/docs/extensions/reference/#javascript-helper). 95 | 96 | The same structure is used to store the data under the `context` field. 97 | 98 | `context.initialized:boolean` return if the context information have been set. 99 | 100 | #### Query Params 101 | 102 | For the default data structure see the [Query Params doc](https://dev.twitch.tv/docs/extensions/reference/#client-query-parameters). 103 | 104 | Use `this.$twitchExtension.queryParams` to access the values. 105 | 106 | ## Values for testing 107 | When developing your extension, you might want to force some feature flags to true. 108 | 109 | These are only meant to make testing easier! Remember to turn these values off when sending to review. 110 | 111 | ### Force isBitEnabled to true 112 | 113 | Set the value 114 | ``` 115 | Vue.use(ExtensionPlugin, { store, settings:{ 116 | forceFlags:{ 117 | forceIsBitsEnabled:true 118 | } 119 | }}); 120 | ``` 121 | Access it 122 | ``` 123 | this.$store.state.forceIsBitsEnabled 124 | ``` 125 | 126 | ## Use a custom main module name 127 | You can use a custom VueX module name to match your project needs 128 | ``` 129 | Vue.use(Vuex); 130 | 131 | const store = new Vuex.Store(); 132 | 133 | Vue.use(ExtensionPlugin, { store, module:'extension' }); 134 | 135 | new Vue({ 136 | el: "#app", 137 | store, 138 | render: h => h(App) 139 | }); 140 | ``` 141 | 142 | ## Other frameworks 143 | 144 | ### React 145 | You can use my other package for React : [TwitchExt-React](https://www.npmjs.com/package/twitchext-react) 146 | 147 | 148 | ## Resources 149 | - [Twitch Documentation](https://dev.twitch.tv/docs/extensions/reference/#javascript-helper) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "twitchext-vuex", 3 | "version": "0.2.2", 4 | "description": "A Vuex module library for Twitch Extension", 5 | "source": "src/index.js", 6 | "main": "dist/index.js", 7 | "module": "dist/index.module.js", 8 | "unpkg": "dist/index.umd.js", 9 | "scripts": { 10 | "build": "microbundle", 11 | "dev": "microbundle watch" 12 | }, 13 | "author": "", 14 | "license": "ISC", 15 | "devDependencies": { 16 | "microbundle": "^0.12.3", 17 | "vue": "^2.6.11", 18 | "vuex": "^3.5.1" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/Breci/twitch-ext-vuex" 23 | }, 24 | "keywords": [ 25 | "es6", 26 | "library", 27 | "umd", 28 | "commonjs", 29 | "vuex", 30 | "vue", 31 | "vuejs", 32 | "vue.js", 33 | "vuex", 34 | "twitch", 35 | "twitchext" 36 | ], 37 | "peerDependencies": { 38 | "vue": "2.x", 39 | "vuex": "3.x" 40 | }, 41 | "files": [ 42 | "dist/index.js" 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /src/errors/TwitchExtensionHelperNotFound.js: -------------------------------------------------------------------------------- 1 | export default class TwitchExtensionHelperNotFound extends Error {} 2 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | ActionsModule, 3 | BitsModule, 4 | ChannelInfoModule, 5 | ClientQueryParametersModule, 6 | ConfigurationServiceModule, 7 | ContextModule, 8 | ExtensionModule, 9 | FeaturesModule, 10 | HighlightModule, 11 | PositionModule, 12 | UserInfoModule, 13 | PubSubModule 14 | } from "./modules"; 15 | import initScript from "./script/init"; 16 | import plugin from "./plugin"; 17 | 18 | const store = { 19 | state:{ 20 | forceIsBitsEnabled:false, 21 | }, 22 | actions:{ 23 | setExtensionForceIsBitsEnabled(context,value) { 24 | context.commit('SET_FORCE_IS_BITS_ENABLED',value) 25 | } 26 | }, 27 | mutations:{ 28 | SET_FORCE_IS_BITS_ENABLED(state,value){ 29 | state.forceIsBitsEnabled = value 30 | } 31 | }, 32 | modules: { 33 | viewer: UserInfoModule, 34 | channel: ChannelInfoModule, 35 | context: ContextModule, 36 | configuration: ConfigurationServiceModule, 37 | queryParams: ClientQueryParametersModule, 38 | bits: BitsModule, 39 | features: FeaturesModule, 40 | highlight: HighlightModule, 41 | position: PositionModule, 42 | actions: ActionsModule, 43 | extension: ExtensionModule, 44 | pubsub: PubSubModule 45 | } 46 | }; 47 | 48 | export { 49 | UserInfoModule, 50 | ChannelInfoModule, 51 | ContextModule, 52 | ConfigurationServiceModule, 53 | ClientQueryParametersModule, 54 | BitsModule, 55 | FeaturesModule, 56 | HighlightModule, 57 | PositionModule, 58 | ActionsModule, 59 | PubSubModule 60 | } from "./modules"; 61 | 62 | export const DefaultStore = store; 63 | export const ExtensionPlugin = plugin; 64 | export const LinkHelperToStore = initScript.init 65 | 66 | export default { 67 | store, 68 | link: initScript.init, 69 | plugin: plugin, 70 | modules: { 71 | UserInfoModule, 72 | ChannelInfoModule, 73 | ContextModule, 74 | ConfigurationServiceModule, 75 | ClientQueryParametersModule, 76 | BitsModule, 77 | FeaturesModule, 78 | HighlightModule, 79 | PositionModule, 80 | ActionsModule 81 | } 82 | }; 83 | -------------------------------------------------------------------------------- /src/modules/actions/actions.js: -------------------------------------------------------------------------------- 1 | export default { 2 | followChannel(context, channelName) { 3 | window.Twitch.ext.actions.followChannel(channelName); 4 | }, 5 | minimize(context) { 6 | window.Twitch.ext.actions.minimize(); 7 | }, 8 | onFollow(context, callback) { 9 | window.Twitch.ext.actions.onFollow(callback); 10 | }, 11 | requestIdShare(context) { 12 | window.Twitch.ext.actions.requestIdShare(); 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /src/modules/actions/index.js: -------------------------------------------------------------------------------- 1 | import actions from "./actions"; 2 | import mutations from "./mutations"; 3 | 4 | export const ActionsModule = { 5 | state: {}, 6 | mutations, 7 | actions 8 | }; 9 | -------------------------------------------------------------------------------- /src/modules/actions/mutations.js: -------------------------------------------------------------------------------- 1 | export default { 2 | } -------------------------------------------------------------------------------- /src/modules/bits/actions.js: -------------------------------------------------------------------------------- 1 | export default { 2 | getProducts() { 3 | return window.Twitch.ext.bits.getProducts() 4 | }, 5 | onTransactionCancelled({dispatch}, callback) { 6 | window.Twitch.ext.bits.onTransactionCancelled(() => { 7 | dispatch('setHasOngoingBitTransaction', false) 8 | callback(); 9 | }) 10 | }, 11 | onTransactionComplete({dispatch}, callback) { 12 | window.Twitch.ext.bits.onTransactionComplete(transaction => { 13 | dispatch('setHasOngoingBitTransaction', false) 14 | callback(transaction); 15 | }) 16 | }, 17 | setUseLoopBack({}, setUseLoopBack) { 18 | window.Twitch.ext.bits.setUseLoopBack(setUseLoopBack); 19 | }, 20 | showBitsBalance() { 21 | window.Twitch.ext.bits.showBitsBalance(); 22 | }, 23 | useBits({dispatch}, sku) { 24 | dispatch('setHasOngoingBitTransaction', true) 25 | window.Twitch.ext.bits.useBits(sku); 26 | }, 27 | setHasOngoingBitTransaction({commit}, hasOngoingBitTransaction) { 28 | commit('SET_HAS_ONGOING_BIT_TRANSACTION', hasOngoingBitTransaction) 29 | }, 30 | async getBitsAmount({dispatch}, sku) { 31 | const product = await dispatch('getProducts').find(product => product.sku === sku); 32 | return product ? product.cost.amount : 0; 33 | } 34 | } -------------------------------------------------------------------------------- /src/modules/bits/index.js: -------------------------------------------------------------------------------- 1 | import actions from "./actions"; 2 | import mutations from "./mutations"; 3 | 4 | export const BitsModule = { 5 | state: { 6 | bitProducts: [], 7 | hasOngoingBitTransaction: false 8 | }, 9 | mutations, 10 | actions, 11 | getters: { 12 | getBitsAmount: state => async sku => { 13 | const product = state.bitProducts.find(product => product.sku === sku); 14 | return (product && product.cost.amount) || 0; 15 | } 16 | } 17 | }; 18 | -------------------------------------------------------------------------------- /src/modules/bits/mutations.js: -------------------------------------------------------------------------------- 1 | export default { 2 | SET_HAS_ONGOING_BIT_TRANSACTION(state,hasOngoingBitTransaction){ 3 | state.hasOngoingBitTransaction = hasOngoingBitTransaction; 4 | } 5 | } -------------------------------------------------------------------------------- /src/modules/channelInfo/actions.js: -------------------------------------------------------------------------------- 1 | 2 | export default { 3 | setChannelId (context,channelId) { 4 | if (channelId){ 5 | context.commit('SET_CHANNEL_ID',channelId) 6 | context.commit('SET_CHANNEL_INFO_INITIALIZED'); 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /src/modules/channelInfo/index.js: -------------------------------------------------------------------------------- 1 | import actions from "./actions"; 2 | import mutations from "./mutations"; 3 | 4 | export const ChannelInfoModule = { 5 | state: { 6 | initialized: false, 7 | channelId: "" 8 | }, 9 | getters: {}, 10 | mutations, 11 | actions 12 | }; 13 | -------------------------------------------------------------------------------- /src/modules/channelInfo/mutations.js: -------------------------------------------------------------------------------- 1 | export default { 2 | SET_CHANNEL_ID (state,channelId) { 3 | state.channelId = channelId; 4 | }, 5 | SET_CHANNEL_INFO_INITIALIZED(state){ 6 | state.initialized = true; 7 | } 8 | } -------------------------------------------------------------------------------- /src/modules/clientQueryParameters/actions.js: -------------------------------------------------------------------------------- 1 | export default { 2 | updateQueryParams(context){ 3 | context.commit('UPDATE_QUERY_PARAMETERS') 4 | }, 5 | } -------------------------------------------------------------------------------- /src/modules/clientQueryParameters/index.js: -------------------------------------------------------------------------------- 1 | import actions from "./actions"; 2 | import mutations from "./mutations"; 3 | 4 | export const ClientQueryParametersModule = { 5 | state: { 6 | anchor: "", 7 | language: "", 8 | locale: "", 9 | mode: "", 10 | platform: "", 11 | popout: false, 12 | state: "" 13 | }, 14 | mutations, 15 | actions 16 | }; 17 | -------------------------------------------------------------------------------- /src/modules/clientQueryParameters/mutations.js: -------------------------------------------------------------------------------- 1 | export default { 2 | UPDATE_QUERY_PARAMETERS(state) { 3 | const urlParams = new URLSearchParams(window.location.search); 4 | state.anchor = urlParams.get("anchor"); 5 | state.language = urlParams.get("language"); 6 | state.locale = urlParams.get("locale"); 7 | state.mode = urlParams.get("mode"); 8 | state.platform = urlParams.get("platform"); 9 | state.popout = urlParams.get("popout"); 10 | state.state = urlParams.get("state"); 11 | state.legacyComponentDesign = urlParams.get("legacyComponentDesign") 12 | } 13 | } -------------------------------------------------------------------------------- /src/modules/configurationService/actions.js: -------------------------------------------------------------------------------- 1 | export default { 2 | updateConfiguration(context, {developer, broadcaster, global}) { 3 | context.dispatch('updateGlobalSegment', global); 4 | context.dispatch('updateDeveloperSegment', developer); 5 | context.dispatch('updateBroadcasterSegment', broadcaster); 6 | context.commit('SET_CONFIGURATION_SERVICE_INITIALIZED') 7 | }, 8 | updateGlobalSegment(context, segment) { 9 | context.commit('UPDATE_GLOBAL', segment) 10 | }, 11 | updateDeveloperSegment(context, segment) { 12 | context.commit('UPDATE_DEVELOPER', segment) 13 | }, 14 | updateBroadcasterSegment(context, segment) { 15 | context.commit('UPDATE_BROADCASTER', segment) 16 | }, 17 | setSegment(context, {segment, version, content}) { 18 | const _content = typeof content === 'string' ? content : JSON.stringify(content) 19 | window.Twitch.ext.configuration.set(segment, version, _content) 20 | }, 21 | onConfigurationChanged(context, callback) { 22 | window.Twitch.ext.configuration.onChanged(callback); 23 | } 24 | } -------------------------------------------------------------------------------- /src/modules/configurationService/index.js: -------------------------------------------------------------------------------- 1 | import actions from "./actions"; 2 | import mutations from "./mutations"; 3 | 4 | export const ConfigurationServiceModule = { 5 | state: { 6 | initialized: false, 7 | developer: null, 8 | global: null, 9 | broadcaster: null 10 | }, 11 | getters: { 12 | globalConfigurationServiceSegment(state) { 13 | if (state.global && state.global.content) { 14 | try { 15 | return JSON.parse(state.global.content); 16 | } catch (e) { 17 | return state.global.content; 18 | } 19 | } else { 20 | return null; 21 | } 22 | }, 23 | developerConfigurationServiceSegment(state) { 24 | if (state.developer && state.developer.content) { 25 | try { 26 | return JSON.parse(state.developer.content); 27 | } catch (e) { 28 | return state.developer.content; 29 | } 30 | } else { 31 | return null; 32 | } 33 | }, 34 | broadcasterConfigurationServiceSegment(state) { 35 | if (state.broadcaster && state.broadcaster.content) { 36 | try { 37 | return JSON.parse(state.broadcaster.content); 38 | } catch (e) { 39 | return state.broadcaster.content; 40 | } 41 | } else { 42 | return null; 43 | } 44 | } 45 | }, 46 | mutations, 47 | actions 48 | }; 49 | -------------------------------------------------------------------------------- /src/modules/configurationService/mutations.js: -------------------------------------------------------------------------------- 1 | export default { 2 | UPDATE_GLOBAL(state,segment) { 3 | state.global = segment; 4 | }, 5 | UPDATE_DEVELOPER(state,segment) { 6 | state.developer = segment; 7 | }, 8 | UPDATE_BROADCASTER(state,segment) { 9 | state.broadcaster = segment; 10 | }, 11 | SET_CONFIGURATION_SERVICE_INITIALIZED(state){ 12 | state.initialized = true; 13 | } 14 | } -------------------------------------------------------------------------------- /src/modules/context/actions.js: -------------------------------------------------------------------------------- 1 | export default { 2 | updateContext (context,contextData) { 3 | context.commit('UPDATE_CONTEXT',contextData) 4 | context.commit('SET_CONTEXT_INITIALIZED',contextData) 5 | } 6 | } -------------------------------------------------------------------------------- /src/modules/context/index.js: -------------------------------------------------------------------------------- 1 | import actions from "./actions"; 2 | import mutations from "./mutations"; 3 | 4 | export const ContextModule = { 5 | state: { 6 | initialized: false, 7 | arePlayerControlsVisible: false, 8 | bitrate: 0, 9 | bufferSize: 0, 10 | displayResolution: "", 11 | game: "", 12 | hlsLatencyBroadcaster: 0, 13 | hostingInfo: null, 14 | isFullScreen: false, 15 | isMuted: false, 16 | isPaused: false, 17 | isTheatreMode: false, 18 | language: "", 19 | mode: "", 20 | playbackMode: "", 21 | theme: "", 22 | videoResolution: "", 23 | volume: 0 24 | }, 25 | mutations, 26 | actions 27 | }; 28 | -------------------------------------------------------------------------------- /src/modules/context/mutations.js: -------------------------------------------------------------------------------- 1 | export default { 2 | UPDATE_CONTEXT(state,{context,contextFields}) { 3 | contextFields.forEach(field=>{ 4 | state[field] = context[field]; 5 | }); 6 | }, 7 | SET_CONTEXT_INITIALIZED(state){ 8 | state.initialized = true; 9 | } 10 | } -------------------------------------------------------------------------------- /src/modules/extension/actions.js: -------------------------------------------------------------------------------- 1 | export default { 2 | setExtensionInfo(context) { 3 | context.commit('SET_EXTENSION_INFO') 4 | } 5 | } -------------------------------------------------------------------------------- /src/modules/extension/index.js: -------------------------------------------------------------------------------- 1 | import actions from "./actions"; 2 | import mutations from "./mutations"; 3 | 4 | export const ExtensionModule = { 5 | state: { 6 | version: 0, 7 | environment: "production" 8 | }, 9 | mutations, 10 | actions 11 | }; 12 | -------------------------------------------------------------------------------- /src/modules/extension/mutations.js: -------------------------------------------------------------------------------- 1 | export default { 2 | SET_EXTENSION_INFO(state) { 3 | state.version = window.Twitch.ext.version; 4 | state.environment = window.Twitch.ext.environment; 5 | } 6 | } -------------------------------------------------------------------------------- /src/modules/features/actions.js: -------------------------------------------------------------------------------- 1 | export default { 2 | updateFeatures(context,forceFlags) { 3 | context.commit('UPDATE_FEATURES',forceFlags) 4 | } 5 | } -------------------------------------------------------------------------------- /src/modules/features/index.js: -------------------------------------------------------------------------------- 1 | import actions from "./actions"; 2 | import mutations from "./mutations"; 3 | 4 | export const FeaturesModule = { 5 | state: { 6 | isChatEnabled: false, 7 | isBitsEnabled: false, 8 | isSubscriptionStatusAvailable: false 9 | }, 10 | mutations, 11 | actions 12 | }; 13 | -------------------------------------------------------------------------------- /src/modules/features/mutations.js: -------------------------------------------------------------------------------- 1 | export default { 2 | /** 3 | * @param state 4 | * @param forceFlags settings to force flags to a specific value 5 | */ 6 | UPDATE_FEATURES(state, forceFlags) { 7 | state.isChatEnabled = window.Twitch.ext.features.isChatEnabled; 8 | state.isSubscriptionStatusAvailable = window.Twitch.ext.features.isSubscriptionStatusAvailable; 9 | state.isBitsEnabled = ( forceFlags && forceFlags.forceIsBitsEnabled ) || window.Twitch.ext.features.isBitsEnabled; 10 | } 11 | } -------------------------------------------------------------------------------- /src/modules/highlight/actions.js: -------------------------------------------------------------------------------- 1 | export default { 2 | updateHighlight (context,isHighlighted) { 3 | context.commit('UPDATE_HIGHLIGHT',isHighlighted) 4 | } 5 | } -------------------------------------------------------------------------------- /src/modules/highlight/index.js: -------------------------------------------------------------------------------- 1 | import actions from "./actions"; 2 | import mutations from "./mutations"; 3 | 4 | export const HighlightModule = { 5 | state: { 6 | isHighlighted: false 7 | }, 8 | mutations, 9 | actions 10 | }; 11 | -------------------------------------------------------------------------------- /src/modules/highlight/mutations.js: -------------------------------------------------------------------------------- 1 | export default { 2 | UPDATE_HIGHLIGHT(state,isHighlighted) { 3 | state.isHighlighted = isHighlighted; 4 | } 5 | } -------------------------------------------------------------------------------- /src/modules/index.js: -------------------------------------------------------------------------------- 1 | import { UserInfoModule } from "./userInfo"; 2 | import { ChannelInfoModule } from "./channelInfo"; 3 | import { ContextModule } from "./context"; 4 | import { HighlightModule } from "./highlight"; 5 | import { PositionModule } from "./position"; 6 | import { ConfigurationServiceModule } from "./configurationService"; 7 | import { ClientQueryParametersModule } from "./clientQueryParameters"; 8 | import { FeaturesModule } from "./features"; 9 | import { BitsModule } from "./bits"; 10 | import { ActionsModule } from "./actions"; 11 | import { ExtensionModule } from "./extension"; 12 | import { PubSubModule } from "./pubsub"; 13 | 14 | export { 15 | UserInfoModule, 16 | ChannelInfoModule, 17 | ContextModule, 18 | HighlightModule, 19 | PositionModule, 20 | ConfigurationServiceModule, 21 | ClientQueryParametersModule, 22 | FeaturesModule, 23 | BitsModule, 24 | ActionsModule, 25 | ExtensionModule, 26 | PubSubModule 27 | }; 28 | -------------------------------------------------------------------------------- /src/modules/position/actions.js: -------------------------------------------------------------------------------- 1 | export default { 2 | updatePosition (context,position) { 3 | context.commit('UPDATE_POSITION',position) 4 | context.commit('SET_POSITION_INITIALIZED') 5 | } 6 | } -------------------------------------------------------------------------------- /src/modules/position/index.js: -------------------------------------------------------------------------------- 1 | import actions from "./actions"; 2 | import mutations from "./mutations"; 3 | 4 | export const PositionModule = { 5 | state: { 6 | initialized: false, 7 | position: null 8 | }, 9 | mutations, 10 | actions 11 | }; 12 | -------------------------------------------------------------------------------- /src/modules/position/mutations.js: -------------------------------------------------------------------------------- 1 | export default { 2 | UPDATE_POSITION(state,position) { 3 | state.position = position; 4 | }, 5 | SET_POSITION_INITIALIZED(state){ 6 | state.initialized = true; 7 | } 8 | } -------------------------------------------------------------------------------- /src/modules/pubsub/actions.js: -------------------------------------------------------------------------------- 1 | export default { 2 | sendPubSubMessage(context, {target,contentType,message}) { 3 | window.Twitch.ext.send(target, contentType, message) 4 | }, 5 | listenPubSubMessage(context,{target,callback}) { 6 | window.Twitch.ext.listen(target, callback) 7 | }, 8 | unlistenPubSubMessage(context,{target,callback}) { 9 | window.Twitch.ext.unlisten(target, callback) 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /src/modules/pubsub/index.js: -------------------------------------------------------------------------------- 1 | import actions from "./actions"; 2 | 3 | export const PubSubModule = { 4 | actions 5 | }; 6 | -------------------------------------------------------------------------------- /src/modules/userInfo/actions.js: -------------------------------------------------------------------------------- 1 | export default { 2 | updateToken (context,token) { 3 | context.commit('UPDATE_TOKEN',token) 4 | }, 5 | updateIsLinked (context,isLinked) { 6 | context.commit('UPDATE_IS_LINKED',isLinked) 7 | }, 8 | updateRole (context,role) { 9 | context.commit('UPDATE_ROLE',role) 10 | }, 11 | updateUserId (context,userId) { 12 | context.commit('UPDATE_USER_ID',userId) 13 | }, 14 | updateOpaqueId (context,opaqueId) { 15 | context.commit('UPDATE_OPAQUE_ID',opaqueId) 16 | }, 17 | updateSubscriptionStatus (context,subscriptionStatus) { 18 | context.commit('UPDATE_SUBSCRIPTION_STATUS',subscriptionStatus) 19 | }, 20 | updateUserInfo({ dispatch, commit }){ 21 | dispatch('updateToken', window.Twitch.ext.viewer.sessionToken) 22 | dispatch('updateIsLinked', window.Twitch.ext.viewer.isLinked) 23 | dispatch('updateRole', window.Twitch.ext.viewer.role) 24 | dispatch('updateUserId', window.Twitch.ext.viewer.id) 25 | dispatch('updateOpaqueId', window.Twitch.ext.viewer.opaqueId) 26 | dispatch('updateSubscriptionStatus', window.Twitch.ext.viewer.subscriptionStatus) 27 | commit('SET_USER_INFO_INITIALIZED') 28 | } 29 | } -------------------------------------------------------------------------------- /src/modules/userInfo/index.js: -------------------------------------------------------------------------------- 1 | import actions from "./actions"; 2 | import mutations from "./mutations"; 3 | 4 | export const UserInfoModule = { 5 | state: { 6 | initialized: false, 7 | opaqueId: "", 8 | userId: null, 9 | role: "", 10 | isLinked: false, 11 | sessionToken: "", 12 | subscriptionStatus: null 13 | }, 14 | mutations, 15 | actions 16 | }; 17 | -------------------------------------------------------------------------------- /src/modules/userInfo/mutations.js: -------------------------------------------------------------------------------- 1 | export default { 2 | UPDATE_TOKEN (state,token) { 3 | state.sessionToken = token; 4 | }, 5 | UPDATE_IS_LINKED (state,isLinked) { 6 | state.isLinked = isLinked; 7 | }, 8 | UPDATE_ROLE (state,role) { 9 | state.role = role; 10 | }, 11 | UPDATE_USER_ID (state,userId) { 12 | state.userId = userId; 13 | }, 14 | UPDATE_OPAQUE_ID (state,opaqueId) { 15 | state.opaqueId = opaqueId; 16 | }, 17 | UPDATE_SUBSCRIPTION_STATUS (state,subscriptionStatus) { 18 | state.subscriptionStatus = subscriptionStatus 19 | }, 20 | SET_USER_INFO_INITIALIZED(state){ 21 | state.initialized = true; 22 | } 23 | } -------------------------------------------------------------------------------- /src/plugin/index.js: -------------------------------------------------------------------------------- 1 | import initScript from "../script/init"; 2 | import { DefaultStore } from "../index"; 3 | 4 | const DEFAULT_MODULE_NAME = "twitch"; 5 | 6 | const plugin = { 7 | install(Vue, { module = DEFAULT_MODULE_NAME, store, settings = {} }) { 8 | store.registerModule(module, DefaultStore); 9 | initScript.init(store, settings); 10 | Vue.prototype.$twitchExtension = { 11 | get version() { 12 | return store.state[module].extension.version; 13 | }, 14 | get environment() { 15 | return store.state[module].extension.environment; 16 | }, 17 | onAuthorized(callback) { 18 | return store.dispatch("onAuthorized", callback); 19 | }, 20 | onContext(callback) { 21 | return store.dispatch("onContext", callback); 22 | }, 23 | onError(callback) { 24 | return store.dispatch("onError", callback); 25 | }, 26 | onHighlightChanged(callback) { 27 | return store.dispatch("onHighlightChanged", callback); 28 | }, 29 | onPositionChanged(callback) { 30 | return store.dispatch("onPositionChanged", callback); 31 | }, 32 | onVisibilityChanged(callback) { 33 | return store.dispatch("onVisibilityChanged", callback); 34 | }, 35 | send(target, contentType, message) { 36 | return store.dispatch("sendPubSubMessage", { 37 | target, 38 | contentType, 39 | message 40 | }); 41 | }, 42 | listen(target, callback) { 43 | return store.dispatch("listenPubSubMessage", { 44 | target, 45 | callback 46 | }); 47 | }, 48 | unlisten(target, callback) { 49 | return store.dispatch("unlistenPubSubMessage", { 50 | target, 51 | callback 52 | }); 53 | }, 54 | actions: { 55 | followChannel(channelName) { 56 | return store.dispatch("followChannel", channelName); 57 | }, 58 | minimize() { 59 | return store.dispatch("minimize"); 60 | }, 61 | onFollow(callback) { 62 | return store.dispatch("onFollow", callback); 63 | }, 64 | requestIdShare() { 65 | return store.dispatch("requestIdShare"); 66 | } 67 | }, 68 | bits: { 69 | //custom 70 | getBitsAmount(sku) { 71 | return store.dispatch("getBitsAmount", sku); 72 | }, 73 | //custom 74 | get hasOngoingBitTransaction() { 75 | return store.state[module].bits.hasOngoingBitTransaction; 76 | }, 77 | getProducts() { 78 | return store.dispatch("getProducts"); 79 | }, 80 | onTransactionCancelled(callback) { 81 | return store.dispatch("onTransactionCancelled", callback); 82 | }, 83 | onTransactionComplete(callback) { 84 | return store.dispatch("onTransactionComplete", callback); 85 | }, 86 | setUseLoopBack() { 87 | return store.dispatch("setUseLoopBack"); 88 | }, 89 | showBitsBalance() { 90 | return store.dispatch("showBitsBalance"); 91 | }, 92 | useBits(sku) { 93 | return store.dispatch("useBits", sku); 94 | } 95 | }, 96 | channel: { 97 | // custom 98 | get id() { 99 | return store.state[module].channel.channelId; 100 | }, 101 | // custom 102 | get initialized() { 103 | return store.state[module].channel.initialized; 104 | } 105 | }, 106 | configuration: { 107 | // custom 108 | get initialized() { 109 | return store.state[module].configuration.initialized; 110 | }, 111 | get broadcaster() { 112 | return store.state[module].configuration.broadcaster; 113 | }, 114 | get developer() { 115 | return store.state[module].configuration.developer; 116 | }, 117 | get global() { 118 | return store.state[module].configuration.global; 119 | }, 120 | onChanged(callback) { 121 | return store.dispatch("onConfigurationChanged", callback); 122 | }, 123 | set(segment, version, content) { 124 | return store.dispatch("setSegment", { 125 | segment, 126 | version, 127 | content 128 | }); 129 | } 130 | }, 131 | context: { 132 | // custom 133 | get initialized() { 134 | return store.state[module].context.initialized; 135 | }, 136 | get arePlayerControlsVisible() { 137 | return store.state[module].context.arePlayerControlsVisible; 138 | }, 139 | get bitrate() { 140 | return store.state[module].context.bitrate; 141 | }, 142 | get bufferSize() { 143 | return store.state[module].context.bufferSize; 144 | }, 145 | get displayResolution() { 146 | return store.state[module].context.displayResolution; 147 | }, 148 | get game() { 149 | return store.state[module].context.game; 150 | }, 151 | get hlsLatencyBroadcaster() { 152 | return store.state[module].context.hlsLatencyBroadcaster; 153 | }, 154 | get hostingInfo() { 155 | return store.state[module].context.hostingInfo; 156 | }, 157 | get isFullScreen() { 158 | return store.state[module].context.isFullScreen; 159 | }, 160 | get isMuted() { 161 | return store.state[module].context.isMuted; 162 | }, 163 | get isPaused() { 164 | return store.state[module].context.isPaused; 165 | }, 166 | get isTheatreMode() { 167 | return store.state[module].context.isTheatreMode; 168 | }, 169 | get language() { 170 | return store.state[module].context.language; 171 | }, 172 | get mode() { 173 | return store.state[module].context.mode; 174 | }, 175 | get playbackMode() { 176 | return store.state[module].context.playbackMode; 177 | }, 178 | get theme() { 179 | return store.state[module].context.theme; 180 | }, 181 | get videoResolution() { 182 | return store.state[module].context.videoResolution; 183 | }, 184 | get volume() { 185 | return store.state[module].context.volume; 186 | } 187 | }, 188 | highlight: { 189 | get isHighlighted() { 190 | return store.state[module].highlight.isHighlighted; 191 | } 192 | }, 193 | features: { 194 | get isChatEnabled() { 195 | return store.state[module].features.isChatEnabled; 196 | }, 197 | get isSubscriptionStatusAvailable() { 198 | return store.state[module].features.isSubscriptionStatusAvailable; 199 | }, 200 | get isBitsEnabled() { 201 | return store.state[module].features.isBitsEnabled; 202 | }, 203 | onChanged(callback) { 204 | return store.dispatch("onFeatureChanged", callback); 205 | } 206 | }, 207 | position: { 208 | // custom 209 | get initialized() { 210 | return store.state[module].position.initialized; 211 | }, 212 | get position() { 213 | return store.state[module].position.position; 214 | } 215 | }, 216 | queryParams: { 217 | get anchor() { 218 | return store.state[module].queryParams.anchor; 219 | }, 220 | get language() { 221 | return store.state[module].queryParams.language; 222 | }, 223 | get locale() { 224 | return store.state[module].queryParams.locale; 225 | }, 226 | get mode() { 227 | return store.state[module].queryParams.mode; 228 | }, 229 | get platform() { 230 | return store.state[module].queryParams.platform; 231 | }, 232 | get popout() { 233 | return store.state[module].queryParams.popout; 234 | }, 235 | get state() { 236 | return store.state[module].queryParams.state; 237 | }, 238 | get legacyComponentDesign() { 239 | return store.state[module].queryParams.legacyComponentDesign; 240 | } 241 | }, 242 | purchases: { 243 | // TODO 244 | }, 245 | rig: { 246 | // TODO 247 | }, 248 | viewer: { 249 | // custom 250 | get initialized() { 251 | return store.state[module].viewer.initialized; 252 | }, 253 | get id() { 254 | return store.state[module].viewer.userId; 255 | }, 256 | get opaqueId() { 257 | return store.state[module].viewer.opaqueId; 258 | }, 259 | get role() { 260 | return store.state[module].viewer.role; 261 | }, 262 | get isLinked() { 263 | return store.state[module].viewer.isLinked; 264 | }, 265 | get sessionToken() { 266 | return store.state[module].viewer.sessionToken; 267 | }, 268 | get subscriptionStatus() { 269 | return store.state[module].viewer.subscriptionStatus; 270 | } 271 | } 272 | }; 273 | } 274 | }; 275 | 276 | export const ExtensionPlugin = plugin; 277 | 278 | export default plugin; 279 | -------------------------------------------------------------------------------- /src/script/init.js: -------------------------------------------------------------------------------- 1 | import TwitchExtensionHelperNotFound from "../errors/TwitchExtensionHelperNotFound"; 2 | 3 | export default { 4 | store: null, 5 | 6 | /** 7 | * Basic initialisation of the VueX store for Twitch Extensions 8 | * @param store The VueX store instance 9 | * @param settings settings for the store 10 | */ 11 | init(store,settings) { 12 | if (window.Twitch.ext) { 13 | this.store = store; 14 | this.initListeners(settings); 15 | } else { 16 | throw new TwitchExtensionHelperNotFound(); 17 | } 18 | }, 19 | 20 | initTestingFlags(settings){ 21 | const flags = settings.forceFlags 22 | if (flags && flags.forceIsBitsEnabled){ 23 | this.store.dispatch('setExtensionForceIsBitsEnabled',flags.forceIsBitsEnabled) 24 | } 25 | }, 26 | 27 | initListeners(settings) { 28 | this.initTestingFlags(settings); 29 | this.initExtensionInfo() 30 | this.initFeatureFlags(); 31 | this.initClientQueryParams(); 32 | this.initConfigurationService(); 33 | this.initOnAuthorized(); 34 | this.initOnContext(); 35 | this.onHighlightChanged(); 36 | this.initOnPositionChanged(); 37 | }, 38 | initExtensionInfo(){ 39 | this.store.dispatch('setExtensionInfo') 40 | }, 41 | 42 | initFeatureFlags() { 43 | let warned = false; 44 | window.Twitch.ext.features.onChanged(() => { 45 | if (this.store.state.forceIsBitsEnabled && !warned && this.store.state.queryParams.state !== "testing"){ 46 | console.warn("TwitchExt-Vuex: You have the force flag forceIsBitsEnabled activated, remember to disable it when releasing your extension.") 47 | warned = true; 48 | } 49 | this.store.dispatch("updateFeatures",{ 50 | forceIsBitsEnabled: this.store.state.forceIsBitsEnabled 51 | }); 52 | }); 53 | }, 54 | initClientQueryParams() { 55 | this.store.dispatch("updateQueryParams"); 56 | }, 57 | 58 | initOnAuthorized() { 59 | window.Twitch.ext.onAuthorized( auth => { 60 | this.store.dispatch("updateUserInfo"); 61 | if (!this.initialized) { 62 | this.store.dispatch("setChannelId", auth.channelId); 63 | } 64 | }); 65 | }, 66 | 67 | initOnContext() { 68 | window.Twitch.ext.onContext( (context, contextFields) => { 69 | this.store.dispatch("updateContext", { 70 | context, 71 | contextFields 72 | }); 73 | }); 74 | }, 75 | 76 | initOnPositionChanged() { 77 | window.Twitch.ext.onPositionChanged( position => { 78 | this.store.dispatch("updatePosition", position); 79 | }); 80 | }, 81 | 82 | initConfigurationService() { 83 | window.Twitch.ext.configuration.onChanged(() => { 84 | this.store.dispatch( 85 | "updateConfiguration", 86 | window.Twitch.ext.configuration 87 | ); 88 | }); 89 | }, 90 | onHighlightChanged() { 91 | window.Twitch.ext.onHighlightChanged(isHighlighted => { 92 | this.store.dispatch("updateHighlight", isHighlighted); 93 | }); 94 | } 95 | }; 96 | --------------------------------------------------------------------------------