├── .gitignore ├── LICENSE ├── README.md ├── composer.json └── src ├── Casts └── PhoneCast.php ├── Exceptions ├── PhoneAuthLimitException.php └── PhoneAuthSmsServiceNotFoundException.php ├── Livewire └── PhoneVerification.php ├── Models ├── ConfirmedPhone.php ├── PhoneVerificationCode.php └── Traits │ └── PhoneVerification.php ├── Providers └── PhoneAuthServiceProvider.php ├── Rules └── PhoneNumber.php ├── SmsBox.php ├── SmsServiceExample.php ├── SmsServiceInterface.php ├── config └── phone_auth.php ├── database └── migrations │ └── 2021_02_18_090500_users_phone_auth.php ├── lang ├── en │ └── phone_auth.php └── ru │ └── phone_auth.php └── views └── livewire └── phone-verification.blade.php /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | /vendor/ 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Danil Shutsky 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # laravel-phone-auth 2 | 3 | https://youtu.be/UaYx2VgXMEY 4 | 5 | ## Important 6 | - Need laravel livewire package 7 | - The default template uses tailwind classes (customize it if you want) 8 | 9 | ## Install 10 | 11 | - install livewire 12 | 13 | - install doctrine/dbal 14 | 15 | - composer require lee-to/laravel-phone-auth 16 | 17 | - php artisan vendor:publish --provider="Leeto\PhoneAuth\Providers\PhoneAuthServiceProvider" 18 | 19 | - configure config/phone_auth.php 20 | 21 | ### Usage 22 | 23 | ### User Model 24 | 25 | - Add PhoneVerification Trait to User Model 26 | 27 | ``` php 28 | use PhoneVerification; 29 | ``` 30 | 31 | - Add phone cast to User Model 32 | 33 | ``` php 34 | protected $casts = [ 35 | 'phone' => PhoneCast::class 36 | ]; 37 | ``` 38 | 39 | ### Blade component 40 | #### Auth/Phone verification form 41 | 42 | - Simple 43 | ``` html 44 | @livewire('phone-verification') 45 | ``` 46 | 47 | - Without form wrap 48 | 49 | ``` html 50 | @livewire('phone-verification', ['formWrap' => false]) 51 | ``` 52 | 53 | - Register new or login if phone verified and exist 54 | 55 | ``` html 56 | @livewire('phone-verification', ['loginAndRegister' => true]) 57 | ``` 58 | 59 | ### Check phone confirmed 60 | 61 | \Leeto\PhoneAuth\Models\ConfirmedPhone::confirmed($phone, $user_id = null); 62 | 63 | 64 | #### Components properties (override config) 65 | - stopEvents (bool) = turn off emitBefore, emitAfter 66 | - customRedirectTo (bool|array) = redirect after success 67 | - emptyCustomFields (bool) = disable custom fields 68 | - customParams (array) = send custom properties to view 69 | 70 | ``` html 71 | 79 | ``` 80 | 81 | 82 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lee-to/laravel-phone-auth", 3 | "description": "Laravel+livewire phone auth module", 4 | "keywords": ["laravel", "livewire", "phone auth", "phone register", "phone login"], 5 | "type": "library", 6 | "homepage": "https://github.com/lee-to/laravel-phone-auth", 7 | "require": { 8 | "php": "^7.3|^8.0", 9 | "doctrine/dbal": "^3.0" 10 | }, 11 | "require-dev": { 12 | "phpunit/phpunit": "8.5.x-dev", 13 | "mockery/mockery": "^1.0" 14 | }, 15 | "license": "MIT", 16 | "authors": [ 17 | { 18 | "name": "Danil Shutsky", 19 | "email": "leetodev@ya.ru", 20 | "homepage": "https://github.com/lee-to" 21 | } 22 | ], 23 | "minimum-stability": "dev", 24 | "autoload": { 25 | "psr-4": { 26 | "Leeto\\PhoneAuth\\": "src/" 27 | } 28 | }, 29 | "autoload-dev": { 30 | "psr-4": { 31 | "Leeto\\PhoneAuth\\Tests\\": "tests/" 32 | } 33 | }, 34 | "extra": { 35 | "laravel": { 36 | "providers": [ 37 | "Leeto\\PhoneAuth\\Providers\\PhoneAuthServiceProvider" 38 | ] 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Casts/PhoneCast.php: -------------------------------------------------------------------------------- 1 | 'verification', 115 | 'phoneVerificationDefaultParams' => 'defaultParams' 116 | ]; 117 | 118 | /** 119 | * @return array 120 | */ 121 | protected function rules() 122 | { 123 | $rules = [ 124 | 'phone' => ['required', new PhoneNumber()], 125 | 'code' => ['nullable', 'sometimes'] 126 | ]; 127 | 128 | $customFieldsRules = $this->customFieldsRules ? collect($this->customFieldsRules) : collect(config("phone_auth.custom_fields_rules")); 129 | 130 | if($this->emptyCustomFields) { 131 | $customFieldsRules = collect([]); 132 | } 133 | 134 | $newRules = $customFieldsRules->mapWithKeys(function ($value, $key) { 135 | return ["fieldsValues.{$key}" => $value]; 136 | })->merge($rules); 137 | 138 | 139 | 140 | return $newRules->toArray(); 141 | } 142 | 143 | /** 144 | * 145 | */ 146 | public function mount() { 147 | $customFields = $this->customFields ? $this->customFields : config("phone_auth.custom_fields", []); 148 | 149 | if($this->emptyCustomFields) { 150 | $customFields = []; 151 | } 152 | 153 | $this->fields = $this->fields ? $this->fields : $customFields; 154 | 155 | $this->sendCountRemain(); 156 | } 157 | 158 | public function defaultParams($params) { 159 | $this->fieldsValues = collect($this->fieldsValues)->merge($params)->toArray(); 160 | } 161 | 162 | /** 163 | * 164 | */ 165 | public function nextSend() { 166 | $this->nextSend = PhoneVerificationCode::nextSend(); 167 | } 168 | 169 | /** 170 | * 171 | */ 172 | public function sendCountRemain() { 173 | $this->sendCountRemain = PhoneVerificationCode::sendCountRemain(); 174 | } 175 | 176 | /** 177 | * @param false $resend 178 | */ 179 | public function send($resend = false) { 180 | try { 181 | $this->phoneVerificationCode = PhoneVerificationCode::sendCode($this->phone); 182 | 183 | $this->nextSend(); 184 | $this->sendCountRemain(); 185 | 186 | $this->resetErrorBag(); 187 | $this->resetValidation(); 188 | 189 | $this->eventsSendCode(); 190 | } catch (PhoneAuthLimitException $e) { 191 | $this->addError($resend ? "code" : "phone", $e->getMessage()); 192 | } 193 | } 194 | 195 | /** 196 | * 197 | */ 198 | public function change() { 199 | $this->reset(["phone", "code", "successfullyConfirmed", "phoneVerificationCode"]); 200 | } 201 | 202 | /** 203 | * @return \Illuminate\Contracts\Auth\Factory|\Illuminate\Contracts\Auth\Guard|\Illuminate\Contracts\Auth\StatefulGuard 204 | */ 205 | protected function getAuth() { 206 | return auth(config("phone_auth.auth.guard")); 207 | } 208 | 209 | /** 210 | * @param $value 211 | */ 212 | public function updatingCode($value) { 213 | $value = Str::of($value)->replace(" ", ""); 214 | 215 | if(config("phone_auth.verify_code_dynamic") && Str::length($value) == config("phone_auth.code_length")) { 216 | $this->code = $value; 217 | 218 | $this->dispatchBrowserEvent('code-loading'); 219 | 220 | $this->verification(); 221 | } else { 222 | $this->resetErrorBag(["code"]); 223 | $this->resetValidation(["code"]); 224 | } 225 | } 226 | 227 | /** 228 | * @param $value 229 | */ 230 | public function updatingPhone($value) { 231 | if($this->successfullyConfirmed && $this->confirmedPhone != $value) { 232 | $this->reset(["code", "confirmedPhone", "successfullyConfirmed", "phoneVerificationCode"]); 233 | } 234 | } 235 | 236 | /** 237 | * 238 | */ 239 | public function verification() { 240 | if($this->successfullyConfirmed) { 241 | $this->eventsAfter(); 242 | } else { 243 | $this->dispatchBrowserEvent('phone-verification'); 244 | 245 | $this->eventsBefore(); 246 | 247 | $this->validate(); 248 | 249 | if($this->loginAndRegister == false && !User::isUniquePhone($this->phone)) { 250 | $this->addError("phone", __("phone_auth::phone_auth.validation.phone_already_use")); 251 | 252 | if($this->phoneVerificationCode) { 253 | $this->change(); 254 | } 255 | } else { 256 | if($this->phoneVerificationCode) { 257 | if(PhoneVerificationCode::validateCode($this->code)) { 258 | try { 259 | if(config("phone_auth.auth.createUser")) { 260 | $userModel = app(config("phone_auth.auth.class_user")); 261 | 262 | $fields = collect($this->fields)->mapWithKeys(function ($item) { 263 | return [$item => $this->fieldsValues[$item] ?? '']; 264 | }); 265 | 266 | $formatPhoneNumber = Str::phoneNumber($this->phone); 267 | 268 | $createData = Arr::add($fields->toArray(), "phone", $formatPhoneNumber); 269 | 270 | $user = $this->getAuth()->check() ? $this->getAuth()->user() : $userModel->firstOrCreate(["phone" => $formatPhoneNumber], $createData); 271 | $user->setVerifiedPhone($this->phone); 272 | 273 | if(config("phone_auth.auth.loginAfter") && !$this->getAuth()->check()) { 274 | $this->getAuth()->login($user); 275 | } 276 | 277 | ConfirmedPhone::firstOrCreate(["phone" => $this->phone]); 278 | } 279 | 280 | $this->phone = $this->phoneVerificationCode->phone; 281 | $this->successfullyConfirmed = true; 282 | $this->confirmedPhone = $this->phone; 283 | 284 | $this->eventsAfter(); 285 | 286 | $this->reset(["code", "sendCountRemain", "phoneVerificationCode", "nextSend"]); 287 | 288 | if(config("phone_auth.flushCode")) { 289 | PhoneVerificationCode::flush(); 290 | } 291 | 292 | $this->dispatchBrowserEvent('phone-verification-done'); 293 | 294 | $redirectTo = $this->customRedirectTo ? $this->customRedirectTo : config("phone_auth.auth.redirectTo"); 295 | 296 | if($redirectTo) { 297 | $this->redirect($redirectTo); 298 | } 299 | } catch (\Exception $e) { 300 | $this->addError("code", __("phone_auth::phone_auth.validation.error")); 301 | } 302 | } else { 303 | $this->addError("code", __("phone_auth::phone_auth.validation.code_invalid")); 304 | } 305 | } else { 306 | $this->send(); 307 | } 308 | } 309 | } 310 | } 311 | 312 | /** 313 | * 314 | */ 315 | protected function eventsSendCode() { 316 | if(!$this->stopEvents && config("phone_auth.emitSendCode")) { 317 | $this->emit(config("phone_auth.emitSendCode"), [ 318 | "phone" => Str::phoneNumber($this->phone), 319 | "values" => $this->fieldsValues, 320 | ]); 321 | } 322 | } 323 | 324 | /** 325 | * 326 | */ 327 | protected function eventsBefore() { 328 | if(!$this->stopEvents && config("phone_auth.emitBefore")) { 329 | $this->emit(config("phone_auth.emitBefore"), [ 330 | "success" => $this->successfullyConfirmed, 331 | "phone" => Str::phoneNumber($this->phone), 332 | "values" => $this->fieldsValues, 333 | ]); 334 | } 335 | } 336 | 337 | /** 338 | * 339 | */ 340 | protected function eventsAfter() { 341 | if(!$this->stopEvents && config("phone_auth.emitAfter")) { 342 | $this->emit(config("phone_auth.emitAfter"), [ 343 | "success" => $this->successfullyConfirmed, 344 | "phone" => Str::phoneNumber($this->phone), 345 | "values" => $this->fieldsValues, 346 | ]); 347 | } 348 | } 349 | 350 | /** 351 | * @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View 352 | */ 353 | public function render() 354 | { 355 | return view('phone_auth::livewire.phone-verification'); 356 | } 357 | } 358 | -------------------------------------------------------------------------------- /src/Models/ConfirmedPhone.php: -------------------------------------------------------------------------------- 1 | PhoneCast::class 27 | ]; 28 | 29 | /** 30 | * 31 | */ 32 | public static function boot() 33 | { 34 | parent::boot(); 35 | 36 | self::creating(function (Model $model) { 37 | $model->user_id = auth(config("phone_auth.auth.guard"))->check() ? auth(config("phone_auth.auth.guard"))->id() : null; 38 | $model->ip = request()->ip(); 39 | }); 40 | } 41 | 42 | 43 | /** 44 | * @param $phone 45 | * @param null $user_id 46 | * @return int 47 | */ 48 | public static function confirmed($phone, $user_id = null) { 49 | return self::query()->where(["phone" => Str::phoneNumber($phone)])->when($user_id, function (Builder $query) use($user_id) { 50 | return $query->where(["user_id" => $user_id]); 51 | })->when(is_null($user_id), function (Builder $query) { 52 | return $query->where(["ip" => request()->ip()]); 53 | })->count(); 54 | } 55 | 56 | /** 57 | * @return mixed 58 | */ 59 | public static function flush() { 60 | return self::query()->where("ip", request()->ip())->delete(); 61 | } 62 | 63 | /** 64 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo 65 | */ 66 | public function user() { 67 | return $this->belongsTo(config("phone_auth.auth.class_user")); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Models/PhoneVerificationCode.php: -------------------------------------------------------------------------------- 1 | PhoneCast::class 28 | ]; 29 | 30 | /** 31 | * 32 | */ 33 | public static function boot() 34 | { 35 | parent::boot(); 36 | 37 | self::creating(function (Model $model) { 38 | if(config("phone_auth.code_digits_only")) { 39 | $code = rand(pow(10, config("phone_auth.code_length")-1), pow(10, config("phone_auth.code_length"))-1); 40 | } else { 41 | $code = Str::upper(Str::random(config("phone_auth.code_length"))); 42 | } 43 | 44 | $model->user_id = self::getUserId(); 45 | $model->ip = self::getIp(); 46 | $model->code = $code; 47 | $model->expires_at = now()->addSeconds(config("phone_auth.expire_seconds")); 48 | 49 | app("sms")->send($model->phone, __("phone_auth::phone_auth.sms.text", ["code" => $code])); 50 | }); 51 | } 52 | 53 | /** 54 | * @param Builder $query 55 | * @return Builder 56 | */ 57 | public function scopeUnexpired(Builder $query): Builder 58 | { 59 | return $query->where('ip', self::getIp())->where('expires_at', '>=', now()); 60 | } 61 | 62 | /** 63 | * @param Builder $query 64 | * @return Builder 65 | */ 66 | public function scopeLimiter(Builder $query): Builder 67 | { 68 | return $query->where('ip', self::getIp())->whereDate('expires_at', '=', now()); 69 | } 70 | 71 | /** 72 | * @return float|int 73 | */ 74 | public static function nextSend() { 75 | $nexSendTimestamp = self::query()->unexpired()->max('created_at'); 76 | 77 | $nexSendTimestamp = $nexSendTimestamp ? now()->setDateTimeFrom($nexSendTimestamp)->addSeconds(config("phone_auth.next_send_after")) : now(); 78 | 79 | return $nexSendTimestamp->isPast() ? 0 : now()->diffInSeconds($nexSendTimestamp); 80 | } 81 | 82 | /** 83 | * @return mixed 84 | */ 85 | public static function sendCountRemain() { 86 | $sendCount = config("phone_auth.limit_send_count") - self::query()->limiter()->count(); 87 | 88 | return $sendCount < 0 ? 0 : $sendCount; 89 | } 90 | 91 | /** 92 | * @return mixed 93 | */ 94 | public static function sendCount() { 95 | return self::query()->limiter()->count(); 96 | } 97 | 98 | /** 99 | * @param $phone 100 | * @return mixed 101 | * @throws PhoneAuthLimitException 102 | */ 103 | public static function sendCode($phone) { 104 | $nextSend = self::nextSend(); 105 | $sendCount = self::sendCount(); 106 | 107 | if($nextSend > 0) { 108 | throw new PhoneAuthLimitException(__("phone_auth::phone_auth.ui.sms_count_limit", ["seconds" => $nextSend])); 109 | } 110 | 111 | if(config("phone_auth.limit_send_count") > $sendCount) { 112 | return self::create(["phone" => $phone]); 113 | } else { 114 | throw new PhoneAuthLimitException(__("phone_auth::phone_auth.ui.send_count", ["count" => config("phone_auth.limit_send_count")])); 115 | } 116 | } 117 | 118 | /** 119 | * @param $code 120 | * @return mixed 121 | */ 122 | public static function validateCode($code) { 123 | return self::query()->unexpired()->where("code", $code)->count(); 124 | } 125 | 126 | /** 127 | * @return null|string 128 | */ 129 | protected static function getIp() { 130 | return request()->ip(); 131 | } 132 | 133 | /** 134 | * @return int|null|string 135 | */ 136 | protected static function getUserId() { 137 | return auth(config("phone_auth.auth.guard"))->check() ? auth(config("phone_auth.auth.guard"))->id() : null; 138 | } 139 | 140 | /** 141 | * @return mixed 142 | */ 143 | public static function flush() { 144 | return self::query()->where("ip", self::getIp())->delete(); 145 | } 146 | 147 | /** 148 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo 149 | */ 150 | public function user() { 151 | return $this->belongsTo(config("phone_auth.class_user")); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/Models/Traits/PhoneVerification.php: -------------------------------------------------------------------------------- 1 | where("phone", Str::phoneNumber($phone))->where("phone_verified", true)->count(); 20 | } 21 | 22 | /** 23 | * @return mixed 24 | */ 25 | public function hasVerifiedPhone() 26 | { 27 | return $this->phone_verified; 28 | } 29 | 30 | /** 31 | * @param $phone 32 | * @return bool 33 | */ 34 | public function setVerifiedPhone($phone) 35 | { 36 | $this->forceFill([ 37 | 'phone' => $phone, 38 | 'phone_verified' => true, 39 | 'phone_verified_at' => $this->freshTimestamp(), 40 | ]); 41 | 42 | return $this->save(); 43 | } 44 | } -------------------------------------------------------------------------------- /src/Providers/PhoneAuthServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->singleton('sms', function ($app) { 26 | return (new SmsBox())->smsService(); 27 | }); 28 | } 29 | 30 | /** 31 | * Bootstrap services. 32 | * 33 | * @return void 34 | */ 35 | public function boot() 36 | { 37 | $path = __DIR__ . "/.."; 38 | 39 | /* Config */ 40 | $this->publishes([ 41 | $path . '/config/' . $this->namespace . '.php' => config_path($this->namespace . '.php'), 42 | ]); 43 | 44 | /* Views */ 45 | $this->loadViewsFrom($path . '/views', $this->namespace); 46 | 47 | $this->publishes([ 48 | $path . '/views' => resource_path('views/vendor/' . $this->namespace), 49 | ]); 50 | 51 | /* Translates */ 52 | 53 | $this->loadTranslationsFrom($path . '/lang', $this->namespace); 54 | 55 | $this->publishes([ 56 | $path . '/lang' => resource_path('lang/vendor/' . $this->namespace), 57 | ]); 58 | 59 | Livewire::component('phone-verification', PhoneVerification::class); 60 | 61 | /* Migrations */ 62 | $this->loadMigrationsFrom($path . '/database/migrations'); 63 | 64 | Str::macro('phoneNumber', function ($string) { 65 | return preg_replace('/^8{1}/', '7', preg_replace('/[^0-9]/', '', $string)); 66 | }); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Rules/PhoneNumber.php: -------------------------------------------------------------------------------- 1 | smsService = new $smsServiceClass(); 34 | $this->smsService->settings(config("phone_auth.sms_service.settings")); 35 | } 36 | } 37 | 38 | /** 39 | * @return SmsServiceInterface 40 | */ 41 | public function smsService() : SmsServiceInterface { 42 | return $this->smsService; 43 | } 44 | } -------------------------------------------------------------------------------- /src/SmsServiceExample.php: -------------------------------------------------------------------------------- 1 | [ 5 | "class_user" => "App\\Models\\User", 6 | "guard" => "web", 7 | "createUser" => true, 8 | "loginAfter" => true, 9 | "redirectTo" => false, 10 | ], 11 | 12 | "emitSendCode" => false, 13 | "emitBefore" => false, 14 | "emitAfter" => false, 15 | 16 | "code_length" => 4, 17 | "code_digits_only" => false, 18 | "verify_code_dynamic" => false, 19 | "limit_send_count" => 3, 20 | "next_send_after" => 30, 21 | "expire_seconds" => 240, 22 | "flushCode" => true, 23 | 24 | "custom_phone_field_name" => "phone", //If you need to use without a form 25 | 26 | "custom_fields" => ["name", "email"], 27 | "custom_fields_rules" => [ 28 | "name" => "required", 29 | "email" => "required|email", 30 | ], 31 | 32 | "sms_service" => [ 33 | "class" => \Leeto\PhoneAuth\SmsServiceExample::class, 34 | "settings" => [], 35 | ] 36 | ]; 37 | -------------------------------------------------------------------------------- /src/database/migrations/2021_02_18_090500_users_phone_auth.php: -------------------------------------------------------------------------------- 1 | string('phone')->unique()->nullable()->after("id"); 21 | } 22 | 23 | if (!Schema::hasColumn('users', 'phone_verified')) { 24 | $table->boolean('phone_verified')->default(false)->after("phone"); 25 | } 26 | 27 | if (!Schema::hasColumn('users', 'phone_verified_at')) { 28 | $table->timestamp('phone_verified_at')->nullable()->after("phone_verified"); 29 | } 30 | 31 | if (Schema::hasColumn('users', 'email')) { 32 | $table->string('email')->nullable()->change(); 33 | } 34 | 35 | if (Schema::hasColumn('users', 'name')) { 36 | $table->string('name')->nullable()->change(); 37 | } 38 | 39 | if (Schema::hasColumn('users', 'password')) { 40 | $table->string('password')->nullable()->change(); 41 | } 42 | }); 43 | } 44 | 45 | if (!Schema::hasTable('phone_verification_codes')) { 46 | Schema::create('phone_verification_codes', function (Blueprint $table) { 47 | $table->id(); 48 | 49 | $table->foreignId('user_id') 50 | ->nullable() 51 | ->constrained() 52 | ->onUpdate('cascade') 53 | ->onDelete('cascade') 54 | ; 55 | 56 | $table->ipAddress('ip'); 57 | $table->string('phone'); 58 | $table->string('code'); 59 | $table->timestamp('expires_at'); 60 | 61 | $table->timestamps(); 62 | }); 63 | } 64 | 65 | if (!Schema::hasTable('confirmed_phones')) { 66 | Schema::create('confirmed_phones', function (Blueprint $table) { 67 | $table->id(); 68 | 69 | $table->ipAddress('ip'); 70 | $table->foreignId('user_id') 71 | ->nullable() 72 | ->constrained() 73 | ->onUpdate('cascade') 74 | ->onDelete('cascade') 75 | ; 76 | $table->string('phone'); 77 | 78 | $table->timestamps(); 79 | }); 80 | } 81 | } 82 | 83 | 84 | } 85 | 86 | /** 87 | * Reverse the migrations. 88 | * 89 | * @return void 90 | */ 91 | public function down() 92 | { 93 | Schema::dropColumns('users', ['phone', 'phone_verified', 'phone_verified_at']); 94 | Schema::dropIfExists('phone_verification_codes'); 95 | Schema::dropIfExists('confirmed_phones'); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/lang/en/phone_auth.php: -------------------------------------------------------------------------------- 1 | [ 5 | "current_phone" => "Current phone", 6 | "change" => "Change", 7 | "next_send" => "Next send after :seconds sec.", 8 | "send_count" => "Send count limit - :count", 9 | "send_count_remain" => "Send count remain - :count", 10 | "sms_count_limit" => "Sms sending limit wait. Try after :seconds sec.", 11 | "send_again" => "Send again", 12 | "check_code" => "Check Code", 13 | "send_code" => "Send Code", 14 | "phone_field_label" => "Phone", 15 | "code_field_label" => "Code", 16 | "phone_verified_message" => "Phone verified", 17 | ], 18 | "validation" => [ 19 | "phone_format" => "Incorrect phone format", 20 | "phone_already_use" => "The phone is already in use", 21 | "code_invalid" => "Code is invalid", 22 | "error" => "Something went wrong" 23 | ], 24 | "sms" => [ 25 | "text" => ":code - Your code", 26 | ] 27 | ]; -------------------------------------------------------------------------------- /src/lang/ru/phone_auth.php: -------------------------------------------------------------------------------- 1 | [ 5 | "current_phone" => "Ваш телефон", 6 | "change" => "Изменить", 7 | "next_send" => "Повторить отправку смс можно через :seconds сек.", 8 | "send_count" => "Ограничение в количестве отправок - :count", 9 | "send_count_remain" => "Осталось повторных попыток - :count", 10 | "sms_count_limit" => "Ограничение в отправке смс. Попробуйте через :seconds секунд.", 11 | "send_again" => "Отправить снова", 12 | "check_code" => "Проверить", 13 | "send_code" => "Отправить код", 14 | "phone_field_label" => "Телефон", 15 | "code_field_label" => "Код", 16 | "phone_verified_message" => "Телефон подтвержден", 17 | ], 18 | "validation" => [ 19 | "phone_format" => "Формат номера телефона введен не правильно", 20 | "phone_already_use" => "Номер телефона уже используется", 21 | "code_invalid" => "Код введен не правильно", 22 | "error" => "Произошла ошибка", 23 | ], 24 | "sms" => [ 25 | "text" => ":code - Ваш проверочный код", 26 | ] 27 | ]; -------------------------------------------------------------------------------- /src/views/livewire/phone-verification.blade.php: -------------------------------------------------------------------------------- 1 |
2 | @if($formWrap)
@endif 3 | 4 | @foreach($fields as $field) 5 |
6 | 8 | @error('fieldsValues.' . $field)
{{ $message }}
@enderror 9 |
10 | @endforeach 11 | 12 | @if($successfullyConfirmed) 13 | 14 | 15 | 16 |

@lang("phone_auth::phone_auth.ui.phone_verified_message")

17 | @else 18 | 19 | @if($phoneVerificationCode) 20 |
21 | @lang("phone_auth::phone_auth.ui.current_phone") {{ $phone }} 22 | @lang("phone_auth::phone_auth.ui.change") 23 |
24 | 25 |
26 | 30 | @error('code')
{{ $message }}
@enderror 31 |
32 | 33 | @if($nextSend) 34 |
@lang("phone_auth::phone_auth.ui.next_send", ["seconds" => $nextSend])
35 | @else 36 | @if($sendCountRemain) 37 | 38 | @lang("phone_auth::phone_auth.ui.send_again") 39 | 40 | @endif 41 | 42 |
43 | @lang("phone_auth::phone_auth.ui.send_count_remain", ["count" => $sendCountRemain]) 44 |
45 | @endif 46 | @else 47 |
48 | 50 | @error('phone')
{{ $message }}
@enderror 51 |
52 | @endif 53 | 54 | 57 | @endif 58 | 59 | @if($formWrap)
@endif 60 |
61 | --------------------------------------------------------------------------------